Repository: etcd-io/etcd

Issue:
mvcc: negative watcher count gauges
### Bug report criteria

- [x] This bug report is not security related, security issues should be disclosed privately via [etcd maintainers](mailto:etcd-maintainers@googlegroups.com).
- [x] This is not a support request or question, support requests or questions should be raised in the etcd [discussion forums](https://github.com/etcd-io/etcd/discussions).
- [x] You have read the etcd [bug reporting guidelines](https://github.com/etcd-io/etcd/blob/main/Documentation/contributor-guide/reporting_bugs.md).
- [x] Existing open issues along with etcd [frequently asked questions](https://etcd.io/docs/latest/faq) have been checked and this is not a duplicate.

### What happened?

We observe a slow negative trend on the `etcd_debugging_mvcc_watcher_total` gauge—implying there are unpaired increment/decrement operations:

<img width="1728" alt="Image" src="https://github.com/user-attachments/assets/63a6476f-f791-4c49-a04e-0a46e49e6283" />

### What did you expect to happen?

Logically the active watcher gauge should never be negative.

### How can we reproduce it (as minimally and precisely as possible)?

I think this issue is very closely related to [this PR][0] which previously attempted to address negative watch counts from double decrements—but covers specifically the case when `wa.compacted` is true, and there is a `cancel/close` race. It's not so easy to reproduce as it is a race condition I believe, but I think you can somewhat provoke the scenario by contriving a unit test. 

Consider for example adding this test to `mvcc/watcher_test.go`:

```go
func TestDoubledDecrementWithCompactedWatcher(t *testing.T) {
	b, _ := betesting.NewDefaultTmpBackend(t)
	s := New(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
	defer cleanup(s, b)

	w := s.NewWatchStream()

	wid, err := w.Watch(0, []byte("foo"), nil, 0)
	if err != nil {
		t.Fatalf("failed to watch: %v", err)
	}

	// pretend this is now compacted
	for id, wa := range w.(*watchStream).watchers {
		if wid == id {
			wa.compacted = true
		}
	}

	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		w.Cancel(wid)
		wg.Done()
	}()

	time.Sleep(time.Millisecond * 50)

	go func() {
		w.Close()
		wg.Done()
	}()

	wg.Wait()

	t.Fatalf("print output")
}
```

> [!NOTE]
> I provoke the `wa.compacted` state by just directly setting it here—I'm not sure if in practice the act of compaction might somehow change this scenario, but just reading the code I'm struggling to see how a double decrement could occur on any other branch, so am somewhat surmising this must be the state we're in.  

and then to provoke the race add a delay in the unlocked portion of [watcher.go's `Cancel(id WatchID)` method][1]:

```diff
	ws.mu.Unlock()

	if !ok {
		return ErrWatcherNotExist
	}
+
+	time.Sleep(time.Second * 1)
+
	cancel()

	ws.mu.Lock()
```

Then what is possible to observe is that:

1. We start the `Cancel` method
2. We interleave the `Close` method with our specifically placed `Sleeps`...
3. `Close` will find a synced watcher, delete it, and decrement the counter in [this branch][2]
4. `Cancel` wakes up and resumes, and invokes the `cancelFunc` again
5. Now we [encounter the `wa.compacted` branch and decrement again][3]

It seems as though [the `wa.ch == nil` case][4] is designed to prevent re-processing a cancelled watcher, and both the `if s.synced.delete` and `if s.unsynced.delete` branches are "idempotent", as once the watch is deleted subsequent invocation return false. However, checking `wa.compacted` is _not_ idempotent, and by being placed before the `if wa.ch == nil` case seems like it can be encountered on a subsequent invocation of `cancelWatch`.

Possibly a fix would be to move the "has this already been cancelled" check higher, e.g.:

```diff
                } else if s.synced.delete(wa) {
                        watcherGauge.Dec()
                        break
-               } else if wa.compacted {
-                       watcherGauge.Dec()
-                       break
                } else if wa.ch == nil {
                        // already canceled (e.g., cancel/close race)
                        break
+               } else if wa.compacted {
+                       watcherGauge.Dec()
+                       break
                }
```

this would avoid possibly performing the decrement twice.

[0]: https://github.com/etcd-io/etcd/pull/11882
[1]: https://github.com/etcd-io/etcd/blob/ffb6ff9acf049b6b864955bea46ac6e33c54d9c9/server/storage/mvcc/watcher.go#L156
[2]: https://github.com/etcd-io/etcd/blob/ffb6ff9acf049b6b864955bea46ac6e33c54d9c9/server/storage/mvcc/watchable_store.go#L162-L164
[3]: https://github.com/etcd-io/etcd/blob/ffb6ff9acf049b6b864955bea46ac6e33c54d9c9/server/storage/mvcc/watchable_store.go#L165-L167
[4]: https://github.com/etcd-io/etcd/blob/ffb6ff9acf049b6b864955bea46ac6e33c54d9c9/server/storage/mvcc/watchable_store.go#L168-L170

### Anything else we need to know?

_No response_

### Etcd version (please run commands below)

<details>

```console
/ # /usr/local/bin/etcd --version
etcd Version: 3.5.17
Git SHA: 507c0de87
Go Version: go1.23.4
Go OS/Arch: linux/amd64

/ # /usr/local/bin/etcdctl version
etcdctl version: 3.5.17
API version: 3.5
```

</details>


### Etcd configuration (command line flags or environment variables)

_No response_

### Etcd debug information (please run commands below, feel free to obfuscate the IP address or FQDN in the output)

_No response_

### Relevant log output

```Shell

```

Hints from issue comments:
@ivanvc: Discussed during our bi-weekly triage meeting. Thanks, @kjgorman for raising this issue. This looks like a valid issue. Given that you have invested in tracing where the possible culprit is, and wrote a test case, would you be interested in opening a pull request addressing it? Thanks.@kjgorman: Thanks for the triage @ivanvc — I've raised a PR with the proposed tweak here: https://github.com/etcd-io/etcd/pull/19600@ivanvc: /assign @kjgorman @github-actions: This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 21 days if no further activity occurs. Thank you for your contributions.@kjgorman: I didn't add the "fixes"/"closes" message on the PR I sent, but I believe this issued should be resolved by #19600 so I will close this issue now.@serathius: I think we would want to backport the fix to v3.5 and v3.4@ivanvc: > I think we would want to backport the fix to v3.5 and v3.4

And v3.6, too. Right?@serathius: Yes

Solution patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 75a99abd7..0e1a770af 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -3,7 +3,7 @@
 {
 	"name": "Go",
 	// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
-	"image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm",
+	"image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm",
 	// Features to add to the dev container. More info: https://containers.dev/features.
 	"features": {
 		"ghcr.io/devcontainers/features/docker-in-docker:2": {},
diff --git a/.github/stale.yml b/.github/stale.yml
deleted file mode 100644
index 1c04a756b..000000000
--- a/.github/stale.yml
+++ /dev/null
@@ -1,56 +0,0 @@
----
-# Configuration for probot-stale - https://github.com/probot/stale
-
-# Number of days of inactivity before an Issue or Pull Request becomes stale
-daysUntilStale: 90
-
-# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
-# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
-daysUntilClose: 21
-
-# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
-onlyLabels: []
-
-# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
-exemptLabels:
-  - "stage/tracked"
-
-# Set to true to ignore issues in a project (defaults to false)
-exemptProjects: false
-
-# Set to true to ignore issues in a milestone (defaults to false)
-exemptMilestones: false
-
-# Set to true to ignore issues with an assignee (defaults to false)
-exemptAssignees: false
-
-# Label to use when marking as stale
-staleLabel: stale
-
-# Comment to post when marking as stale. Set to `false` to disable
-markComment: This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 21 days if no further activity occurs. Thank you for your contributions.
-# Comment to post when removing the stale label.
-# unmarkComment: >
-#   Your comment here.
-
-# Comment to post when closing a stale Issue or Pull Request.
-# closeComment: >
-#   Your comment here.
-
-# Limit the number of actions per hour, from 1-30. Default is 30
-limitPerRun: 30
-
-# Limit to only `issues` or `pulls`
-# only: issues
-
-# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
-# pulls:
-#   daysUntilStale: 30
-#   markComment: >
-#     This pull request has been automatically marked as stale because it has not had
-#     recent activity. It will be closed if no further activity occurs. Thank you
-#     for your contributions.
-
-# issues:
-#   exemptLabels:
-#     - confirmed
diff --git a/.github/workflows/antithesis-test.yml b/.github/workflows/antithesis-test.yml
new file mode 100644
index 000000000..3e1b74192
--- /dev/null
+++ b/.github/workflows/antithesis-test.yml
@@ -0,0 +1,99 @@
+---
+name: Build and trigger Antithesis exploration
+
+on:
+  # Disabled as discussed in https://github.com/etcd-io/etcd/pull/19750#issuecomment-2809840402
+  # pull_request:
+  #   branches: [main]
+  # schedule:
+  #   - cron: "0 0 * * *" # run every day at midnight
+  workflow_dispatch:
+    inputs:
+      test:
+        description: 'Test name'
+        required: false
+        default: 'etcd nightly antithesis run'
+        type: string
+      config_image:
+        description: 'Config image'
+        required: true
+        default: us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-config:latest
+        type: string
+      images:
+        description: 'System images (separate with ;)'
+        required: true
+        default: us-central1-docker.pkg.dev/molten-verve-216720/linuxfoundation-repository/etcd-client:latest;gcr.io/etcd-development/etcd:v3.5.21;docker.io/library/ubuntu:latest
+        type: string
+      duration:
+        description: 'Duration (exploration hours)'
+        required: true
+        type: int
+        default: 12
+      description:
+        description: 'Description (avoid quotes, please!)'
+        required: true
+        type: string
+        default: "etcd nightly antithesis run"
+      email:
+        description: 'Additional email notification recipient (separate with ;)'
+        required: true
+        type: string
+        default: ""
+
+# Declare default permissions as read only.
+permissions: read-all
+
+env:
+  REGISTRY: us-central1-docker.pkg.dev
+  REPOSITORY: molten-verve-216720/linuxfoundation-repository
+
+jobs:
+  build-and-push-and-test:
+    runs-on: ubuntu-latest
+    environment: Antithesis
+    steps:
+      - name: Checkout the code
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+      - name: Login to Antithesis Docker Registry
+        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: _json_key
+          password: ${{ secrets.ANTITHESIS_CONTAINER_REGISTRY_TOKEN }}
+
+      - name: Build and push config image
+        uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
+        with:
+          context: ./tests/antithesis
+          file: ./tests/antithesis/Dockerfile.config
+          push: true
+          tags: |
+            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-config:latest,
+            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-config:${{ github.sha }}
+
+      - name: Build and push client image
+        uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
+        with:
+          context: .
+          file: ./tests/antithesis/test-template/Dockerfile
+          push: true
+          tags: |
+            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-client:latest,
+            ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/etcd-client:${{ github.sha }}
+
+      - name: Run Antithesis Tests
+        uses: antithesishq/antithesis-trigger-action@b7d0c9d1d9316bd4de73a44144c56636ea3a64ba # main commit on Mar 13, 2025
+        with:
+          notebook_name: etcd
+          tenant: linuxfoundation
+          username: ${{ secrets.ANTITHESIS_WEBHOOK_USERNAME }}
+          password: ${{ secrets.ANTITHESIS_WEBHOOK_PASSWORD }}
+          github_token: ${{ secrets.GH_PAT }}
+          config_image: ${{ inputs.config_image }}
+          images: ${{ inputs.images }}
+          description: ${{ inputs.description }}
+          email_recipients: ${{ inputs.email }}
+          test_name: ${{ inputs.test }}
+          additional_parameters: |-
+            custom.duration = ${{ inputs.duration }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index d01f0c1a9..fbe9c42a4 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -40,7 +40,7 @@ jobs:
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
+        uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
         with:
           # If you wish to specify custom queries, you can do so here or in a config file.
           # By default, queries listed here will override any specified in a config file.
@@ -50,6 +50,6 @@ jobs:
       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
       # If this step fails, then you should remove it and run the build manually (see below)
       - name: Autobuild
-        uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
+        uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
+        uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
deleted file mode 100644
index 0659c6e2b..000000000
--- a/.github/workflows/coverage.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
----
-name: Coverage
-on: [push, pull_request]
-permissions: read-all
-jobs:
-  coverage:
-    # this is to prevent the job to run at forked projects
-    if: github.repository == 'etcd-io/etcd'
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        target:
-          - linux-amd64-coverage
-    steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
-      - id: goversion
-        run: echo "goversion=$(cat .go-version)" >> "$GITHUB_OUTPUT"
-      - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
-        with:
-          go-version: ${{ steps.goversion.outputs.goversion }}
-      - env:
-          TARGET: ${{ matrix.target }}
-        run: |
-          mkdir "${TARGET}"
-          case "${TARGET}" in
-            linux-amd64-coverage)
-              GOARCH=amd64 make upload-coverage-report
-              ;;
-            *)
-              echo "Failed to find target"
-              exit 1
-              ;;
-          esac
diff --git a/.github/workflows/measure-testgrid-flakiness.yaml b/.github/workflows/measure-testgrid-flakiness.yaml
index 67bdd68d3..8c5a8fd73 100644
--- a/.github/workflows/measure-testgrid-flakiness.yaml
+++ b/.github/workflows/measure-testgrid-flakiness.yaml
@@ -15,7 +15,7 @@ jobs:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - id: goversion
         run: echo "goversion=$(cat .go-version)" >> "$GITHUB_OUTPUT"
-      - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
+      - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
         with:
           go-version: ${{ steps.goversion.outputs.goversion }}
       - env:
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 557a85229..a14cd3962 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -42,7 +42,7 @@ jobs:
       # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
       # format to the repository Actions tab.
       - name: "Upload artifact"
-        uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
           name: SARIF file
           path: results.sarif
@@ -50,6 +50,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
+        uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
         with:
           sarif_file: results.sarif
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
new file mode 100644
index 000000000..240f9f362
--- /dev/null
+++ b/.github/workflows/stale.yaml
@@ -0,0 +1,30 @@
+---
+name: Mark and close stale issues and PRs
+
+on:
+  schedule:
+    - cron: '0 0 * * *'
+
+permissions:
+  issues: write
+  pull-requests: write
+
+jobs:
+  stale:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 #v9.1.0
+        with:
+          days-until-stale: 90
+          days-until-close: 21
+          stale-issue-label: 'stale'
+          stale-pr-label: 'stale'
+          exempt-labels: 'stage/tracked'
+          exempt-projects: false
+          exempt-milestones: false
+          exempt-assignees: false
+          stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed after 21 days if no further activity occurs. Thank you for your contributions.'
+          stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed after 21 days if no further activity occurs. Thank you for your contributions.'
+          close-issue-message: ''
+          close-pr-message: ''
+          operations-per-run: 30
diff --git a/.go-version b/.go-version
index f9e8384bb..ae96cc731 100644
--- a/.go-version
+++ b/.go-version
@@ -1 +1 @@
-1.24.1
+1.24.3
diff --git a/CHANGELOG/CHANGELOG-2.3.md b/CHANGELOG/CHANGELOG-2.3.md
index 0b54062b1..3d809290d 100644
--- a/CHANGELOG/CHANGELOG-2.3.md
+++ b/CHANGELOG/CHANGELOG-2.3.md
@@ -1,6 +1,6 @@
 
 
-<hr>
+---
 
 
 ## [v2.3.8](https://github.com/etcd-io/etcd/releases/tag/v2.3.8) (2017-02-17)
@@ -12,5 +12,5 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v2.3.7...v2.3.8).
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
diff --git a/CHANGELOG/CHANGELOG-3.0.md b/CHANGELOG/CHANGELOG-3.0.md
index bc11c80a5..19a032fc8 100644
--- a/CHANGELOG/CHANGELOG-3.0.md
+++ b/CHANGELOG/CHANGELOG-3.0.md
@@ -1,6 +1,6 @@
 
 
-<hr>
+---
 
 
 ## [v3.0.16](https://github.com/etcd-io/etcd/releases/tag/v3.0.16) (2016-11-13)
@@ -14,7 +14,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.15...v3.0.16) an
 - Compile with [*Go 1.6.4*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.15](https://github.com/etcd-io/etcd/releases/tag/v3.0.15) (2016-11-11)
@@ -32,7 +32,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.14...v3.0.15) an
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.14](https://github.com/etcd-io/etcd/releases/tag/v3.0.14) (2016-11-04)
@@ -50,7 +50,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.13...v3.0.14) an
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.13](https://github.com/etcd-io/etcd/releases/tag/v3.0.13) (2016-10-24)
@@ -64,7 +64,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.12...v3.0.13) an
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.12](https://github.com/etcd-io/etcd/releases/tag/v3.0.12) (2016-10-07)
@@ -78,7 +78,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.11...v3.0.12) an
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.11](https://github.com/etcd-io/etcd/releases/tag/v3.0.11) (2016-10-07)
@@ -98,7 +98,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.10...v3.0.11) an
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.10](https://github.com/etcd-io/etcd/releases/tag/v3.0.10) (2016-09-23)
@@ -112,7 +112,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.9...v3.0.10) and
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.9](https://github.com/etcd-io/etcd/releases/tag/v3.0.9) (2016-09-15)
@@ -130,7 +130,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.8...v3.0.9) and
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.8](https://github.com/etcd-io/etcd/releases/tag/v3.0.8) (2016-09-09)
@@ -148,7 +148,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.7...v3.0.8) and
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.7](https://github.com/etcd-io/etcd/releases/tag/v3.0.7) (2016-08-31)
@@ -166,7 +166,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.6...v3.0.7) and
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.6](https://github.com/etcd-io/etcd/releases/tag/v3.0.6) (2016-08-19)
@@ -180,7 +180,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.5...v3.0.6) and
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.5](https://github.com/etcd-io/etcd/releases/tag/v3.0.5) (2016-08-19)
@@ -198,7 +198,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.4...v3.0.5) and
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.4](https://github.com/etcd-io/etcd/releases/tag/v3.0.4) (2016-07-27)
@@ -221,7 +221,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.3...v3.0.4) and
 - Compile with [*Go 1.6.3*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.3](https://github.com/etcd-io/etcd/releases/tag/v3.0.3) (2016-07-15)
@@ -241,7 +241,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.2...v3.0.3) and
 - Compile with [*Go 1.6.2*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.2](https://github.com/etcd-io/etcd/releases/tag/v3.0.2) (2016-07-08)
@@ -259,7 +259,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.1...v3.0.2) and
 - Compile with [*Go 1.6.2*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.1](https://github.com/etcd-io/etcd/releases/tag/v3.0.1) (2016-07-01)
@@ -273,7 +273,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.0.0...v3.0.1) and
 - Compile with [*Go 1.6.2*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
 
 ## [v3.0.0](https://github.com/etcd-io/etcd/releases/tag/v3.0.0) (2016-06-30)
@@ -287,5 +287,5 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v2.3.0...v3.0.0) and
 - Compile with [*Go 1.6.2*](https://golang.org/doc/devel/release.html#go1.6).
 
 
-<hr>
+---
 
diff --git a/CHANGELOG/CHANGELOG-3.1.md b/CHANGELOG/CHANGELOG-3.1.md
index 0c97517a7..e7d9a1460 100644
--- a/CHANGELOG/CHANGELOG-3.1.md
+++ b/CHANGELOG/CHANGELOG-3.1.md
@@ -2,7 +2,7 @@
 
 Previous change logs can be found at [CHANGELOG-3.0](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.0.md).
 
-<hr>
+---
 
 ## [v3.1.21](https://github.com/etcd-io/etcd/releases/tag/v3.1.21) (2019-TBD)
 
@@ -23,7 +23,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 
 - Fix bug where [db_compaction_total_duration_milliseconds metric incorrectly measured duration as 0](https://github.com/etcd-io/etcd/pull/10646).
 
-<hr>
+---
 
 ## [v3.1.20](https://github.com/etcd-io/etcd/releases/tag/v3.1.20) (2018-10-10)
 
@@ -69,7 +69,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.19](https://github.com/etcd-io/etcd/releases/tag/v3.1.19) (2018-07-24)
@@ -115,7 +115,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.18](https://github.com/etcd-io/etcd/releases/tag/v3.1.18) (2018-06-15)
@@ -138,7 +138,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.17](https://github.com/etcd-io/etcd/releases/tag/v3.1.17) (2018-06-06)
@@ -159,7 +159,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.16...v3.1.17) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.16](https://github.com/etcd-io/etcd/releases/tag/v3.1.16) (2018-05-31)
@@ -179,7 +179,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.15...v3.1.16) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.15](https://github.com/etcd-io/etcd/releases/tag/v3.1.15) (2018-05-09)
@@ -199,7 +199,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.14...v3.1.15) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.14](https://github.com/etcd-io/etcd/releases/tag/v3.1.14) (2018-04-24)
@@ -233,7 +233,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.13](https://github.com/etcd-io/etcd/releases/tag/v3.1.13) (2018-03-29)
@@ -261,7 +261,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.12](https://github.com/etcd-io/etcd/releases/tag/v3.1.12) (2018-03-08)
@@ -284,7 +284,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.11...v3.1.12) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.11](https://github.com/etcd-io/etcd/releases/tag/v3.1.11) (2017-11-28)
@@ -303,7 +303,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.10...v3.1.11) an
 - Compile with [*Go 1.8.5*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.1.10](https://github.com/etcd-io/etcd/releases/tag/v3.1.10) (2017-07-14)
@@ -323,7 +323,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.9...v3.1.10) and
   - Fix panic on `net/http.CloseNotify`
 
 
-<hr>
+---
 
 
 ## [v3.1.9](https://github.com/etcd-io/etcd/releases/tag/v3.1.9) (2017-06-09)
@@ -341,7 +341,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.8...v3.1.9) and
 - Compile with [*Go 1.7.6*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.8](https://github.com/etcd-io/etcd/releases/tag/v3.1.8) (2017-05-19)
@@ -355,7 +355,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.7...v3.1.8) and
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.7](https://github.com/etcd-io/etcd/releases/tag/v3.1.7) (2017-04-28)
@@ -369,7 +369,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.6...v3.1.7) and
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.6](https://github.com/etcd-io/etcd/releases/tag/v3.1.6) (2017-04-19)
@@ -388,7 +388,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.5...v3.1.6) and
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.5](https://github.com/etcd-io/etcd/releases/tag/v3.1.5) (2017-03-27)
@@ -411,7 +411,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.4...v3.1.5) and
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.4](https://github.com/etcd-io/etcd/releases/tag/v3.1.4) (2017-03-22)
@@ -425,7 +425,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.3...v3.1.4) and
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.3](https://github.com/etcd-io/etcd/releases/tag/v3.1.3) (2017-03-10)
@@ -452,7 +452,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.2...v3.1.3) and
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.2](https://github.com/etcd-io/etcd/releases/tag/v3.1.2) (2017-02-24)
@@ -474,7 +474,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.1...v3.1.2) and
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.1](https://github.com/etcd-io/etcd/releases/tag/v3.1.1) (2017-02-17)
@@ -488,7 +488,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.1.0...v3.1.1) and
 - Compile with [*Go 1.7.5*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
 
 ## [v3.1.0](https://github.com/etcd-io/etcd/releases/tag/v3.1.0) (2017-01-20)
@@ -570,5 +570,5 @@ See [security doc](https://etcd.io/docs/latest/op-guide/security/) for more deta
 - Compile with [*Go 1.7.4*](https://golang.org/doc/devel/release.html#go1.7).
 
 
-<hr>
+---
 
diff --git a/CHANGELOG/CHANGELOG-3.2.md b/CHANGELOG/CHANGELOG-3.2.md
index 095ff6e9f..42bdc53a0 100644
--- a/CHANGELOG/CHANGELOG-3.2.md
+++ b/CHANGELOG/CHANGELOG-3.2.md
@@ -5,7 +5,7 @@ Previous change logs can be found at [CHANGELOG-3.1](https://github.com/etcd-io/
 
 ## v3.2.33 (TBD)
 
-<hr>
+---
 
 ## [v3.2.32](https://github.com/etcd-io/etcd/releases/tag/v3.2.32) (2021-03-28)
 See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.31...v3.2.32) and [v3.2 upgrade guide](https://etcd.io/docs/latest/upgrades/upgrade_3_2/) for any breaking changes.
@@ -23,7 +23,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.31...v3.2.32) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.2.31](https://github.com/etcd-io/etcd/releases/tag/v3.2.31) (2020-08-18)
@@ -49,7 +49,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.30...v3.2.31) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.2.30](https://github.com/etcd-io/etcd/releases/tag/v3.2.30) (2020-04-01)
@@ -69,7 +69,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.29...v3.2.30) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.2.29](https://github.com/etcd-io/etcd/releases/tag/v3.2.29) (2020-03-18)
@@ -97,7 +97,7 @@ See [List of metrics](https://github.com/etcd-io/etcd/tree/main/Documentation/me
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.28](https://github.com/etcd-io/etcd/releases/tag/v3.2.28) (2019-11-10)
@@ -125,7 +125,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.27](https://github.com/etcd-io/etcd/releases/tag/v3.2.27) (2019-09-17)
@@ -161,7 +161,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.26](https://github.com/etcd-io/etcd/releases/tag/v3.2.26) (2019-01-11)
@@ -183,7 +183,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.25...v3.2.26) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.25](https://github.com/etcd-io/etcd/releases/tag/v3.2.25) (2018-10-10)
@@ -230,7 +230,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.24](https://github.com/etcd-io/etcd/releases/tag/v3.2.24) (2018-07-24)
@@ -286,7 +286,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.23](https://github.com/etcd-io/etcd/releases/tag/v3.2.23) (2018-06-15)
@@ -317,7 +317,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.22](https://github.com/etcd-io/etcd/releases/tag/v3.2.22) (2018-06-06)
@@ -339,7 +339,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.21...v3.2.22) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.21](https://github.com/etcd-io/etcd/releases/tag/v3.2.21) (2018-05-31)
@@ -360,7 +360,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.20...v3.2.21) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.20](https://github.com/etcd-io/etcd/releases/tag/v3.2.20) (2018-05-09)
@@ -380,7 +380,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.19...v3.2.20) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.19](https://github.com/etcd-io/etcd/releases/tag/v3.2.19) (2018-04-24)
@@ -423,7 +423,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.18](https://github.com/etcd-io/etcd/releases/tag/v3.2.18) (2018-03-29)
@@ -451,7 +451,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.17](https://github.com/etcd-io/etcd/releases/tag/v3.2.17) (2018-03-08)
@@ -481,7 +481,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.16...v3.2.17) an
 - Compile with [*Go 1.8.7*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.16](https://github.com/etcd-io/etcd/releases/tag/v3.2.16) (2018-02-12)
@@ -504,7 +504,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.15...v3.2.16) an
 - Compile with [*Go 1.8.5*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.15](https://github.com/etcd-io/etcd/releases/tag/v3.2.15) (2018-01-22)
@@ -523,7 +523,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.14...v3.2.15) an
 - Compile with [*Go 1.8.5*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.14](https://github.com/etcd-io/etcd/releases/tag/v3.2.14) (2018-01-11)
@@ -545,7 +545,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.13...v3.2.14) an
 - Compile with [*Go 1.8.5*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.13](https://github.com/etcd-io/etcd/releases/tag/v3.2.13) (2018-01-02)
@@ -564,7 +564,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.12...v3.2.13) an
 - Compile with [*Go 1.8.5*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.12](https://github.com/etcd-io/etcd/releases/tag/v3.2.12) (2017-12-20)
@@ -596,7 +596,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.11...v3.2.12) an
 - Compile with [*Go 1.8.5*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.11](https://github.com/etcd-io/etcd/releases/tag/v3.2.11) (2017-12-05)
@@ -629,7 +629,7 @@ See [security doc](https://etcd.io/docs/latest/op-guide/security/) for more deta
 - Compile with [*Go 1.8.5*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.10](https://github.com/etcd-io/etcd/releases/tag/v3.2.10) (2017-11-16)
@@ -663,7 +663,7 @@ See [security doc](https://etcd.io/docs/latest/op-guide/security/) for more deta
 - Compile with [*Go 1.8.5*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.9](https://github.com/etcd-io/etcd/releases/tag/v3.2.9) (2017-10-06)
@@ -685,7 +685,7 @@ See [security doc](https://etcd.io/docs/latest/op-guide/security/) for more deta
 - Compile with [*Go 1.8.4*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.8](https://github.com/etcd-io/etcd/releases/tag/v3.2.8) (2017-09-29)
@@ -707,7 +707,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.7...v3.2.8) and
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.7](https://github.com/etcd-io/etcd/releases/tag/v3.2.7) (2017-09-01)
@@ -730,7 +730,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.6...v3.2.7) and
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.6](https://github.com/etcd-io/etcd/releases/tag/v3.2.6) (2017-08-21)
@@ -758,7 +758,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.5](https://github.com/etcd-io/etcd/releases/tag/v3.2.5) (2017-08-04)
@@ -798,7 +798,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.4](https://github.com/etcd-io/etcd/releases/tag/v3.2.4) (2017-07-19)
@@ -820,7 +820,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.3...v3.2.4) and
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.3](https://github.com/etcd-io/etcd/releases/tag/v3.2.3) (2017-07-14)
@@ -843,7 +843,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.2.2...v3.2.3) and
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.2](https://github.com/etcd-io/etcd/releases/tag/v3.2.2) (2017-07-07)
@@ -879,7 +879,7 @@ See [security doc](https://etcd.io/docs/latest/op-guide/security/) for more deta
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.1](https://github.com/etcd-io/etcd/releases/tag/v3.2.1) (2017-06-23)
@@ -909,7 +909,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
 
 ## [v3.2.0](https://github.com/etcd-io/etcd/releases/tag/v3.2.0) (2017-06-09)
@@ -1017,5 +1017,5 @@ See [security doc](https://etcd.io/docs/latest/op-guide/security/) for more deta
 - Compile with [*Go 1.8.3*](https://golang.org/doc/devel/release.html#go1.8).
 
 
-<hr>
+---
 
diff --git a/CHANGELOG/CHANGELOG-3.3.md b/CHANGELOG/CHANGELOG-3.3.md
index 8addba112..825883ab4 100644
--- a/CHANGELOG/CHANGELOG-3.3.md
+++ b/CHANGELOG/CHANGELOG-3.3.md
@@ -2,7 +2,7 @@
 
 Previous change logs can be found at [CHANGELOG-3.2](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.2.md).
 
-<hr>
+---
 
 ## v3.3.27 (2021-10-15)
 
@@ -16,7 +16,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.26...v3.3.27) an
   - [CVE-2019-9893](https://nvd.nist.gov/vuln/detail/CVE-2019-9893): incorrect syscall argument generation in libseccomp
   - [CVE-2021-36159](https://nvd.nist.gov/vuln/detail/CVE-2021-36159): libfetch in apk-tools mishandles numeric strings in FTP and HTTP protocols to allow out of bound reads.
 
-<hr>
+---
 
 ## v3.3.26 (2021-10-03)
 
@@ -34,7 +34,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.25...v3.3.26) an
 
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
-<hr>
+---
 
 ## v3.3.25 (2020-08-24)
 
@@ -68,7 +68,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.23...v3.3.24) an
 
 
 
-<hr>
+---
 
 
 
@@ -96,7 +96,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.22...v3.3.23) an
 
 
 
-<hr>
+---
 
 
 ## [v3.3.22](https://github.com/etcd-io/etcd/releases/tag/v3.3.22) (2020-05-20)
@@ -113,7 +113,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.21...v3.3.22) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.3.21](https://github.com/etcd-io/etcd/releases/tag/v3.3.21) (2020-05-18)
@@ -150,7 +150,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.20...v3.3.21) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.3.20](https://github.com/etcd-io/etcd/releases/tag/v3.3.20) (2020-04-01)
@@ -170,7 +170,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.19...v3.3.20) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.3.19](https://github.com/etcd-io/etcd/releases/tag/v3.3.19) (2020-03-18)
@@ -206,7 +206,7 @@ See [List of metrics](https://github.com/etcd-io/etcd/tree/main/Documentation/me
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.3.18](https://github.com/etcd-io/etcd/releases/tag/v3.3.18) (2019-11-26)
@@ -227,7 +227,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
   - Now, etcd makes sure the purge file loop exits before server signals stop of the raft node.
 
 
-<hr>
+---
 
 
 ## [v3.3.17](https://github.com/etcd-io/etcd/releases/tag/v3.3.17) (2019-10-11)
@@ -241,7 +241,7 @@ This release replaces 3.3.16.
 Due to the etcd 3.3.16 release being incorrectly released (see details below), please use this release instead.
 
 
-<hr>
+---
 
 
 ## [v3.3.16](https://github.com/etcd-io/etcd/releases/tag/v3.3.16) (2019-10-10)
@@ -290,7 +290,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
   - Fix ["1.16: etcd client does not parse IPv6 addresses correctly when members are joining" (kubernetes#83550)](https://github.com/kubernetes/kubernetes/issues/83550).
 
 
-<hr>
+---
 
 
 ## [v3.3.15](https://github.com/etcd-io/etcd/releases/tag/v3.3.15) (2019-08-19)
@@ -313,7 +313,7 @@ NOTE: This patch release had to include some new features from 3.4, while trying
 - Compile with [*Go 1.12.9*](https://golang.org/doc/devel/release.html#go1.12) including [*Go 1.12.8*](https://groups.google.com/d/msg/golang-announce/65QixT3tcmg/DrFiG6vvCwAJ) security fixes.
 
 
-<hr>
+---
 
 
 ## [v3.3.14](https://github.com/etcd-io/etcd/releases/tag/v3.3.14) (2019-08-16)
@@ -416,7 +416,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.12.9*](https://golang.org/doc/devel/release.html#go1.12) including [*Go 1.12.8*](https://groups.google.com/d/msg/golang-announce/65QixT3tcmg/DrFiG6vvCwAJ) security fixes.
 
 
-<hr>
+---
 
 
 ## [v3.3.13](https://github.com/etcd-io/etcd/releases/tag/v3.3.13) (2019-05-02)
@@ -456,7 +456,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.10.8*](https://golang.org/doc/devel/release.html#go1.10).
 
 
-<hr>
+---
 
 
 ## [v3.3.12](https://github.com/etcd-io/etcd/releases/tag/v3.3.12) (2019-02-07)
@@ -474,7 +474,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.11...v3.3.12) an
 - Compile with [*Go 1.10.8*](https://golang.org/doc/devel/release.html#go1.10).
 
 
-<hr>
+---
 
 
 ## [v3.3.11](https://github.com/etcd-io/etcd/releases/tag/v3.3.11) (2019-01-11)
@@ -496,7 +496,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.10...v3.3.11) an
 - Compile with [*Go 1.10.7*](https://golang.org/doc/devel/release.html#go1.10).
 
 
-<hr>
+---
 
 
 ## [v3.3.10](https://github.com/etcd-io/etcd/releases/tag/v3.3.10) (2018-10-10)
@@ -542,7 +542,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.10.4*](https://golang.org/doc/devel/release.html#go1.10).
 
 
-<hr>
+---
 
 
 ## [v3.3.9](https://github.com/etcd-io/etcd/releases/tag/v3.3.9) (2018-07-24)
@@ -597,7 +597,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.10.3*](https://golang.org/doc/devel/release.html#go1.10).
 
 
-<hr>
+---
 
 
 ## [v3.3.8](https://github.com/etcd-io/etcd/releases/tag/v3.3.8) (2018-06-15)
@@ -619,7 +619,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.7...v3.3.8) and
 - Compile with [*Go 1.9.7*](https://golang.org/doc/devel/release.html#go1.9).
 
 
-<hr>
+---
 
 
 ## [v3.3.7](https://github.com/etcd-io/etcd/releases/tag/v3.3.7) (2018-06-06)
@@ -645,7 +645,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.6...v3.3.7) and
 - Compile with [*Go 1.9.6*](https://golang.org/doc/devel/release.html#go1.9).
 
 
-<hr>
+---
 
 
 ## [v3.3.6](https://github.com/etcd-io/etcd/releases/tag/v3.3.6) (2018-05-31)
@@ -668,7 +668,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.5...v3.3.6) and
 - Compile with [*Go 1.9.6*](https://golang.org/doc/devel/release.html#go1.9).
 
 
-<hr>
+---
 
 
 ## [v3.3.5](https://github.com/etcd-io/etcd/releases/tag/v3.3.5) (2018-05-09)
@@ -687,7 +687,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.4...v3.3.5) and
 - Compile with [*Go 1.9.6*](https://golang.org/doc/devel/release.html#go1.9).
 
 
-<hr>
+---
 
 
 ## [v3.3.4](https://github.com/etcd-io/etcd/releases/tag/v3.3.4) (2018-04-24)
@@ -735,7 +735,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.9.5*](https://golang.org/doc/devel/release.html#go1.9).
 
 
-<hr>
+---
 
 
 ## [v3.3.3](https://github.com/etcd-io/etcd/releases/tag/v3.3.3) (2018-03-29)
@@ -772,7 +772,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.9.5*](https://golang.org/doc/devel/release.html#go1.9).
 
 
-<hr>
+---
 
 
 ## [v3.3.2](https://github.com/etcd-io/etcd/releases/tag/v3.3.2) (2018-03-08)
@@ -805,7 +805,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.1...v3.3.2) and
 - Compile with [*Go 1.9.4*](https://golang.org/doc/devel/release.html#go1.9).
 
 
-<hr>
+---
 
 
 ## [v3.3.1](https://github.com/etcd-io/etcd/releases/tag/v3.3.1) (2018-02-12)
@@ -833,7 +833,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.3.0...v3.3.1) and
 - Compile with [*Go 1.9.4*](https://golang.org/doc/devel/release.html#go1.9).
 
 
-<hr>
+---
 
 
 ## [v3.3.0](https://github.com/etcd-io/etcd/releases/tag/v3.3.0) (2018-02-01)
@@ -1117,5 +1117,5 @@ See [security doc](https://etcd.io/docs/latest/op-guide/security/) for more deta
 - Deprecate [`golang.org/x/net/context`](https://github.com/etcd-io/etcd/pull/8511).
 
 
-<hr>
+---
 
diff --git a/CHANGELOG/CHANGELOG-3.4.md b/CHANGELOG/CHANGELOG-3.4.md
index c5c52c8f6..aab809de8 100644
--- a/CHANGELOG/CHANGELOG-3.4.md
+++ b/CHANGELOG/CHANGELOG-3.4.md
@@ -2,15 +2,23 @@
 
 Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.3.md).
 
-<hr>
+---
 
-## v3.4.37 (TBC)
+## v3.4.38 (TBC)
+
+### Dependencies
+
+- Compile binaries using [go 1.23.9](https://github.com/etcd-io/etcd/pull/19872).
+
+---
+
+## v3.4.37 (2025-04-15)
 
 ### Dependencies
 - Bump [golang.org/x/net to v0.36.0 to address CVE-2025-22870](https://github.com/etcd-io/etcd/pull/19529).
-- Compile binaries using [go 1.23.7](https://github.com/etcd-io/etcd/pull/19533)
+- Compile binaries using [go 1.23.8](https://github.com/etcd-io/etcd/pull/19726)
 
-<hr>
+---
 
 ## v3.4.36 (2025-02-25)
 
@@ -26,7 +34,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 - Bump golang.org/x/crypto to v0.35.0 to address [CVE-2024-45337](https://github.com/etcd-io/etcd/pull/19197) and [CVE-2025-22869](https://github.com/etcd-io/etcd/pull/19477).
 - Bump golang.org/x/net to v0.34.0 to address [CVE-2024-45338](https://github.com/etcd-io/etcd/pull/19197).
 
-<hr>
+---
 
 ## v3.4.35 (2024-11-12)
 
@@ -38,7 +46,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Dependencies
 - Compile binaries using [go 1.22.9](https://github.com/etcd-io/etcd/pull/18850).
 
-<hr>
+---
 
 ## v3.4.34 (2024-09-11)
 
@@ -53,7 +61,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 - Compile binaries using [go 1.22.7](https://github.com/etcd-io/etcd/pull/18549).
 - Upgrade [bbolt to 1.3.11](https://github.com/etcd-io/etcd/pull/18488).
 
-<hr>
+---
 
 ## v3.4.33 (2024-06-13)
 
@@ -64,7 +72,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 - Compile binaries using go [1.21.11](https://github.com/etcd-io/etcd/pull/18130).
 - Upgrade [bbolt to 1.3.10](https://github.com/etcd-io/etcd/pull/17945).
 
-<hr>
+---
 
 ## v3.4.32 (2024-04-25)
 
@@ -83,7 +91,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Dependencies
 - Compile binaries using [go 1.21.9](https://github.com/etcd-io/etcd/pull/17709).
 
-<hr>
+---
 
 ## v3.4.31 (2024-03-21)
 
@@ -106,7 +114,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Others
 - [Make CGO_ENABLED configurable](https://github.com/etcd-io/etcd/pull/17422).
 
-<hr>
+---
 
 ## v3.4.30 (2024-01-31)
 
@@ -117,7 +125,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 - Compile binaries using go [1.20.13](https://github.com/etcd-io/etcd/pull/17276).
 - Upgrade [golang.org/x/crypto to v0.17+ to address CVE-2023-48795](https://github.com/etcd-io/etcd/pull/17347).
 
-<hr>
+---
 
 ## v3.4.29 (2024-01-09)
 
@@ -131,7 +139,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Dependencies
 - Compile binaries using go [1.20.12](https://github.com/etcd-io/etcd/pull/17076).
 
-<hr>
+---
 
 ## v3.4.28 (2023-11-23)
 
@@ -153,7 +161,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 - Upgrade [bbolt to 1.3.8](https://github.com/etcd-io/etcd/pull/16834).
 - Upgrade gRPC to 1.58.3 in https://github.com/etcd-io/etcd/pull/16997 and https://github.com/etcd-io/etcd/pull/16999. Note that gRPC server will reject requests with connection header (refer to https://github.com/grpc/grpc-go/pull/4803).
 
-<hr>
+---
 
 ## v3.4.27 (2023-07-11)
 
@@ -168,7 +176,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Dependencies
 - Compile binaries using [go 1.19.10](https://github.com/etcd-io/etcd/pull/16038).
 
-<hr>
+---
 
 ## v3.4.26 (2023-05-12)
 
@@ -179,7 +187,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Dependencies
 - Compile binaries using [go 1.19.9](https://github.com/etcd-io/etcd/pull/15823)
 
-<hr>
+---
 
 ## v3.4.25 (2023-04-14)
 
@@ -206,7 +214,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Docker image
 - Fix [etcd docker images all tagged with amd64 architecture](https://github.com/etcd-io/etcd/pull/15681)
 
-<hr>
+---
 
 ## v3.4.24 (2023-02-16)
 
@@ -228,7 +236,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Docker image
 - Updated [base image from base-debian11 to static-debian11 and removed dependency on busybox](https://github.com/etcd-io/etcd/pull/15038).
 
-<hr>
+---
 
 ## v3.4.23 (2022-12-21)
 
@@ -247,7 +255,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### Docker image
 - Use [distroless base image](https://github.com/etcd-io/etcd/pull/15017) to address critical Vulnerabilities.
 
-<hr>
+---
 
 ## v3.4.22 (2022-11-02)
 
@@ -266,7 +274,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 ### etcd grpc-proxy
 - Add [`etcd grpc-proxy start --listen-cipher-suites`](https://github.com/etcd-io/etcd/pull/14601) flag to support adding configurable cipher list.
 
-<hr>
+---
 
 ## v3.4.21 (2022-09-15)
 
@@ -279,7 +287,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 
 - Fix [etcdctl move-leader may fail for multiple endpoints](https://github.com/etcd-io/etcd/pull/14441)
 
-<hr>
+---
 
 ## v3.4.20 (2022-08-06)
 
@@ -299,7 +307,7 @@ Previous change logs can be found at [CHANGELOG-3.3](https://github.com/etcd-io/
 - Fix [Grant lease with negative ID can possibly cause db out of sync](https://github.com/etcd-io/etcd/pull/14239)
 - Fix [Allow non mutating requests pass through quotaKVServer when NOSPACE](https://github.com/etcd-io/etcd/pull/14254)
 
-<hr>
+---
 
 ## v3.4.19 (2022-07-12)
 
@@ -327,7 +335,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.18...v3.4.19) an
 - Compile with [Go 1.16+](https://go.dev/doc/devel/release#go1.16).
 - etcd uses [go modules](https://github.com/etcd-io/etcd/pull/14136) (instead of vendor dir) to track dependencies.
 
-<hr>
+---
 
 ## v3.4.18 (2021-10-15)
 
@@ -347,7 +355,7 @@ See [List of metrics](https://etcd.io/docs/latest/metrics/) for all metrics per
   - [CVE-2019-9893](https://nvd.nist.gov/vuln/detail/CVE-2019-9893): incorrect syscall argument generation in libseccomp
   - [CVE-2021-36159](https://nvd.nist.gov/vuln/detail/CVE-2021-36159): libfetch in apk-tools mishandles numeric strings in FTP and HTTP protocols to allow out of bound reads.
 
-<hr>
+---
 
 ## v3.4.17 (2021-10-03)
 
@@ -369,7 +377,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.16...v3.4.17) an
 
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
-<hr>
+---
 
 ## v3.4.16 (2021-05-11)
 
@@ -391,7 +399,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.15...v3.4.16) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.15](https://github.com/etcd-io/etcd/releases/tag/v3.4.15) (2021-02-26)
@@ -416,7 +424,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.14...v3.4.15) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.14](https://github.com/etcd-io/etcd/releases/tag/v3.4.14) (2020-11-25)
@@ -446,7 +454,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.13...v3.4.14) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.13](https://github.com/etcd-io/etcd/releases/tag/v3.4.13) (2020-8-24)
@@ -462,7 +470,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.12...v3.4.13) an
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.12](https://github.com/etcd-io/etcd/releases/tag/v3.4.12) (2020-08-19)
@@ -480,7 +488,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.11...v3.4.12) an
 
 
 
-<hr>
+---
 
 
 
@@ -514,7 +522,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.10...v3.4.11) an
 
 
 
-<hr>
+---
 
 
 
@@ -543,7 +551,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.9...v3.4.10) and
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.9](https://github.com/etcd-io/etcd/releases/tag/v3.4.9) (2020-05-20)
@@ -560,7 +568,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.8...v3.4.9) and
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.8](https://github.com/etcd-io/etcd/releases/tag/v3.4.8) (2020-05-18)
@@ -597,7 +605,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.7...v3.4.8) and
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.7](https://github.com/etcd-io/etcd/releases/tag/v3.4.7) (2020-04-01)
@@ -621,7 +629,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.6...v3.4.7) and
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.6](https://github.com/etcd-io/etcd/releases/tag/v3.4.6) (2020-03-29)
@@ -639,7 +647,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.5...v3.4.6) and
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.5](https://github.com/etcd-io/etcd/releases/tag/v3.4.5) (2020-03-18)
@@ -676,7 +684,7 @@ See [List of metrics](https://etcd.io/docs/latest/metrics/) for all metrics per
 - Compile with [*Go 1.12.17*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.4](https://github.com/etcd-io/etcd/releases/tag/v3.4.4) (2020-02-24)
@@ -709,7 +717,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Fix bug where [some auth related messages are logged at wrong level](https://github.com/etcd-io/etcd/pull/11586)
 
 
-<hr>
+---
 
 
 ## [v3.4.3](https://github.com/etcd-io/etcd/releases/tag/v3.4.3) (2019-10-24)
@@ -731,7 +739,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.12.12*](https://golang.org/doc/devel/release.html#go1.12).
 
 
-<hr>
+---
 
 
 ## [v3.4.2](https://github.com/etcd-io/etcd/releases/tag/v3.4.2) (2019-10-11)
@@ -760,7 +768,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.4.1...v3.4.2) and
   - Fix ["1.16: etcd client does not parse IPv6 addresses correctly when members are joining" (kubernetes#83550)](https://github.com/kubernetes/kubernetes/issues/83550).
 
 
-<hr>
+---
 
 
 ## [v3.4.1](https://github.com/etcd-io/etcd/releases/tag/v3.4.1) (2019-09-17)
@@ -796,7 +804,7 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - Compile with [*Go 1.12.9*](https://golang.org/doc/devel/release.html#go1.12) including [*Go 1.12.8*](https://groups.google.com/d/msg/golang-announce/65QixT3tcmg/DrFiG6vvCwAJ) security fixes.
 
 
-<hr>
+---
 
 
 ## v3.4.0 (2019-08-30)
@@ -1401,5 +1409,5 @@ Note: **v3.5 will deprecate `etcd --log-package-levels` flag for `capnslog`**; `
 
 - [Rebase etcd image from Alpine to Debian](https://github.com/etcd-io/etcd/pull/10805) to improve security and maintenance effort for etcd release.
 
-<hr>
+---
 
diff --git a/CHANGELOG/CHANGELOG-3.5.md b/CHANGELOG/CHANGELOG-3.5.md
index c3cc6b8d6..68d94b64d 100644
--- a/CHANGELOG/CHANGELOG-3.5.md
+++ b/CHANGELOG/CHANGELOG-3.5.md
@@ -1,11 +1,51 @@
 
 
 Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.4.md).
-<hr>
 
-## v3.5.20 (TBC)
+---
 
-<hr>
+## v3.5.22 (TBC)
+
+### etcd server
+
+- Fix [the compaction pause duration metric is not emitted for every compaction batch](https://github.com/etcd-io/etcd/pull/19771)
+
+### Package `clientv3`
+
+- [Replace `resolver.State.Addresses` with `resolver.State.Endpoint.Addresses`](https://github.com/etcd-io/etcd/pull/19783).
+- [Deprecated the Metadata field in the Endpoint struct from the client/v3/naming/endpoints package](https://github.com/etcd-io/etcd/pull/19846).
+
+### Dependencies
+
+- Compile binaries using [go 1.23.9](https://github.com/etcd-io/etcd/pull/19870)
+
+---
+
+## v3.5.21 (2025-03-27)
+
+### Dependencies
+
+- Bump [github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 to address CVE-2025-30204](https://github.com/etcd-io/etcd/pull/19646).
+- Bump [bump golang.org/x/net from v0.36.0 to v0.38.0 to address CVE-2025-22870 and CVE-2025-22872](https://github.com/etcd-io/etcd/pull/19686).
+
+---
+
+## v3.5.20 (2025-03-21)
+
+### etcd server
+
+- Fix [the learner promotion changes not being persisted into v3store (bbolt)](https://github.com/etcd-io/etcd/pull/19563)
+- Update [the RLock in Demoted method for read-only access to expiry](https://github.com/etcd-io/etcd/pull/19445)
+
+### etcdctl
+
+- Fix [command `etcdctl member promote` doesn't support json output](https://github.com/etcd-io/etcd/pull/19602)
+
+### etcd grpc-proxy
+
+- Fix [grpcproxy can get stuck in and endless loop causing high CPU usage](https://github.com/etcd-io/etcd/pull/19562)
+
+---
 
 ## v3.5.19 (2025-03-05)
 
@@ -21,7 +61,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - Bump [golang.org/x/crypto to v0.35.0 to address CVE-2025-22869](https://github.com/etcd-io/etcd/pull/19478).
 - Bump [golang.org/x/net to v0.36.0 to address CVE-2025-22870](https://github.com/etcd-io/etcd/pull/19530).
 
-<hr>
+---
 
 ## v3.5.18 (2025-01-24)
 
@@ -45,7 +85,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - Bump [golang.org/x/crypto to 0.32.0 to address CVE-2024-45337](https://github.com/etcd-io/etcd/pull/19154).
 - Bump [golang.org/x/net to 0.34.0 to address CVE-2024-45338](https://github.com/etcd-io/etcd/pull/19158).
 
-<hr>
+---
 
 ## v3.5.17 (2024-11-12)
 
@@ -58,7 +98,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 ### Dependencies
 - Compile binaries using [go 1.22.9](https://github.com/etcd-io/etcd/pull/18849).
 
-<hr>
+---
 
 ## v3.5.16 (2024-09-10)
 
@@ -71,7 +111,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - Compile binaries using [go 1.22.7](https://github.com/etcd-io/etcd/pull/18550).
 - Upgrade [bbolt to v1.3.11](https://github.com/etcd-io/etcd/pull/18489).
 
-<hr>
+---
 
 ## v3.5.15 (2024-07-19)
 
@@ -110,7 +150,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - Compile binaries using [go 1.21.10](https://github.com/etcd-io/etcd/pull/17980).
 - Upgrade [bbolt to v1.3.10](https://github.com/etcd-io/etcd/pull/17943).
 
-<hr>
+---
 
 ## v3.5.13 (2024-03-29)
 
@@ -139,7 +179,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 ### Others
 - [Make CGO_ENABLED configurable](https://github.com/etcd-io/etcd/pull/17421).
 
-<hr>
+---
 
 ## v3.5.12 (2024-01-31)
 
@@ -166,7 +206,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - Compile binaries using [go 1.20.12](https://github.com/etcd-io/etcd/pull/17077)
 - Fix [CVE-2023-47108](https://github.com/advisories/GHSA-8pgv-569h-w5rw) by [bumping go.opentelemetry.io/otel to 1.20.0 and go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc to 0.46.0](https://github.com/etcd-io/etcd/pull/16946).
 
-<hr>
+---
 
 ## v3.5.10 (2023-10-27)
 
@@ -197,7 +237,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - Upgrade gRPC to 1.58.3 in https://github.com/etcd-io/etcd/pull/16625, https://github.com/etcd-io/etcd/pull/16781 and https://github.com/etcd-io/etcd/pull/16790. Note that gRPC server will reject requests with connection header (refer to https://github.com/grpc/grpc-go/pull/4803).
 - Upgrade [bbolt to v1.3.8](https://github.com/etcd-io/etcd/pull/16833)
 
-<hr>
+---
 
 ## v3.5.9 (2023-05-11)
 
@@ -207,7 +247,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 ### Dependencies
 - Compile binaries using [go 1.19.9](https://github.com/etcd-io/etcd/pull/15822).
 
-<hr>
+---
 
 ## v3.5.8 (2023-04-13)
 
@@ -240,7 +280,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - [Remove nsswitch.conf from docker image](https://github.com/etcd-io/etcd/pull/15161)
 - Fix [etcd docker images all tagged with amd64 architecture](https://github.com/etcd-io/etcd/pull/15612)
 
-<hr>
+---
 
 ## v3.5.7 (2023-01-20)
 
@@ -263,7 +303,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - Use [distroless base image](https://github.com/etcd-io/etcd/pull/15016) to address critical Vulnerabilities.
 - Updated [base image from base-debian11 to static-debian11 and removed dependency on busybox](https://github.com/etcd-io/etcd/pull/15037).
 
-<hr>
+---
 
 ## v3.5.6 (2022-11-21)
 
@@ -283,7 +323,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 ### etcd grpc-proxy
 - Add [`etcd grpc-proxy start --listen-cipher-suites`](https://github.com/etcd-io/etcd/pull/14500) flag to support adding configurable cipher list.
 
-<hr>
+---
 
 ## v3.5.5 (2022-09-15)
 
@@ -318,7 +358,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - [Bump golang.org/x/crypto to latest version](https://github.com/etcd-io/etcd/pull/13996) to address [CVE-2022-27191](https://github.com/advisories/GHSA-8c26-wmh5-6g9v).
 - [Bump OpenTelemetry to 1.0.1 and gRPC to 1.41.0](https://github.com/etcd-io/etcd/pull/14312).
 
-<hr>
+---
 
 ## v3.5.4 (2022-04-24)
 
@@ -330,7 +370,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 - [Revert the change of trimming the trailing dot from SRV.Target](https://github.com/etcd-io/etcd/pull/13950) returned by DNS lookup
 
 
-<hr>
+---
 
 ## v3.5.3 (2022-04-13)
 
@@ -351,7 +391,7 @@ Previous change logs can be found at [CHANGELOG-3.4](https://github.com/etcd-io/
 
 - [Always print the raft_term in decimal](https://github.com/etcd-io/etcd/pull/13727) when displaying member list in json.
 
-<hr>
+---
 
 ## [v3.5.2](https://github.com/etcd-io/etcd/releases/tag/v3.5.2) (2022-02-01)
 
@@ -364,7 +404,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.1...v3.5.2) and
 - Fix [assertion failed due to tx closed when recovering v3 backend from a snapshot db](https://github.com/etcd-io/etcd/pull/13501)
 - Fix [segmentation violation(SIGSEGV) error due to premature unlocking of watchableStore](https://github.com/etcd-io/etcd/pull/13541)
 
-<hr>
+---
 
 ## [v3.5.1](https://github.com/etcd-io/etcd/releases/tag/v3.5.1) (2021-10-15)
 
@@ -391,7 +431,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v3.5.1) and
   - [CVE-2019-9893](https://nvd.nist.gov/vuln/detail/CVE-2019-9893): incorrect syscall argument generation in libseccomp
   - [CVE-2021-36159](https://nvd.nist.gov/vuln/detail/CVE-2021-36159): libfetch in apk-tools mishandles numeric strings in FTP and HTTP protocols to allow out of bound reads.
 
-<hr>
+---
 
 ## v3.5.0 (2021-06)
 
@@ -688,5 +728,5 @@ Note that any `etcd_debugging_*` metrics are experimental and subject to change.
 - The etcd team has added, a well defined and openly discussed, project [governance](https://github.com/etcd-io/etcd/pull/11175).
 
 
-<hr>
+---
 
diff --git a/CHANGELOG/CHANGELOG-3.6.md b/CHANGELOG/CHANGELOG-3.6.md
index 73023a17b..c038f0f3e 100644
--- a/CHANGELOG/CHANGELOG-3.6.md
+++ b/CHANGELOG/CHANGELOG-3.6.md
@@ -2,15 +2,59 @@
 
 Previous change logs can be found at [CHANGELOG-3.5](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.5.md).
 
-<hr>
+---
 
 ## v3.6.0 (TBD)
 
-<hr>
+---
 
-## v3.6.0-rc.3 (TBD)
+## v3.6.0-rc.5 (2025-05-08)
 
-<hr>
+### etcd server
+
+- Fix [the compaction pause duration metric is not emitted for every compaction batch](https://github.com/etcd-io/etcd/pull/19770)
+
+### Package `clientv3`
+
+- [Replace `resolver.State.Addresses` with `resolver.State.Endpoint.Addresses`](https://github.com/etcd-io/etcd/pull/19782).
+- [Deprecated the Metadata field in the Endpoint struct from the client/v3/naming/endpoints package](https://github.com/etcd-io/etcd/pull/19842).
+
+### Dependencies
+
+- Compile binaries using [go 1.23.9](https://github.com/etcd-io/etcd/pull/19867).
+
+---
+
+## v3.6.0-rc.4 (2025-04-15)
+
+### etcd server
+
+- [Switch to validating v3 when v2 and v3 are synchronized](https://github.com/etcd-io/etcd/pull/19703).
+
+### Dependencies
+
+- Compile binaries using [go 1.23.8](https://github.com/etcd-io/etcd/pull/19724)
+
+---
+
+## v3.6.0-rc.3 (2025-03-27)
+
+### etcd server
+
+- [Auto sync members in v3store for the issues which have already been affected by #19557](https://github.com/etcd-io/etcd/pull/19636).
+- [Move `client/internal/v2` into `server/internel/clientv2`](https://github.com/etcd-io/etcd/pull/19673).
+- [Replace ExperimentalMaxLearners with a Feature Gate](https://github.com/etcd-io/etcd/pull/19560).
+
+### etcd grpc-proxy
+
+- Fix [grpcproxy can get stuck in and endless loop causing high CPU usage](https://github.com/etcd-io/etcd/pull/19562)
+
+### Dependencies
+
+- Bump [github.com/golang-jwt/jwt/v5 from 5.2.1 to 5.2.2 to address CVE-2025-30204](https://github.com/etcd-io/etcd/pull/19647).
+- Bump [bump golang.org/x/net from v0.37.0 to v0.38.0 to address CVE-2025-22872](https://github.com/etcd-io/etcd/pull/19687).
+
+---
 
 ## v3.6.0-rc.2 (2025-03-05)
 
@@ -24,7 +68,7 @@ Previous change logs can be found at [CHANGELOG-3.5](https://github.com/etcd-io/
 - Bump [golang.org/x/net to v0.36.0 to address CVE-2025-22870](https://github.com/etcd-io/etcd/pull/19531).
 - Bump [github.com/grpc-ecosystem/grpc-gateway/v2 to v2.26.3 to fix the issue of etcdserver crashing on receiving REST watch stream requests](https://github.com/etcd-io/etcd/pull/19522).
 
-<hr>
+---
 
 ## v3.6.0-rc.1 (2025-02-25)
 
@@ -40,7 +84,7 @@ Previous change logs can be found at [CHANGELOG-3.5](https://github.com/etcd-io/
 
 - Bump [golang.org/x/crypto to v0.35.0 to address CVE-2025-22869](https://github.com/etcd-io/etcd/pull/19480).
 
-<hr>
+---
 
 ## v3.6.0-rc.0 (2025-02-13)
 
@@ -53,6 +97,19 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v3.6.0).
 - `etcdctl` will sleep(2s) in case of range delete without `--range` flag. See [pull/13747](https://github.com/etcd-io/etcd/pull/13747)
 - Applications which depend on etcd v3.6 packages must be built with go version >= v1.18.
 
+#### Flags Removed
+
+- The following flags have been removed:
+
+  - `--enable-v2`
+  - `--experimental-enable-v2v3`
+  - `--proxy`
+  - `--proxy-failure-wait`
+  - `--proxy-refresh-interval`
+  - `--proxy-dial-timeout`
+  - `--proxy-write-timeout`
+  - `--proxy-read-timeout`
+
 ### Deprecations
 
 - Deprecated [V2 discovery](https://etcd.io/docs/v3.5/dev-internal/discovery_protocol/).
@@ -60,7 +117,6 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v3.6.0).
 - Removed [etcdctl defrag --data-dir](https://github.com/etcd-io/etcd/pull/13793).
 - Removed [etcdctl snapshot status](https://github.com/etcd-io/etcd/pull/13809).
 - Removed [etcdctl snapshot restore](https://github.com/etcd-io/etcd/pull/13809).
-- Removed [etcdutl snapshot save](https://github.com/etcd-io/etcd/pull/13809).
 - Removed [NewZapCoreLoggerBuilder in server/embed](https://github.com/etcd-io/etcd/pull/19404)
 
 ### etcdctl v3
@@ -148,4 +204,4 @@ See [List of metrics](https://etcd.io/docs/latest/metrics/) for all metrics per
 - [Upgrade grpc-gateway from v1 to v2](https://github.com/etcd-io/etcd/pull/16595).
 - [Switch from grpc-ecosystem/go-grpc-prometheus to grpc-ecosystem/go-grpc-middleware/providers/prometheus](https://github.com/etcd-io/etcd/pull/19195).
 
-<hr>
+---
diff --git a/CHANGELOG/CHANGELOG-3.7.md b/CHANGELOG/CHANGELOG-3.7.md
index ceb11e53f..770e7d1dd 100644
--- a/CHANGELOG/CHANGELOG-3.7.md
+++ b/CHANGELOG/CHANGELOG-3.7.md
@@ -2,10 +2,14 @@
 
 Previous change logs can be found at [CHANGELOG-3.6](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.6.md).
 
-<hr>
+---
 
 ## v3.7.0 (TBD)
 
+### Dependencies
+
+- Compile binaries using [go 1.24.2](https://github.com/etcd-io/etcd/pull/19723)
+
 ### Deprecations
 
 - Deprecated [UsageFunc in pkg/cobrautl](https://github.com/etcd-io/etcd/pull/18356).
diff --git a/CHANGELOG/CHANGELOG-4.0.md b/CHANGELOG/CHANGELOG-4.0.md
index 860e5efd0..f07bb34fd 100644
--- a/CHANGELOG/CHANGELOG-4.0.md
+++ b/CHANGELOG/CHANGELOG-4.0.md
@@ -2,7 +2,7 @@
 
 Previous change logs can be found at [CHANGELOG-3.x](https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.x.md).
 
-<hr>
+---
 
 ## v4.0.0 (TBD)
 
@@ -40,5 +40,5 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v4.0.0) and
 - Require [*Go 2*](https://blog.golang.org/go2draft).
 
 
-<hr>
+---
 
diff --git a/Dockerfile b/Dockerfile
index 16724fa40..47b28ff5c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 ARG ARCH=amd64
-FROM --platform=linux/${ARCH} gcr.io/distroless/static-debian12@sha256:3f2b64ef97bd285e36132c684e6b2ae8f2723293d09aae046196cca64251acac
+FROM --platform=linux/${ARCH} gcr.io/distroless/static-debian12@sha256:3d0f463de06b7ddff27684ec3bfd0b54a425149d0f8685308b1fdf297b0265e9
 
 ADD etcd /usr/local/bin/
 ADD etcdctl /usr/local/bin/
diff --git a/Documentation/contributor-guide/bump_etcd_version_k8s.md b/Documentation/contributor-guide/bump_etcd_version_k8s.md
new file mode 100644
index 000000000..7b157ef6e
--- /dev/null
+++ b/Documentation/contributor-guide/bump_etcd_version_k8s.md
@@ -0,0 +1,191 @@
+# Bump etcd Version in Kubernetes
+
+This guide will walk through the update of etcd in Kubernetes to a new version (`kubernetes/kubernetes` repository).
+
+> Currently we bump etcd v3.5.x for K8s release-1.33 and lower versions, and we bump etcd v3.6.x for K8s release-1.34 and higher versions.
+
+You can use this [issue](https://github.com/kubernetes/kubernetes/issues/131101) as a reference when updating the etcd version in Kubernetes.
+
+Bumping the etcd version in Kubernetes consists of two steps.
+
+* Bump etcd client SDK
+* Bump etcd image
+
+> The commented lines in this document signifies the line to be changed
+
+## Bump etcd client SDK
+
+> Reference: [link](https://github.com/kubernetes/kubernetes/pull/131103)
+
+You can refer to the guide [here](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/vendor.md) under the **Adding or updating a dependency** section.
+
+1. Get all the etcd modules used in Kubernetes.
+
+    ```bash
+    $ grep 'go.etcd.io/etcd/' go.mod | awk '{print $1}'
+    go.etcd.io/etcd/api/v3
+    go.etcd.io/etcd/client/pkg/v3
+    go.etcd.io/etcd/client/v3
+    go.etcd.io/etcd/client/v2
+    go.etcd.io/etcd/pkg/v3
+    go.etcd.io/etcd/raft/v3
+    go.etcd.io/etcd/server/v3
+    ```
+
+2. For each module, in the root directory of the `kubernetes/kubernetes` repository, fetch the new version in `go.mod` using the following command (using `client/v3` as an example):
+
+    ```bash
+    hack/pin-dependency.sh go.etcd.io/etcd/client/v3 NEW_VERSION
+    ```
+
+3. Rebuild the `vendor` directory and update the `go.mod` files for all staging repositories using the command below. This automatically updates the licenses.
+
+    ```bash
+    hack/update-vendor.sh
+    ```
+
+4. Check if the new dependency requires newer versions of existing dependencies we have pinned. You can check this by:
+
+    * Running `hack/lint-dependencies.sh` against your branch and against `master` and comparing the results.
+    * Checking if any new `replace` directives were added to `go.mod` files of components inside the staging directory.
+
+## Bump etcd image
+
+### Build etcd image
+
+> Reference: [link 1](https://github.com/kubernetes/kubernetes/pull/131105) [link 2](https://github.com/kubernetes/kubernetes/pull/131126)
+
+1. In `build/dependencies.yaml`, update the `version` of `etcd-image` to the new version. Update `golang: etcd release version` if necessary.
+
+    ```yaml
+    - name: "etcd-image"
+      # version: 3.5.17
+      version: 3.5.21
+      refPaths:
+      - path: cluster/images/etcd/Makefile
+        match: BUNDLED_ETCD_VERSIONS\?|
+    ---
+    - name: "golang: etcd release version"
+      # version: 1.22.9
+      version: 1.23.7 # https://github.com/etcd-io/etcd/blob/main/CHANGELOG/CHANGELOG-3.6.md
+    ```
+
+2. In `cluster/images/etcd/Makefile`, include the new version in `BUNDLED_ETCD_VERSIONS` and update the `LATEST_ETCD_VERSION` as well (the image tag will be generated from the `LATEST_ETCD_VERSION`). Update `GOLANG_VERSION` according to the version used to compile that release version (`"golang: etcd release version"` in step 1).
+
+    ```Makefile
+    # BUNDLED_ETCD_VERSIONS?=3.4.18 3.5.17
+    BUNDLED_ETCD_VERSIONS?=3.4.18 3.5.21
+
+    # LATEST_ETCD_VERSION?=3.5.17
+    LATEST_ETCD_VERSION?=3.5.21
+
+    # GOLANG_VERSION := 1.22.9
+    GOLANG_VERSION := 1.23.7
+    ```
+
+3. In `cluster/images/etcd/migrate/options.go`, include the new version in the `supportedEtcdVersions` slice.
+
+    ```go
+    var (
+    // supportedEtcdVersions = []string{"3.4.18", "3.5.17"}
+    supportedEtcdVersions = []string{"3.4.18", "3.5.21"}
+    )
+    ```
+
+### Publish etcd image
+
+> Reference: [link](https://github.com/kubernetes/k8s.io/pull/7957)
+
+1. When the previous step is merged, a post-commit job will run to build the image. You can find the newly built image in the [registry](https://gcr.io/k8s-staging-etcd/etcd).
+
+2. Locate the newly built image and copy its SHA256 digest.
+
+3. Inside the `kubernetes/k8s.io` repository, in `registry.k8s.io/images/k8s-staging-etcd/images.yaml`, create a new entry for the desired version and copy the SHA256 digest.
+
+    ```yaml
+    "sha256:b4a9e4a7e1cf08844c7c4db6a19cab380fbf0aad702b8c01e578e9543671b9f9": ["3.5.17-0"]
+    # ADD:
+    "sha256:d58c035df557080a27387d687092e3fc2b64c6d0e3162dc51453a115f847d121": ["3.5.21-0"]
+    ```
+
+### Update to use the new etcd image
+
+> Reference: [link](https://github.com/kubernetes/kubernetes/pull/131144)
+
+1. In `build/dependencies.yaml`, change the `version` of `etcd` to the new version.
+
+    ```yaml
+    # etcd
+    - name: "etcd"
+    # version: 3.5.17
+    version: 3.5.21
+    refPaths:
+    - path: cluster/gce/manifests/etcd.manifest
+    match: etcd_docker_tag|etcd_version
+    ```
+
+2. In `cluster/gce/manifests/etcd.manifest`, change the image tag to the new image tag and `TARGET_VERSION` to the new version.
+
+    ```manifest
+    // "image": "{{ pillar.get('etcd_docker_repository', 'registry.k8s.io/etcd') }}:{{ pillar.get('etcd_docker_tag', '3.5.17-0') }}",
+
+    "image": "{{ pillar.get('etcd_docker_repository', 'registry.k8s.io/etcd') }}:{{ pillar.get('etcd_docker_tag', '3.5.21-0') }}",
+
+    ---
+
+    { "name": "TARGET_VERSION",
+    // "value": "{{ pillar.get('etcd_version', '3.5.17') }}"
+    "value": "{{ pillar.get('etcd_version', '3.5.21') }}"
+    },
+    ```
+
+3. In `cluster/gce/upgrade-aliases.sh`, update the exports for `ETCD_IMAGE` to the new image tag and `ETCD_VERSION` to the new version.
+
+    ```sh
+    # export ETCD_IMAGE=3.5.17-0
+    export ETCD_IMAGE=3.5.21-0
+    # export ETCD_VERSION=3.5.17
+    export ETCD_VERSION=3.5.21
+    ```
+
+4. In `cmd/kubeadm/app/constants/constants.go`, change the `DefaultEtcdVersion` to the new version. In the same file, update `SupportedEtcdVersion` accordingly.
+
+    ```go
+    // DefaultEtcdVersion = "3.5.17-0"
+    DefaultEtcdVersion = "3.5.21-0"
+
+    ---
+
+    SupportedEtcdVersion = map[uint8]string{
+    // 30: "3.5.17-0",
+    // 31: "3.5.17-0",
+    // 32: "3.5.17-0",
+    // 33: "3.5.17-0",
+    30: "3.5.21-0",
+    31: "3.5.21-0",
+    32: "3.5.21-0",
+    33: "3.5.21-0",
+    }
+    ```
+
+5. In `hack/lib/etcd.sh`, update the `ETCD_VERSION`.
+
+    ```sh
+    # ETCD_VERSION=${ETCD_VERSION:-3.5.17}
+    ETCD_VERSION=${ETCD_VERSION:-3.5.21}
+    ```
+
+6. In `staging/src/k8s.io/sample-apiserver/artifacts/example/deployment.yaml`, update the etcd image used.
+
+    ```yaml
+    - name: etcd
+    # image: gcr.io/etcd-development/etcd:v3.5.17
+    image: gcr.io/etcd-development/etcd:v3.5.21
+    ```
+
+7. In `test/utils/image/manifest.go`, update the etcd image tag.
+
+    ```go
+    // configs[Etcd] = Config{list.GcEtcdRegistry, "etcd", "3.5.17-0"}
+    configs[Etcd] = Config{list.GcEtcdRegistry, "etcd", "3.5.21-0"}
+    ```
diff --git a/Documentation/contributor-guide/dependency_management.md b/Documentation/contributor-guide/dependency_management.md
index 8b022b424..5299677ba 100644
--- a/Documentation/contributor-guide/dependency_management.md
+++ b/Documentation/contributor-guide/dependency_management.md
@@ -147,11 +147,11 @@ References:
 
 [bbolt](https://github.com/etcd-io/bbolt) and [raft](https://github.com/etcd-io/raft) are two core dependencies of etcd.
 
-Both etcd 3.4.x and 3.5.x depend on bbolt 1.3.x, and etcd 3.6.x (`main` branch) depends on bbolt 1.4.x.
+Both etcd 3.4.x and 3.5.x depend on bbolt 1.3.x, and etcd 3.6.x depends on bbolt 1.4.x.
 
 raft is included in the etcd repository for release-3.4 and release-3.5 branches, so etcd 3.4.x and 3.5.x do not depend on any
-external raft module. We moved raft into [a separate repository](https://github.com/etcd-io/raft) starting from 3.6 (`main` branch), and the first raft
-release will be v3.6.0, so etcd 3.6.x will depend on raft 3.6.x.
+external raft module. We moved raft into [a separate repository](https://github.com/etcd-io/raft) starting from 3.6, and the first raft
+release is v3.6.0, so etcd 3.6.0 depends on raft v3.6.0.
 
 Please see the table below:
 
diff --git a/Documentation/contributor-guide/features.md b/Documentation/contributor-guide/features.md
index 5f9839107..ed2d55acc 100644
--- a/Documentation/contributor-guide/features.md
+++ b/Documentation/contributor-guide/features.md
@@ -4,80 +4,84 @@ This document provides an overview of etcd features and general development guid
 
 ## Overview
 
-The etcd features fall into three stages, experimental, stable, and unsafe.
+The etcd features fall into three stages: Alpha, Beta, and GA.
 
-### Experimental
+### Alpha
 
-Any new feature is usually added as an experimental feature. An experimental feature is characterized as below:
+Any new feature is usually added as an Alpha feature. An Alpha feature is characterized as below:
 - Might be buggy due to a lack of user testing. Enabling the feature may not work as expected.
-- Disabled by default when added initially.
+- Disabled by default.
 - Support for such a feature may be dropped at any time without notice
     - Feature-related issues may be given lower priorities.
-    - It can be removed in the next minor or major release without following the feature deprecation policy unless it graduates to a stable future.
+    - It can be removed in the next minor or major release without following the feature deprecation policy unless it graduates to a more stable stage.
 
-### Stable
+### Beta
 
-A stable feature is characterized as below:
+A Beta feature is characterized as below:
 - Supported as part of the supported releases of etcd.
-- May be enabled by default.
+- Enabled by default.
 - Discontinuation of support must follow the feature deprecation policy.
 
-### Unsafe
+### GA
 
-Unsafe features are rare and listed under the `Unsafe feature:` section in the etcd usage documentation. By default, they are disabled. They should be used with caution following documentation. An unsafe feature can be removed in the next minor or major release without following the feature deprecation policy.
+A GA feature is characterized as below:
+- Supported as part of the supported releases of etcd.
+- Always enabled; you cannot disable it. The corresponding feature gate is no longer needed.
+- Discontinuation of support must follow the feature deprecation policy.
 
 ## Development Guidelines
 
 ### Adding a new feature
 
-Any new enhancements to the etcd are typically added as an experimental feature. The general development requirements are listed below. They can be somewhat flexible depending on the scope of the feature and review discussions and will evolve over time.
-- Open an issue
+Any new enhancements to the etcd are typically added as an Alpha feature. 
+
+etcd follows the Kubernetes [KEP process](https://github.com/kubernetes/enhancements/blob/master/keps/sig-architecture/0000-kep-process/README.md) for new enhancements. The general development requirements are listed below. They can be somewhat flexible depending on the scope of the feature and review discussions and will evolve over time.
+- Open a [KEP](https://github.com/kubernetes/enhancements/issues) issue
     - It must provide a clear need for the proposed feature.
-    - It should list development work items as checkboxes. There must be one work item towards future graduation to a stable future.
-    - Label the issue with `type/feature` and `experimental`.
+    - It should list development work items as checkboxes. There must be one work item towards future graduation to Beta.
+    - Label the issue with `/sig etcd`.
     - Keep the issue open for tracking purposes until a decision is made on graduation.
-- Open a Pull Request (PR)
+- Open a [KEP](https://github.com/kubernetes/enhancements) Pull Request (PR). 
+    - The KEP template can be simplified for etcd.
+    - It must provide clear graduation criteria for each stage.
+    - The KEP doc should reside in [keps/sig-etcd](https://github.com/kubernetes/enhancements/tree/master/keps/sig-etcd/)
+- Open Pull Requests (PRs) in [etcd](https://github.com/etcd-io/etcd)
     - Provide unit tests. Integration tests are also recommended as possible.
     - Provide robust e2e test coverage. If the feature being added is complicated or quickly needed, maintainers can decide to go with e2e tests for basic coverage initially and have robust coverage added at a later time before the feature graduation to the stable feature.
     - Provide logs for proper debugging.
     - Provide metrics and benchmarks as needed.
-    - The Feature should be disabled by default.
-    - Any configuration flags related to the implementation of the feature must be prefixed with `experimental` e.g. `--experimental-feature-name`.
+    - Add an Alpha [feature gate](https://etcd.io/docs/v3.6/feature-gates/).
+    - Any code changes or configuration flags related to the implementation of the feature must be gated with the feature gate e.g. `if cfg.ServerFeatureGate.Enabled(features.FeatureName)`.
     - Add a CHANGELOG entry.
-- At least two maintainers must approve feature requirements and related code changes.
+- At least two maintainers must approve the KEP and related code changes.
 
-### Graduating an Experimental feature to Stable
+### Graduating a feature to the next stage
 
-It is important that experimental features don't get stuck in that stage. They should be revisited and moved to the stable stage following the graduation steps as described here.
-
-#### Locate graduation candidate
-Decide if an experimental feature is ready for graduation to the stable stage.
-- Find the issue that was used to enable the experimental feature initially. One way to find such issues is to search for issues with `type/feature` and `experimental` labels.
-- Fix any known open issues against the feature.
-- Make sure the feature was enabled for at least one previous release. Check the PR(s) reference from the issue to see when the feature-related code changes were merged.
+It is important that features don't get stuck in one stage. They should be revisited and moved to the next stage once they meet the graduation criteria listed in the KEP. A feature should stay at one stage for at least one release before being promoted.
 
 #### Provide implementation
-If an experimental feature is found ready for graduation to the stable stage, open a Pull Request (PR) with the following changes.
-- Add robust e2e tests if not already provided.
-- Add a new stable feature flag identical to the experimental feature flag but without the `--experimental` prefix.
-- Deprecate the experimental feature following the [feature deprecation policy](#Deprecating-a-feature).
-- Implementation must ensure that both the graduated and deprecated experimental feature flags work as expected. Note that both these flags will co-exist for the timeframe described in the feature deprecation policy.
-- Enable the graduated feature by default if needed.
+
+If a feature is found ready for graduation to the next stage, open a Pull Request (PR) with the following changes.
+- Update the feature `PreRelease` stage in `server/features/etcd_features.go`.
+- Update the status in the original KEP issue.
 
 At least two maintainers must approve the work. Patch releases should not be considered for graduation.
 
 ### Deprecating a feature
 
-#### Experimental
-An experimental feature deprecates when it graduates to the stable stage.
-- Add a deprecation message in the documentation of the experimental feature with a recommendation to use a related stable feature. e.g. `DEPRECATED. Use <feature-name> instead.`
-- Add a `deprecated` label in the issue that was initially used to enable the experimental feature.
+#### Alpha
+Alpha features can be removed without going through the deprecation process.
+- Remove the feature gate in `server/features/etcd_features.go`, and clean up all relevant code.
+- Close the original KEP issue with reasons to drop the feature.
+
+#### Beta and GA
+As the project evolves, a Beta/GA feature may sometimes need to be deprecated and removed. Such a situation should be handled using the steps below:
 
-#### Stable
-As the project evolves, a stable feature may sometimes need to be deprecated and removed. Such a situation should be handled using the steps below:
-- Create an issue for tracking purposes.
-- Add a deprecation message in the feature usage documentation before a planned release for feature deprecation. e.g. `To be deprecated in <release>.`. If a new feature replaces the `To be deprecated` feature, then also provide a message saying so. e.g. `Use <feature-name> instead.`.
-- Deprecate the feature in the planned release with a message as part of the feature usage documentation. e.g. `DEPRECATED`. If a new feature replaces the deprecated feature, then also provide a message saying so. e.g. `DEPRECATED. Use <feature-name> instead.`.
-- Add a `deprecated` label in the related issue.
+- A Beta/GA feature can only be deprecated after at least 2 minor or major releases.
+- Update original KEP issue if it has not been closed or create a new etcd issue with reasons and steps to deprecate the feature.
+- Add the feature deprecation documentation in the release notes and feature gates documentation of the next minor/major release.
+- In the next minor/major release, set the feature gate to `{Default: false, PreRelease: featuregate.Deprecated, LockedToDefault: false}` in `server/features/etcd_features.go`. Deprecated feature gates must respond with a warning when used.
+    - If the feature has GAed, and the original gated codes has been cleaned up, add the disablement codes back with the feature gate.
+- In the minor/major release after the next, set the feature gate to `{Default: false, PreRelease: featuregate.Deprecated, LockedToDefault: true}` in `server/features/etcd_features.go`, and start cleaning the code.
 
-Remove the deprecated feature in the following release. Close any related issue(s). At least two maintainers must approve the work. Patch releases should not be considered for deprecation.
+At least two maintainers must approve the work. Patch releases should not be considered for deprecation.
diff --git a/Documentation/contributor-guide/release.md b/Documentation/contributor-guide/release.md
index 54f7f7646..081c0ac96 100644
--- a/Documentation/contributor-guide/release.md
+++ b/Documentation/contributor-guide/release.md
@@ -71,9 +71,14 @@ which don't need to be executed before releasing each version.
 
 ### Release steps
 
+At least one day before the release:
+
 1. Raise an issue to publish the release plan, e.g. [issues/17350](https://github.com/etcd-io/etcd/issues/17350).
-2. Raise a `kubernetes/org` pull request to ensure members of the release team are added to the [release github team](https://github.com/orgs/etcd-io/teams/release-etcd).
-3. Verify you can pass the authentication to the image registries,
+2. Raise a `kubernetes/org` pull request ([example PR](https://github.com/kubernetes/org/pull/5582)) to ensure members of the release team are added to the [release github team](https://github.com/orgs/etcd-io/teams/release-etcd).
+
+On the day of the release:
+
+1. Verify you can pass the authentication to the image registries,
    - `docker login gcr.io`
    - `docker login quay.io`
      - If the release person doesn't have access to 1password, one of the owners (@ahrtr, @ivanvc, @jmhbnz, @serathius) needs to share the password with them per [this guide](https://support.1password.com/share-items/). See rough steps below,
@@ -82,45 +87,46 @@ which don't need to be executed before releasing each version.
        - Select `Password of quay.io`.
        - Click `Share` on the top right, and set expiration as `1 hour` and only available to the release person using his/her email.
        - Click `Copy Link` then send the link to the release person via slack or email.
-4. Clone the etcd repository and checkout the target branch,
+2. Clone the etcd repository and checkout the target branch,
    - `git clone --branch release-3.X git@github.com:etcd-io/etcd.git`
-5. Run the release script under the repository's root directory, replacing `${VERSION}` with a value without the `v` prefix, i.e. `3.5.13`.
+3. Run the release script under the repository's root directory, replacing `${VERSION}` with a value without the `v` prefix, i.e. `3.5.13`.
    - `DRY_RUN=false ./scripts/release.sh ${VERSION}`
    - **NOTE:** When doing a pre-release (i.e., a version from the main branch, 3.6.0-alpha.2), you will need to explicitly set the branch to main:
-     ```
+
+     ```bash
      DRY_RUN=false BRANCH=main ./scripts/release.sh ${VERSION}
      ```
 
    It generates all release binaries under the directory `/tmp/etcd-release-${VERSION}/etcd/release/` and images. Binaries are pushed to the Google Cloud bucket
    under project `etcd-development`, and images are pushed to `quay.io` and `gcr.io`.
-6. Publish the release page on GitHub
+4. Publish the release page on GitHub
    - Open the **draft** release URL shown by the release script
    - Click the pen button at the top right to edit the release
    - Review that it looks correct, reviewing that the bottom checkboxes are checked depending on the
      release version (v3.4 no checkboxes, v3.5 has the set as latest release checkbox checked,
      v3.6 has the set as pre-release checkbox checked)
    - Then, publish the release
-7. Announce to the etcd-dev googlegroup
+5. Announce to the etcd-dev googlegroup
 
    Follow the format of previous release emails sent to etcd-dev@googlegroups.com, see an example below. After sending out the email, ask one of the mailing list maintainers to approve the email from the pending list. Additionally, label the release email as `Release`.
 
-```text
-Hello,
+   ```text
+   Hello,
 
-etcd v3.4.30 is now public!
+   etcd v3.4.30 is now public!
 
-https://github.com/etcd-io/etcd/releases/tag/v3.4.30
+   https://github.com/etcd-io/etcd/releases/tag/v3.4.30
 
-Thanks to everyone who contributed to the release!
+   Thanks to everyone who contributed to the release!
 
-etcd team
-```
+   etcd team
+   ```
 
-8. Update the changelog to reflect the correct release date.
-9. Paste the release link to the issue raised in Step 1 and close the issue.
-10. Raise a follow-up `kubernetes/org` pull request to return the GitHub release team to empty, least privilege state.
-11. Crease a new stable branch through `git push origin release-${VERSION_MAJOR}.${VERSION_MINOR}` if this is a new major or minor stable release.
-12. Re-generate a new password for quay.io if needed (e.g. shared to a contributor who isn't in the release team, and we should rotate the password at least once every 3 months).
+6. Update the changelog to reflect the correct release date.
+7. Paste the release link to the issue raised in Step 1 and close the issue.
+8. Raise a follow-up `kubernetes/org` pull request to return the GitHub release team to empty, least privilege state.
+9. Crease a new stable branch through `git push origin release-${VERSION_MAJOR}.${VERSION_MINOR}` if this is a new major or minor stable release.
+10. Re-generate a new password for quay.io if needed (e.g. shared to a contributor who isn't in the release team, and we should rotate the password at least once every 3 months).
 
 #### Release known issues
 
diff --git a/Documentation/contributor-guide/roadmap.md b/Documentation/contributor-guide/roadmap.md
index c036e2eb4..f683490e7 100644
--- a/Documentation/contributor-guide/roadmap.md
+++ b/Documentation/contributor-guide/roadmap.md
@@ -12,18 +12,18 @@ Each item has an assigned priority. Refer to [priority definitions](https://gith
 
 For a full list of tasks in `v3.6.0`, please see [milestone etcd-v3.6](https://github.com/etcd-io/etcd/milestone/38).
 
-| Title                                                                                                              | Priority | Status      | Note                                                                                                         |
-|--------------------------------------------------------------------------------------------------------------------|----------|-------------|--------------------------------------------------------------------------------------------------------------|
-| [Support downgrade](https://github.com/etcd-io/etcd/issues/11716)                                                  | priority/important-soon | In progress | etcd will support downgrade starting from 3.6.0. But it will also support offline downgrade from 3.5 to 3.4. |
-| [StoreV2 deprecation](https://github.com/etcd-io/etcd/issues/12913)                                                | priority/important-soon | In progress | This task will be covered in both 3.6 and 3.7.                                                               |
-| [Release raft 3.6.0](https://github.com/etcd-io/raft/issues/89)                                                    | priority/important-soon | Not started | etcd 3.6.0 will depends on raft 3.6.0                                                                        |
-| [Release bbolt 1.4.0](https://github.com/etcd-io/bbolt/issues/553)                                                 | priority/important-soon | Not started | etcd 3.6.0 will depends on bbolt 1.4.0                                                                       |
-| [Support /livez and /readyz endpoints](https://github.com/etcd-io/etcd/issues/16007)                               | priority/important-longterm | In progress | It provides clearer APIs, and can also work around the stalled writes issue                                   |
+| Title                                                                                                              | Priority                    | Status      | Note                                                                                                         |
+|--------------------------------------------------------------------------------------------------------------------|-----------------------------|-------------|--------------------------------------------------------------------------------------------------------------|
+| [Support downgrade](https://github.com/etcd-io/etcd/issues/11716)                                                  | priority/important-soon     | In progress | etcd will support downgrade starting from 3.6.0. But it will also support offline downgrade from 3.5 to 3.4. |
+| [StoreV2 deprecation](https://github.com/etcd-io/etcd/issues/12913)                                                | priority/important-soon     | In progress | This task will be covered in both 3.6 and 3.7.                                                               |
+| [Release raft 3.6.0](https://github.com/etcd-io/raft/issues/89)                                                    | priority/important-soon     | Completed   | etcd 3.6.0 will depends on raft 3.6.0                                                                        |
+| [Release bbolt 1.4.0](https://github.com/etcd-io/bbolt/issues/553)                                                 | priority/important-soon     | Completed   | etcd 3.6.0 will depends on bbolt 1.4.0                                                                       |
+| [Support /livez and /readyz endpoints](https://github.com/etcd-io/etcd/issues/16007)                               | priority/important-longterm | Completed   | It provides clearer APIs, and can also work around the stalled writes issue                                  |
 | [Bump gRPC](https://github.com/etcd-io/etcd/issues/16290)                                                          | priority/important-longterm | Completed   | It isn't guaranteed to be resolved in 3.6, and might be postponed to 3.7 depending on the effort and risk.   |
 | [Deprecate grpc-gateway or bump it](https://github.com/etcd-io/etcd/issues/14499)                                  | priority/important-longterm | Completed   | It isn't guaranteed to be resolved in 3.6, and might be postponed to 3.7 depending on the effort and risk.   |
 | [bbolt: Add logger into bbolt](https://github.com/etcd-io/bbolt/issues/509)                                        | priority/important-longterm | Completed   | It's important to diagnose bbolt issues                                                                      |
 | [bbolt: Add surgery commands](https://github.com/etcd-io/bbolt/issues/370)                                         | priority/important-longterm | Completed   | Surgery commands are important for fixing corrupted db files                                                 |
-| [Evaluate and (Gradulate or deprecate/remove) experimental features](https://github.com/etcd-io/etcd/issues/16292) | priority/backlog | Not started | This task will be covered in both 3.6 and 3.7.                                                               |
+| [Evaluate and (Gradulate or deprecate/remove) experimental features](https://github.com/etcd-io/etcd/issues/16292) | priority/backlog            | Not started | This task will be covered in both 3.6 and 3.7.                                                               |
 
 ## v3.7.0
 
@@ -32,8 +32,9 @@ For a full list of tasks in `v3.7.0`, please see [milestone etcd-v3.7](https://g
 | Title                                                                                                             | Priority | Note                                                                              |
 |-------------------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------------------------------------------|
 | [StoreV2 deprecation](https://github.com/etcd-io/etcd/issues/12913)                                               | P0       | Finish the remaining tasks 3.7.                                                   |
+| [Support range stream](https://github.com/etcd-io/etcd/issues/12342)                                              | P0       | to be investigated & discussed.                                                   |
 | [Refactor lease: Lease might be revoked by mistake by old leader](https://github.com/etcd-io/etcd/issues/15247)   | P1       | to be investigated & discussed                                                    |
-| [Integrate raft's new feature (async write) into etcd](https://github.com/etcd-io/etcd/issues/16291)              | P1       | It should improve the performance                                             |
+| [Integrate raft's new feature (async write) into etcd](https://github.com/etcd-io/etcd/issues/16291)              | P1       | It should improve the performance                                                 |
 | [bbolt: Support customizing the bbolt rebalance threshold](https://github.com/etcd-io/bbolt/issues/422)           | P2       | It may get rid of etcd's defragmentation. Both bbolt and etcd need to be changed. |
 | [Evaluate and (graduate or deprecate/remove) experimental features](https://github.com/etcd-io/etcd/issues/16292) | P2       | Finish the remaining tasks 3.7.                                                   |
 
diff --git a/Makefile b/Makefile
index 20df43d29..abadf8f17 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,8 @@
+REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel)
+
 .PHONY: all
 all: build
-include tests/robustness/makefile.mk
+include $(REPOSITORY_ROOT)/tests/robustness/Makefile
 
 .PHONY: build
 build:
@@ -54,6 +56,10 @@ test-grpcproxy-e2e: build
 test-e2e-release: build
 	PASSES="release e2e" ./scripts/test.sh $(GO_TEST_FLAGS)
 
+.PHONY: test-release
+test-release:
+	PASSES="release_tests" ./scripts/test.sh $(GO_TEST_FLAGS)
+
 .PHONY: test-robustness
 test-robustness:
 	PASSES="robustness" ./scripts/test.sh $(GO_TEST_FLAGS)
@@ -63,8 +69,11 @@ test-coverage:
 	COVERDIR=covdir PASSES="build cov" ./scripts/test.sh $(GO_TEST_FLAGS)
 
 .PHONY: upload-coverage-report
-upload-coverage-report: test-coverage
-	./scripts/codecov_upload.sh
+upload-coverage-report:
+	return_code=0; \
+	$(MAKE) test-coverage || return_code=$$?; \
+	COVERDIR=covdir ./scripts/codecov_upload.sh; \
+	exit $$return_code
 
 .PHONY: fuzz
 fuzz: 
@@ -222,4 +231,3 @@ sync-toolchain-directive:
 .PHONY: markdown-diff-lint
 markdown-diff-lint:
 	./scripts/markdown_diff_lint.sh
-	
\ No newline at end of file
diff --git a/api/go.mod b/api/go.mod
index 51967d0df..e5ca9cc5c 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/api/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/coreos/go-semver v0.3.1
@@ -10,21 +10,21 @@ require (
 	github.com/golang/protobuf v1.5.4
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
 	github.com/stretchr/testify v1.10.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb
-	google.golang.org/grpc v1.70.0
-	google.golang.org/protobuf v1.36.5
+	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34
+	google.golang.org/grpc v1.72.0
+	google.golang.org/protobuf v1.36.6
 )
 
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
-	go.opentelemetry.io/otel v1.34.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.34.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/api/go.sum b/api/go.sum
index 5bc304426..fc48f215f 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -1,7 +1,7 @@
 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -22,8 +22,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -32,16 +32,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -51,20 +51,20 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/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=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 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=
@@ -73,14 +73,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/bill-of-materials.json b/bill-of-materials.json
index 584a53d42..bd0efd460 100644
--- a/bill-of-materials.json
+++ b/bill-of-materials.json
@@ -17,6 +17,15 @@
 			}
 		]
 	},
+	{
+		"project": "github.com/antithesishq/antithesis-sdk-go",
+		"licenses": [
+			{
+				"type": "MIT License",
+				"confidence": 1
+			}
+		]
+	},
 	{
 		"project": "github.com/beorn7/perks/quantile",
 		"licenses": [
@@ -260,33 +269,6 @@
 			}
 		]
 	},
-	{
-		"project": "github.com/klauspost/compress",
-		"licenses": [
-			{
-				"type": "Apache License 2.0",
-				"confidence": 0.9376299376299376
-			}
-		]
-	},
-	{
-		"project": "github.com/klauspost/compress/internal/snapref",
-		"licenses": [
-			{
-				"type": "BSD 3-clause \"New\" or \"Revised\" License",
-				"confidence": 0.9663865546218487
-			}
-		]
-	},
-	{
-		"project": "github.com/klauspost/compress/zstd/internal/xxhash",
-		"licenses": [
-			{
-				"type": "MIT License",
-				"confidence": 1
-			}
-		]
-	},
 	{
 		"project": "github.com/mattn/go-colorable",
 		"licenses": [
diff --git a/client/internal/v2/go.mod b/client/internal/v2/go.mod
index 7b7e0a473..c7013cf1f 100644
--- a/client/internal/v2/go.mod
+++ b/client/internal/v2/go.mod
@@ -2,20 +2,20 @@ module go.etcd.io/etcd/client/v2
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
-	sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3
 )
 
 require (
 	github.com/coreos/go-semver v0.3.1 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/kr/text v0.2.0 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/client/internal/v2/go.sum b/client/internal/v2/go.sum
index 51cbd9176..5e88bc955 100644
--- a/client/internal/v2/go.sum
+++ b/client/internal/v2/go.sum
@@ -1,14 +1,14 @@
 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -18,5 +18,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
diff --git a/client/pkg/go.mod b/client/pkg/go.mod
index cfef8ade3..634655bda 100644
--- a/client/pkg/go.mod
+++ b/client/pkg/go.mod
@@ -2,19 +2,19 @@ module go.etcd.io/etcd/client/pkg/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/coreos/go-systemd/v22 v22.5.0
 	github.com/stretchr/testify v1.10.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/sys v0.30.0
+	golang.org/x/sys v0.33.0
 )
 
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/kr/pretty v0.3.1 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
diff --git a/client/pkg/go.sum b/client/pkg/go.sum
index 5de2e9fdb..b7f7b4897 100644
--- a/client/pkg/go.sum
+++ b/client/pkg/go.sum
@@ -1,8 +1,8 @@
 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -12,8 +12,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
@@ -25,8 +25,8 @@ 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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/client/pkg/types/urls.go b/client/pkg/types/urls.go
index 49a38967e..7dca16c04 100644
--- a/client/pkg/types/urls.go
+++ b/client/pkg/types/urls.go
@@ -47,7 +47,6 @@ func NewURLs(strs []string) (URLs, error) {
 				return nil, fmt.Errorf("URL must not contain a path: %s", in)
 			}
 		case "unix", "unixs":
-			break
 		default:
 			return nil, fmt.Errorf("URL scheme must be http, https, unix, or unixs: %s", in)
 		}
diff --git a/client/v3/client.go b/client/v3/client.go
index 24f598898..998f5ae03 100644
--- a/client/v3/client.go
+++ b/client/v3/client.go
@@ -43,6 +43,7 @@ import (
 var (
 	ErrNoAvailableEndpoints = errors.New("etcdclient: no available endpoints")
 	ErrOldCluster           = errors.New("etcdclient: old cluster version")
+	ErrMutuallyExclusiveCfg = errors.New("Username/Password and Token configurations are mutually exclusive")
 )
 
 // Client provides and manages an etcd v3 client session.
@@ -69,7 +70,10 @@ type Client struct {
 	// Username is a user name for authentication.
 	Username string
 	// Password is a password for authentication.
-	Password        string
+	Password string
+	// Token is a JWT used for authentication instead of a password.
+	Token string
+
 	authTokenBundle credentials.PerRPCCredentialsBundle
 
 	callOpts []grpc.CallOption
@@ -288,6 +292,11 @@ func (c *Client) Dial(ep string) (*grpc.ClientConn, error) {
 func (c *Client) getToken(ctx context.Context) error {
 	var err error // return last error in a case of fail
 
+	if c.Token != "" {
+		c.authTokenBundle.UpdateAuthToken(c.Token)
+		return nil
+	}
+
 	if c.Username == "" || c.Password == "" {
 		return nil
 	}
@@ -376,6 +385,10 @@ func newClient(cfg *Config) (*Client, error) {
 		creds = credentials.NewTransportCredential(cfg.TLS)
 	}
 
+	if cfg.Token != "" && (cfg.Username != "" || cfg.Password != "") {
+		return nil, ErrMutuallyExclusiveCfg
+	}
+
 	// use a temporary skeleton client to bootstrap first connection
 	baseCtx := context.TODO()
 	if cfg.Context != nil {
@@ -414,6 +427,12 @@ func newClient(cfg *Config) (*Client, error) {
 		client.Password = cfg.Password
 		client.authTokenBundle = credentials.NewPerRPCCredentialBundle()
 	}
+
+	if cfg.Token != "" {
+		client.Token = cfg.Token
+		client.authTokenBundle = credentials.NewPerRPCCredentialBundle()
+	}
+
 	if cfg.MaxCallSendMsgSize > 0 || cfg.MaxCallRecvMsgSize > 0 {
 		if cfg.MaxCallRecvMsgSize > 0 && cfg.MaxCallSendMsgSize > cfg.MaxCallRecvMsgSize {
 			return nil, fmt.Errorf("gRPC message recv limit (%d bytes) must be greater than send limit (%d bytes)", cfg.MaxCallRecvMsgSize, cfg.MaxCallSendMsgSize)
diff --git a/client/v3/config.go b/client/v3/config.go
index 8351828d2..61d093760 100644
--- a/client/v3/config.go
+++ b/client/v3/config.go
@@ -66,6 +66,9 @@ type Config struct {
 	// Password is a password for authentication.
 	Password string `json:"password"`
 
+	// Token is a JWT used for authentication instead of a password.
+	Token string `json:"token"`
+
 	// RejectOldCluster when set will refuse to create a client against an outdated cluster.
 	RejectOldCluster bool `json:"reject-old-cluster"`
 
@@ -130,6 +133,7 @@ type SecureConfig struct {
 type AuthConfig struct {
 	Username string `json:"username"`
 	Password string `json:"password"`
+	Token    string `json:"token"`
 }
 
 func (cs *ConfigSpec) Clone() *ConfigSpec {
@@ -157,7 +161,7 @@ func (cs *ConfigSpec) Clone() *ConfigSpec {
 }
 
 func (cfg AuthConfig) Empty() bool {
-	return cfg.Username == "" && cfg.Password == ""
+	return cfg.Username == "" && cfg.Password == "" && cfg.Token == ""
 }
 
 // NewClientConfig creates a Config based on the provided ConfigSpec.
@@ -180,6 +184,7 @@ func NewClientConfig(confSpec *ConfigSpec, lg *zap.Logger) (*Config, error) {
 	if confSpec.Auth != nil {
 		cfg.Username = confSpec.Auth.Username
 		cfg.Password = confSpec.Auth.Password
+		cfg.Token = confSpec.Auth.Token
 	}
 
 	return cfg, nil
diff --git a/client/v3/go.mod b/client/v3/go.mod
index 98b2640ba..2f732e07f 100644
--- a/client/v3/go.mod
+++ b/client/v3/go.mod
@@ -2,18 +2,18 @@ module go.etcd.io/etcd/client/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/dustin/go-humanize v1.0.1
 	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
-	github.com/prometheus/client_golang v1.21.0
+	github.com/prometheus/client_golang v1.22.0
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.uber.org/zap v1.27.0
-	google.golang.org/grpc v1.70.0
+	google.golang.org/grpc v1.72.0
 	sigs.k8s.io/yaml v1.4.0
 )
 
@@ -21,26 +21,25 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
-	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.62.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.63.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
-	go.opentelemetry.io/otel/metric v1.34.0 // indirect
-	go.opentelemetry.io/otel/trace v1.34.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/client/v3/go.sum b/client/v3/go.sum
index 84124bc68..1daaf247e 100644
--- a/client/v3/go.sum
+++ b/client/v3/go.sum
@@ -6,8 +6,8 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr
 github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -42,14 +42,14 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
-github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -60,16 +60,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -85,20 +85,20 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/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=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 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=
@@ -107,14 +107,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/client/v3/internal/resolver/resolver.go b/client/v3/internal/resolver/resolver.go
index b5c9de007..c7f9fb1ae 100644
--- a/client/v3/internal/resolver/resolver.go
+++ b/client/v3/internal/resolver/resolver.go
@@ -60,16 +60,28 @@ func (r *EtcdManualResolver) SetEndpoints(endpoints []string) {
 }
 
 func (r EtcdManualResolver) updateState() {
-	if r.CC != nil {
-		addresses := make([]resolver.Address, len(r.endpoints))
+	if getCC(r) != nil {
+		eps := make([]resolver.Endpoint, len(r.endpoints))
 		for i, ep := range r.endpoints {
 			addr, serverName := endpoint.Interpret(ep)
-			addresses[i] = resolver.Address{Addr: addr, ServerName: serverName}
+			eps[i] = resolver.Endpoint{Addresses: []resolver.Address{
+				{Addr: addr, ServerName: serverName},
+			}}
 		}
 		state := resolver.State{
-			Addresses:     addresses,
+			Endpoints:     eps,
 			ServiceConfig: r.serviceConfig,
 		}
 		r.UpdateState(state)
 	}
 }
+
+func getCC(r EtcdManualResolver) (cc resolver.ClientConn) {
+	defer func() {
+		if rec := recover(); rec != nil {
+			cc = nil
+		}
+	}()
+
+	return r.CC()
+}
diff --git a/client/v3/naming/endpoints/endpoints.go b/client/v3/naming/endpoints/endpoints.go
index bcc96b14f..322b6c286 100644
--- a/client/v3/naming/endpoints/endpoints.go
+++ b/client/v3/naming/endpoints/endpoints.go
@@ -32,6 +32,8 @@ type Endpoint struct {
 	// Metadata is the information associated with Addr, which may be used
 	// to make load balancing decision.
 	// Since etcd 3.1
+	//
+	// Deprecated: The field is deprecated and will be removed in 3.7.
 	Metadata any
 }
 
diff --git a/client/v3/naming/resolver/resolver.go b/client/v3/naming/resolver/resolver.go
index f42b2b0b6..f8629aaaa 100644
--- a/client/v3/naming/resolver/resolver.go
+++ b/client/v3/naming/resolver/resolver.go
@@ -100,22 +100,26 @@ func (r *resolver) watch() {
 				}
 			}
 
-			addrs := convertToGRPCAddress(allUps)
-			r.cc.UpdateState(gresolver.State{Addresses: addrs})
+			eps := convertToGRPCEndpoint(allUps)
+			r.cc.UpdateState(gresolver.State{Endpoints: eps})
 		}
 	}
 }
 
-func convertToGRPCAddress(ups map[string]*endpoints.Update) []gresolver.Address {
-	var addrs []gresolver.Address
+func convertToGRPCEndpoint(ups map[string]*endpoints.Update) []gresolver.Endpoint {
+	var eps []gresolver.Endpoint
 	for _, up := range ups {
-		addr := gresolver.Address{
-			Addr:     up.Endpoint.Addr,
-			Metadata: up.Endpoint.Metadata,
+		ep := gresolver.Endpoint{
+			Addresses: []gresolver.Address{
+				{
+					Addr:     up.Endpoint.Addr,
+					Metadata: up.Endpoint.Metadata,
+				},
+			},
 		}
-		addrs = append(addrs, addr)
+		eps = append(eps, ep)
 	}
-	return addrs
+	return eps
 }
 
 // ResolveNow is a no-op here.
diff --git a/etcdctl/ctlv3/command/global.go b/etcdctl/ctlv3/command/global.go
index 019b638d3..602f2776d 100644
--- a/etcdctl/ctlv3/command/global.go
+++ b/etcdctl/ctlv3/command/global.go
@@ -58,6 +58,7 @@ type GlobalFlags struct {
 
 	User     string
 	Password string
+	Token    string
 
 	Debug bool
 }
@@ -290,13 +291,22 @@ func authCfgFromCmd(cmd *cobra.Command) *clientv3.AuthConfig {
 	if err != nil {
 		cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
 	}
+	tokenFlag, err := cmd.Flags().GetString("auth-jwt-token")
+	if err != nil {
+		cobrautl.ExitWithError(cobrautl.ExitBadArgs, err)
+	}
 
-	if userFlag == "" {
+	if userFlag == "" && tokenFlag == "" {
 		return nil
 	}
 
 	var cfg clientv3.AuthConfig
 
+	if tokenFlag != "" {
+		cfg.Token = tokenFlag
+		return &cfg
+	}
+
 	if passwordFlag == "" {
 		splitted := strings.SplitN(userFlag, ":", 2)
 		if len(splitted) < 2 {
diff --git a/etcdctl/ctlv3/ctl.go b/etcdctl/ctlv3/ctl.go
index 992538503..e992fa4fa 100644
--- a/etcdctl/ctlv3/ctl.go
+++ b/etcdctl/ctlv3/ctl.go
@@ -70,6 +70,7 @@ func init() {
 	rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.CertFile, "cert", "", "identify secure client using this TLS certificate file")
 	rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.KeyFile, "key", "", "identify secure client using this TLS key file")
 	rootCmd.PersistentFlags().StringVar(&globalFlags.TLS.TrustedCAFile, "cacert", "", "verify certificates of TLS-enabled secure servers using this CA bundle")
+	rootCmd.PersistentFlags().StringVar(&globalFlags.Token, "auth-jwt-token", "", "JWT token used for authentication (if this option is used, --user and --password should not be set)")
 	rootCmd.PersistentFlags().StringVar(&globalFlags.User, "user", "", "username[:password] for authentication (prompt if password is not supplied)")
 	rootCmd.PersistentFlags().StringVar(&globalFlags.Password, "password", "", "password for authentication (if this option is used, --user option shouldn't include password)")
 	rootCmd.PersistentFlags().StringVarP(&globalFlags.TLS.ServerName, "discovery-srv", "d", "", "domain name to query for SRV records describing cluster endpoints")
diff --git a/etcdctl/go.mod b/etcdctl/go.mod
index 4dca248df..98e6f8fa5 100644
--- a/etcdctl/go.mod
+++ b/etcdctl/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/etcdctl/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/bgentry/speakeasy v0.2.0
@@ -17,15 +17,15 @@ require (
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/time v0.10.0
-	google.golang.org/grpc v1.70.0
+	golang.org/x/time v0.11.0
+	google.golang.org/grpc v1.72.0
 )
 
 require (
 	github.com/VividCortex/ewma v1.2.0 // indirect
 	github.com/coreos/go-semver v0.3.1 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/fatih/color v1.18.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
@@ -34,16 +34,15 @@ require (
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
-	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/etcdctl/go.sum b/etcdctl/go.sum
index 2966b4e18..fa6ac53fd 100644
--- a/etcdctl/go.sum
+++ b/etcdctl/go.sum
@@ -13,8 +13,8 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -42,8 +42,6 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
-github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -59,14 +57,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
-github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -85,16 +83,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -110,8 +108,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/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=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -119,14 +117,14 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 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=
@@ -135,14 +133,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/etcdutl/go.mod b/etcdutl/go.mod
index 2c4fb823a..39eca540f 100644
--- a/etcdutl/go.mod
+++ b/etcdutl/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/etcdutl/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 replace (
 	go.etcd.io/etcd/api/v3 => ../api
@@ -42,28 +42,27 @@ require (
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/gorilla/websocket v1.4.2 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jonboulle/clockwork v0.5.0 // indirect
-	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_golang v1.21.0 // indirect
-	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.62.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_golang v1.22.0 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.63.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -73,26 +72,26 @@ require (
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
-	go.opentelemetry.io/otel v1.34.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
-	go.opentelemetry.io/otel/metric v1.34.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.34.0 // indirect
-	go.opentelemetry.io/otel/trace v1.34.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/crypto v0.35.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	golang.org/x/time v0.10.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/grpc v1.70.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	golang.org/x/crypto v0.37.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	golang.org/x/time v0.11.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/grpc v1.72.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/etcdutl/go.sum b/etcdutl/go.sum
index 95d3a0f29..bcbbf2f9e 100644
--- a/etcdutl/go.sum
+++ b/etcdutl/go.sum
@@ -12,8 +12,9 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -24,8 +25,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 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/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
@@ -35,8 +36,8 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
@@ -64,14 +65,15 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
-github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -104,24 +106,24 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
-go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
-go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
+go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
 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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -131,8 +133,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -140,25 +142,25 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/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=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 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=
@@ -167,14 +169,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -183,7 +185,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/go.mod b/go.mod
index 357fc0cf2..2e496d521 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 replace (
 	go.etcd.io/etcd/api/v3 => ./api
@@ -35,9 +35,9 @@ require (
 	go.etcd.io/etcd/tests/v3 v3.0.0-00010101000000-000000000000
 	go.etcd.io/raft/v3 v3.6.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/time v0.10.0
-	google.golang.org/grpc v1.70.0
-	google.golang.org/protobuf v1.36.5
+	golang.org/x/time v0.11.0
+	google.golang.org/grpc v1.72.0
+	google.golang.org/protobuf v1.36.6
 )
 
 require (
@@ -46,34 +46,33 @@ require (
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/fatih/color v1.18.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
+	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/gorilla/websocket v1.4.2 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jonboulle/clockwork v0.5.0 // indirect
-	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_golang v1.21.0 // indirect
-	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.62.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/client_golang v1.22.0 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.63.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -83,23 +82,23 @@ require (
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.etcd.io/gofail v0.2.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
-	go.opentelemetry.io/otel v1.34.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
-	go.opentelemetry.io/otel/metric v1.34.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.34.0 // indirect
-	go.opentelemetry.io/otel/trace v1.34.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/crypto v0.35.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
+	golang.org/x/crypto v0.37.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 16e860331..dda36b2db 100644
--- a/go.sum
+++ b/go.sum
@@ -23,8 +23,9 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -44,11 +45,11 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 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/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -63,8 +64,8 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
@@ -102,15 +103,16 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
-github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+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/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -149,24 +151,24 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
-go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
-go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
+go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -179,8 +181,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -196,16 +198,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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=
@@ -213,14 +215,14 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -238,19 +240,19 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -262,7 +264,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/pkg/featuregate/feature_gate.go b/pkg/featuregate/feature_gate.go
index 343668fc3..2f3eaf558 100644
--- a/pkg/featuregate/feature_gate.go
+++ b/pkg/featuregate/feature_gate.go
@@ -166,7 +166,7 @@ func setUnsetBetaGates(known map[Feature]FeatureSpec, enabled map[Feature]bool,
 // Set, String, and Type implement pflag.Value
 var _ pflag.Value = &featureGate{}
 
-func New(name string, lg *zap.Logger) *featureGate {
+func New(name string, lg *zap.Logger) MutableFeatureGate {
 	if lg == nil {
 		lg = zap.NewNop()
 	}
diff --git a/pkg/go.mod b/pkg/go.mod
index 84104eb9e..5b5ce7d2a 100644
--- a/pkg/go.mod
+++ b/pkg/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/pkg/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/creack/pty v1.1.18
@@ -12,23 +12,23 @@ require (
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.uber.org/zap v1.27.0
-	google.golang.org/grpc v1.70.0
+	google.golang.org/grpc v1.72.0
 )
 
 require (
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	go.opentelemetry.io/otel v1.34.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.34.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/pkg/go.sum b/pkg/go.sum
index cee007e6d..4eb0b74e5 100644
--- a/pkg/go.sum
+++ b/pkg/go.sum
@@ -3,8 +3,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -24,8 +24,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -37,34 +37,34 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 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/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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/scripts/codecov_upload.sh b/scripts/codecov_upload.sh
index 2983310ae..7dbad265b 100755
--- a/scripts/codecov_upload.sh
+++ b/scripts/codecov_upload.sh
@@ -1,9 +1,18 @@
 #!/usr/bin/env bash
 
-# Script used to collect and upload test coverage (mostly by travis).
-# Usage ./test_coverage_upload.sh [log_file]
+# Script used to collect and upload test coverage.
 
 set -o pipefail
 
 # We try to upload whatever we have:
-bash <(curl -s https://codecov.io/bash) -f ./covdir/all.coverprofile -cF all
+mkdir -p bin
+curl -sf -o ./bin/codecov.sh https://codecov.io/bash
+
+bash ./bin/codecov.sh -f "${COVERDIR}/all.coverprofile" \
+  -cF all \
+  -C "${PULL_PULL_SHA:-${PULL_BASE_SHA}}" \
+  -r "${REPO_OWNER}/${REPO_NAME}" \
+  -P "${PULL_NUMBER}" \
+  -b "${BUILD_ID}" \
+  -B "${PULL_BASE_REF}" \
+  -N "${PULL_BASE_SHA}"
diff --git a/scripts/markdown_diff_lint.sh b/scripts/markdown_diff_lint.sh
index d864d3e7e..4649593ca 100755
--- a/scripts/markdown_diff_lint.sh
+++ b/scripts/markdown_diff_lint.sh
@@ -2,11 +2,12 @@
 # This script runs markdownlint-cli2 on changed files.
 # Usage: ./markdown_lint.sh
 
+ETCD_ROOT_DIR=$(git rev-parse --show-toplevel)
 
-# We source ./scripts/test_lib.sh, it sets the log functions and color variables.
-source ./scripts/test_lib.sh
+# We source ./scripts/test_utils.sh, it sets the log functions and color variables.
+source ./scripts/test_utils.sh
 
-# When we source ./scripts/test_lib.sh, it has the line set -u which treats unset variables as errors.
+# When we source ./scripts/test_utils.sh, it has the line set -u which treats unset variables as errors.
 # We need to unset the variable to avoid the error.
 set +u -eo pipefail
 
@@ -66,6 +67,7 @@ for file in "${changed_files[@]}"; do
   markdownlint-cli2 "${file}" --config "${ETCD_ROOT_DIR}/tools/.markdownlint.jsonc" 2>/dev/null || true
   while IFS= read -r line; do
     line_number=$(echo "${line}" | awk -F: '{print $2}' | awk '{print $1}')
+
     while [ "${i}" -lt "${#end_ranges[@]}" ] && [ "${line_number}" -gt "${end_ranges["${i}"]}" ]; do
       i=$((1 + i))
     done
diff --git a/scripts/release_notes.tpl.txt b/scripts/release_notes.tpl.txt
index 67701d988..3fe1284fd 100644
--- a/scripts/release_notes.tpl.txt
+++ b/scripts/release_notes.tpl.txt
@@ -16,7 +16,7 @@ rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
 rm -rf /tmp/etcd-download-test && mkdir -p /tmp/etcd-download-test
 
 curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
-tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd-download-test --strip-components=1
+tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd-download-test --strip-components=1 --no-same-owner
 rm -f /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
 
 /tmp/etcd-download-test/etcd --version
diff --git a/scripts/test.sh b/scripts/test.sh
index e7176dfce..7a1376548 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -67,6 +67,18 @@ if [ -z "${GOARCH:-}" ]; then
   GOARCH=$(go env GOARCH);
 fi
 
+if [ -z "${OS:-}" ]; then
+  OS=$(uname -s | tr '[:upper:]' '[:lower:]')
+fi
+
+if [ -z "${ARCH:-}" ]; then
+  ARCH=$(uname -m)
+
+  if [ "$ARCH" = "arm64" ]; then
+    ARCH="aarch64"
+  fi
+fi
+
 # determine whether target supports race detection
 if [ -z "${RACE:-}" ] ; then
   if [ "$GOARCH" == "amd64" ] || [ "$GOARCH" == "arm64" ]; then
@@ -331,7 +343,7 @@ function shellcheck_pass {
   SHELLCHECK=shellcheck
   if ! tool_exists "shellcheck" "https://github.com/koalaman/shellcheck#installing"; then
     log_callout "Installing shellcheck $SHELLCHECK_VERSION"
-    wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/shellcheck-${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" | tar -xJv -C /tmp/ --strip-components=1
+    wget -qO- "https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/shellcheck-${SHELLCHECK_VERSION}.${OS}.${ARCH}.tar.xz" | tar -xJv -C /tmp/ --strip-components=1
     mkdir -p ./bin
     mv /tmp/shellcheck ./bin/
     SHELLCHECK=./bin/shellcheck
@@ -356,7 +368,14 @@ function markdown_marker_pass {
   # TODO: check other markdown files when marker handles headers with '[]'
   if ! tool_exists "$marker" "https://crates.io/crates/marker"; then
     log_callout "Installing markdown marker $MARKDOWN_MARKER_VERSION"
-    wget -qO- "https://github.com/crawford/marker/releases/download/${MARKDOWN_MARKER_VERSION}/marker-${MARKDOWN_MARKER_VERSION}-x86_64-unknown-linux-musl.tar.gz" | tar -xzv -C /tmp/ --strip-components=1 >/dev/null
+    MARKER_OS=$OS
+    if [ "$OS" = "darwin" ]; then
+      MARKER_OS="apple-darwin"
+    elif [ "$OS" = "linux" ]; then
+      MARKER_OS="unknown-linux-musl"
+    fi
+
+    wget -qO- "https://github.com/crawford/marker/releases/download/${MARKDOWN_MARKER_VERSION}/marker-${MARKDOWN_MARKER_VERSION}-${ARCH}-${MARKER_OS}.tar.gz" | tar -xzv -C /tmp/ --strip-components=1 >/dev/null
     mkdir -p ./bin
     mv /tmp/marker ./bin/
     marker=./bin/marker
@@ -483,7 +502,10 @@ function bom_pass {
   run cp go.sum go.sum.tmp || return 2
   run cp go.mod go.mod.tmp || return 2
 
-  output=$(GOFLAGS=-mod=mod run_go_tool github.com/appscodelabs/license-bill-of-materials \
+  # BOM file should be generated for linux. Otherwise running this command on other operating systems such as OSX
+  # results in certain dependencies being excluded from the BOM file, such as procfs. 
+  # For more info, https://github.com/etcd-io/etcd/issues/19665
+  output=$(GOOS=linux GOFLAGS=-mod=mod run_go_tool github.com/appscodelabs/license-bill-of-materials \
     --override-file ./bill-of-materials.override.json \
     "${modules[@]}")
   code="$?"
@@ -596,11 +618,39 @@ function release_pass {
       ;;
   esac
 
-  tar xzvf "/tmp/$file" -C /tmp/ --strip-components=1
+  tar xzvf "/tmp/$file" -C /tmp/ --strip-components=1 --no-same-owner
   mkdir -p ./bin
   mv /tmp/etcd ./bin/etcd-last-release
 }
 
+function release_tests_pass {
+  if [ -z "${VERSION:-}" ]; then
+    VERSION=$(go list -m go.etcd.io/etcd/api/v3 2>/dev/null | \
+     awk '{split(substr($2,2), a, "."); print a[1]"."a[2]".99"}')
+  fi
+
+  if [ -n "${CI:-}" ]; then
+    git config user.email "prow@etcd.io"
+    git config user.name "Prow"
+
+    gpg --batch --gen-key <<EOF
+%no-protection
+Key-Type: 1
+Key-Length: 2048
+Subkey-Type: 1
+Subkey-Length: 2048
+Name-Real: Prow
+Name-Email: prow@etcd.io
+Expire-Date: 0
+EOF
+
+    git remote add origin https://github.com/etcd-io/etcd.git
+  fi
+
+  DRY_RUN=true run "${ETCD_ROOT_DIR}/scripts/release.sh" --no-upload --no-docker-push --no-gh-release --in-place "${VERSION}"
+  VERSION="${VERSION}" run "${ETCD_ROOT_DIR}/scripts/test_images.sh"
+}
+
 function mod_tidy_for_module {
   run go mod tidy -diff
 }
diff --git a/scripts/test_lib.sh b/scripts/test_lib.sh
index 1691ecb56..31ca532b0 100644
--- a/scripts/test_lib.sh
+++ b/scripts/test_lib.sh
@@ -2,6 +2,8 @@
 
 set -euo pipefail
 
+source ./scripts/test_utils.sh
+
 ROOT_MODULE="go.etcd.io/etcd"
 
 if [[ "$(go list)" != "${ROOT_MODULE}/v3" ]]; then
@@ -15,96 +17,6 @@ function set_root_dir {
 
 set_root_dir
 
-####   Convenient IO methods #####
-
-COLOR_RED='\033[0;31m'
-COLOR_ORANGE='\033[0;33m'
-COLOR_GREEN='\033[0;32m'
-COLOR_LIGHTCYAN='\033[0;36m'
-COLOR_BLUE='\033[0;94m'
-COLOR_MAGENTA='\033[95m'
-COLOR_BOLD='\033[1m'
-COLOR_NONE='\033[0m' # No Color
-
-
-function log_error {
-  >&2 echo -n -e "${COLOR_BOLD}${COLOR_RED}"
-  >&2 echo "$@"
-  >&2 echo -n -e "${COLOR_NONE}"
-}
-
-function log_warning {
-  >&2 echo -n -e "${COLOR_ORANGE}"
-  >&2 echo "$@"
-  >&2 echo -n -e "${COLOR_NONE}"
-}
-
-function log_callout {
-  >&2 echo -n -e "${COLOR_LIGHTCYAN}"
-  >&2 echo "$@"
-  >&2 echo -n -e "${COLOR_NONE}"
-}
-
-function log_cmd {
-  >&2 echo -n -e "${COLOR_BLUE}"
-  >&2 echo "$@"
-  >&2 echo -n -e "${COLOR_NONE}"
-}
-
-function log_success {
-  >&2 echo -n -e "${COLOR_GREEN}"
-  >&2 echo "$@"
-  >&2 echo -n -e "${COLOR_NONE}"
-}
-
-function log_info {
-  >&2 echo -n -e "${COLOR_NONE}"
-  >&2 echo "$@"
-  >&2 echo -n -e "${COLOR_NONE}"
-}
-
-# From http://stackoverflow.com/a/12498485
-function relativePath {
-  # both $1 and $2 are absolute paths beginning with /
-  # returns relative path to $2 from $1
-  local source=$1
-  local target=$2
-
-  local commonPart=$source
-  local result=""
-
-  while [[ "${target#"$commonPart"}" == "${target}" ]]; do
-    # no match, means that candidate common part is not correct
-    # go up one level (reduce common part)
-    commonPart="$(dirname "$commonPart")"
-    # and record that we went back, with correct / handling
-    if [[ -z $result ]]; then
-      result=".."
-    else
-      result="../$result"
-    fi
-  done
-
-  if [[ $commonPart == "/" ]]; then
-    # special case for root (no common path)
-    result="$result/"
-  fi
-
-  # since we now have identified the common part,
-  # compute the non-common part
-  local forwardPart="${target#"$commonPart"}"
-
-  # and now stick all parts together
-  if [[ -n $result ]] && [[ -n $forwardPart ]]; then
-    result="$result$forwardPart"
-  elif [[ -n $forwardPart ]]; then
-    # extra slash removal
-    result="${forwardPart:1}"
-  fi
-
-  echo "$result"
-}
-
 ####   Discovery of files/packages within a go module #####
 
 # go_srcs_in_module
@@ -408,7 +320,7 @@ function tool_pkg_dir {
 # tool_get_bin [tool]
 function run_go_tool {
   local cmdbin
-  if ! cmdbin=$(GOARCH="" tool_get_bin "${1}"); then
+  if ! cmdbin=$(GOARCH="" GOOS="" tool_get_bin "${1}"); then
     log_warning "Failed to install tool '${1}'"
     return 2
   fi
diff --git a/scripts/test_utils.sh b/scripts/test_utils.sh
new file mode 100644
index 000000000..5925fc7a0
--- /dev/null
+++ b/scripts/test_utils.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+####   Convenient IO methods #####
+
+export COLOR_RED='\033[0;31m'
+export COLOR_ORANGE='\033[0;33m'
+export COLOR_GREEN='\033[0;32m'
+export COLOR_LIGHTCYAN='\033[0;36m'
+export COLOR_BLUE='\033[0;94m'
+export COLOR_BOLD='\033[1m'
+export COLOR_MAGENTA='\033[95m'
+export COLOR_NONE='\033[0m' # No Color
+
+
+function log_error {
+  >&2 echo -n -e "${COLOR_BOLD}${COLOR_RED}"
+  >&2 echo "$@"
+  >&2 echo -n -e "${COLOR_NONE}"
+}
+
+function log_warning {
+  >&2 echo -n -e "${COLOR_ORANGE}"
+  >&2 echo "$@"
+  >&2 echo -n -e "${COLOR_NONE}"
+}
+
+function log_callout {
+  >&2 echo -n -e "${COLOR_LIGHTCYAN}"
+  >&2 echo "$@"
+  >&2 echo -n -e "${COLOR_NONE}"
+}
+
+function log_cmd {
+  >&2 echo -n -e "${COLOR_BLUE}"
+  >&2 echo "$@"
+  >&2 echo -n -e "${COLOR_NONE}"
+}
+
+function log_success {
+  >&2 echo -n -e "${COLOR_GREEN}"
+  >&2 echo "$@"
+  >&2 echo -n -e "${COLOR_NONE}"
+}
+
+function log_info {
+  >&2 echo -n -e "${COLOR_NONE}"
+  >&2 echo "$@"
+  >&2 echo -n -e "${COLOR_NONE}"
+}
+
+# From http://stackoverflow.com/a/12498485
+function relativePath {
+  # both $1 and $2 are absolute paths beginning with /
+  # returns relative path to $2 from $1
+  local source=$1
+  local target=$2
+
+  local commonPart=$source
+  local result=""
+
+  while [[ "${target#"$commonPart"}" == "${target}" ]]; do
+    # no match, means that candidate common part is not correct
+    # go up one level (reduce common part)
+    commonPart="$(dirname "$commonPart")"
+    # and record that we went back, with correct / handling
+    if [[ -z $result ]]; then
+      result=".."
+    else
+      result="../$result"
+    fi
+  done
+
+  if [[ $commonPart == "/" ]]; then
+    # special case for root (no common path)
+    result="$result/"
+  fi
+
+  # since we now have identified the common part,
+  # compute the non-common part
+  local forwardPart="${target#"$commonPart"}"
+
+  # and now stick all parts together
+  if [[ -n $result ]] && [[ -n $forwardPart ]]; then
+    result="$result$forwardPart"
+  elif [[ -n $forwardPart ]]; then
+    # extra slash removal
+    result="${forwardPart:1}"
+  fi
+
+  echo "$result"
+}
diff --git a/scripts/updatebom.sh b/scripts/updatebom.sh
index 2c6bf8720..a1de4a876 100755
--- a/scripts/updatebom.sh
+++ b/scripts/updatebom.sh
@@ -14,7 +14,10 @@ function bom_fixlet {
   # shellcheck disable=SC2207
   modules=($(modules_for_bom))
 
-  if GOFLAGS=-mod=mod run_go_tool "github.com/appscodelabs/license-bill-of-materials" \
+  # BOM file should be generated for linux. Otherwise running this command on other operating systems such as OSX
+  # results in certain dependencies being excluded from the BOM file, such as procfs. 
+  # For more info, https://github.com/etcd-io/etcd/issues/19665
+  if GOOS=linux GOFLAGS=-mod=mod run_go_tool "github.com/appscodelabs/license-bill-of-materials" \
       --override-file ./bill-of-materials.override.json \
       "${modules[@]}" > ./bill-of-materials.json.tmp; then
     cp ./bill-of-materials.json.tmp ./bill-of-materials.json
diff --git a/server/etcdmain/etcd.go b/server/etcdmain/etcd.go
index 16bba3736..cb633e7e5 100644
--- a/server/etcdmain/etcd.go
+++ b/server/etcdmain/etcd.go
@@ -64,8 +64,7 @@ func startEtcdOrProxyV2(args []string) {
 	lg.Info("Running: ", zap.Strings("args", args))
 	if err != nil {
 		lg.Warn("failed to verify flags", zap.Error(err))
-		switch {
-		case errorspkg.Is(err, embed.ErrUnsetAdvertiseClientURLsFlag):
+		if errorspkg.Is(err, embed.ErrUnsetAdvertiseClientURLsFlag) {
 			lg.Warn("advertise client URLs are not set", zap.Error(err))
 		}
 		os.Exit(1)
diff --git a/server/etcdserver/api/membership/cluster.go b/server/etcdserver/api/membership/cluster.go
index d2c97df74..8f6e0d2ae 100644
--- a/server/etcdserver/api/membership/cluster.go
+++ b/server/etcdserver/api/membership/cluster.go
@@ -313,9 +313,16 @@ func (c *RaftCluster) Recover(onSet func(*zap.Logger, *semver.Version)) {
 
 // ValidateConfigurationChange takes a proposed ConfChange and
 // ensures that it is still valid.
-func (c *RaftCluster) ValidateConfigurationChange(cc raftpb.ConfChange) error {
-	// TODO: this must be switched to backend as well.
-	membersMap, removedMap := membersFromStore(c.lg, c.v2store)
+func (c *RaftCluster) ValidateConfigurationChange(cc raftpb.ConfChange, shouldApplyV3 ShouldApplyV3) error {
+	var membersMap map[types.ID]*Member
+	var removedMap map[types.ID]bool
+
+	if shouldApplyV3 {
+		membersMap, removedMap = c.be.MustReadMembersFromBackend()
+	} else {
+		membersMap, removedMap = membersFromStore(c.lg, c.v2store)
+	}
+
 	id := types.ID(cc.NodeID)
 	if removedMap[id] {
 		return ErrIDRemoved
diff --git a/server/etcdserver/apply/apply.go b/server/etcdserver/apply/apply.go
index 661760880..ff454973d 100644
--- a/server/etcdserver/apply/apply.go
+++ b/server/etcdserver/apply/apply.go
@@ -16,6 +16,7 @@ package apply
 
 import (
 	"context"
+	"time"
 
 	"github.com/coreos/go-semver/semver"
 	"github.com/gogo/protobuf/proto"
@@ -62,13 +63,13 @@ type Result struct {
 	Trace *traceutil.Trace
 }
 
-type applyFunc func(r *pb.InternalRaftRequest) *Result
+type applyFunc func(*pb.InternalRaftRequest, membership.ShouldApplyV3) *Result
 
 // applierV3 is the interface for processing V3 raft messages
 type applierV3 interface {
 	// Apply executes the generic portion of application logic for the current applier, but
 	// delegates the actual execution to the applyFunc method.
-	Apply(r *pb.InternalRaftRequest, applyFunc applyFunc) *Result
+	Apply(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3, applyFunc applyFunc) *Result
 
 	Put(p *pb.PutRequest) (*pb.PutResponse, *traceutil.Trace, error)
 	Range(r *pb.RangeRequest) (*pb.RangeResponse, *traceutil.Trace, error)
@@ -102,6 +103,25 @@ type applierV3 interface {
 	RoleDelete(ua *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error)
 	UserList(ua *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error)
 	RoleList(ua *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error)
+	ClusterVersionSet(r *membershippb.ClusterVersionSetRequest, shouldApplyV3 membership.ShouldApplyV3)
+	ClusterMemberAttrSet(r *membershippb.ClusterMemberAttrSetRequest, shouldApplyV3 membership.ShouldApplyV3)
+	DowngradeInfoSet(r *membershippb.DowngradeInfoSetRequest, shouldApplyV3 membership.ShouldApplyV3)
+}
+
+type ApplierOptions struct {
+	Logger                       *zap.Logger
+	KV                           mvcc.KV
+	AlarmStore                   *v3alarm.AlarmStore
+	AuthStore                    auth.AuthStore
+	Lessor                       lease.Lessor
+	Cluster                      *membership.RaftCluster
+	RaftStatus                   RaftStatusGetter
+	SnapshotServer               SnapshotServer
+	ConsistentIndex              cindex.ConsistentIndexer
+	TxnModeWriteWithSharedBuffer bool
+	Backend                      backend.Backend
+	QuotaBackendBytesCfg         int64
+	WarningApplyDuration         time.Duration
 }
 
 type SnapshotServer interface {
@@ -109,85 +129,55 @@ type SnapshotServer interface {
 }
 
 type applierV3backend struct {
-	lg              *zap.Logger
-	kv              mvcc.KV
-	alarmStore      *v3alarm.AlarmStore
-	authStore       auth.AuthStore
-	lessor          lease.Lessor
-	cluster         *membership.RaftCluster
-	raftStatus      RaftStatusGetter
-	snapshotServer  SnapshotServer
-	consistentIndex cindex.ConsistentIndexer
-
-	txnModeWriteWithSharedBuffer bool
-}
-
-func newApplierV3Backend(
-	lg *zap.Logger,
-	kv mvcc.KV,
-	alarmStore *v3alarm.AlarmStore,
-	authStore auth.AuthStore,
-	lessor lease.Lessor,
-	cluster *membership.RaftCluster,
-	raftStatus RaftStatusGetter,
-	snapshotServer SnapshotServer,
-	consistentIndex cindex.ConsistentIndexer,
-	txnModeWriteWithSharedBuffer bool,
-) applierV3 {
+	options ApplierOptions
+}
+
+func newApplierV3Backend(opts ApplierOptions) applierV3 {
 	return &applierV3backend{
-		lg:                           lg,
-		kv:                           kv,
-		alarmStore:                   alarmStore,
-		authStore:                    authStore,
-		lessor:                       lessor,
-		cluster:                      cluster,
-		raftStatus:                   raftStatus,
-		snapshotServer:               snapshotServer,
-		consistentIndex:              consistentIndex,
-		txnModeWriteWithSharedBuffer: txnModeWriteWithSharedBuffer,
+		options: opts,
 	}
 }
 
-func (a *applierV3backend) Apply(r *pb.InternalRaftRequest, applyFunc applyFunc) *Result {
-	return applyFunc(r)
+func (a *applierV3backend) Apply(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3, applyFunc applyFunc) *Result {
+	return applyFunc(r, shouldApplyV3)
 }
 
 func (a *applierV3backend) Put(p *pb.PutRequest) (resp *pb.PutResponse, trace *traceutil.Trace, err error) {
-	return mvcctxn.Put(context.TODO(), a.lg, a.lessor, a.kv, p)
+	return mvcctxn.Put(context.TODO(), a.options.Logger, a.options.Lessor, a.options.KV, p)
 }
 
 func (a *applierV3backend) DeleteRange(dr *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, *traceutil.Trace, error) {
-	return mvcctxn.DeleteRange(context.TODO(), a.lg, a.kv, dr)
+	return mvcctxn.DeleteRange(context.TODO(), a.options.Logger, a.options.KV, dr)
 }
 
 func (a *applierV3backend) Range(r *pb.RangeRequest) (*pb.RangeResponse, *traceutil.Trace, error) {
-	return mvcctxn.Range(context.TODO(), a.lg, a.kv, r)
+	return mvcctxn.Range(context.TODO(), a.options.Logger, a.options.KV, r)
 }
 
 func (a *applierV3backend) Txn(rt *pb.TxnRequest) (*pb.TxnResponse, *traceutil.Trace, error) {
-	return mvcctxn.Txn(context.TODO(), a.lg, rt, a.txnModeWriteWithSharedBuffer, a.kv, a.lessor)
+	return mvcctxn.Txn(context.TODO(), a.options.Logger, rt, a.options.TxnModeWriteWithSharedBuffer, a.options.KV, a.options.Lessor)
 }
 
 func (a *applierV3backend) Compaction(compaction *pb.CompactionRequest) (*pb.CompactionResponse, <-chan struct{}, *traceutil.Trace, error) {
 	resp := &pb.CompactionResponse{}
 	resp.Header = &pb.ResponseHeader{}
 	trace := traceutil.New("compact",
-		a.lg,
+		a.options.Logger,
 		traceutil.Field{Key: "revision", Value: compaction.Revision},
 	)
 
-	ch, err := a.kv.Compact(trace, compaction.Revision)
+	ch, err := a.options.KV.Compact(trace, compaction.Revision)
 	if err != nil {
 		return nil, ch, nil, err
 	}
 	// get the current revision. which key to get is not important.
-	rr, _ := a.kv.Range(context.TODO(), []byte("compaction"), nil, mvcc.RangeOptions{})
+	rr, _ := a.options.KV.Range(context.TODO(), []byte("compaction"), nil, mvcc.RangeOptions{})
 	resp.Header.Revision = rr.Rev
 	return resp, ch, trace, err
 }
 
 func (a *applierV3backend) LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) {
-	l, err := a.lessor.Grant(lease.LeaseID(lc.ID), lc.TTL)
+	l, err := a.options.Lessor.Grant(lease.LeaseID(lc.ID), lc.TTL)
 	resp := &pb.LeaseGrantResponse{}
 	if err == nil {
 		resp.ID = int64(l.ID)
@@ -198,13 +188,13 @@ func (a *applierV3backend) LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantR
 }
 
 func (a *applierV3backend) LeaseRevoke(lc *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) {
-	err := a.lessor.Revoke(lease.LeaseID(lc.ID))
+	err := a.options.Lessor.Revoke(lease.LeaseID(lc.ID))
 	return &pb.LeaseRevokeResponse{Header: a.newHeader()}, err
 }
 
 func (a *applierV3backend) LeaseCheckpoint(lc *pb.LeaseCheckpointRequest) (*pb.LeaseCheckpointResponse, error) {
 	for _, c := range lc.Checkpoints {
-		err := a.lessor.Checkpoint(lease.LeaseID(c.ID), c.Remaining_TTL)
+		err := a.options.Lessor.Checkpoint(lease.LeaseID(c.ID), c.Remaining_TTL)
 		if err != nil {
 			return &pb.LeaseCheckpointResponse{Header: a.newHeader()}, err
 		}
@@ -217,19 +207,19 @@ func (a *applierV3backend) Alarm(ar *pb.AlarmRequest) (*pb.AlarmResponse, error)
 
 	switch ar.Action {
 	case pb.AlarmRequest_GET:
-		resp.Alarms = a.alarmStore.Get(ar.Alarm)
+		resp.Alarms = a.options.AlarmStore.Get(ar.Alarm)
 	case pb.AlarmRequest_ACTIVATE:
 		if ar.Alarm == pb.AlarmType_NONE {
 			break
 		}
-		m := a.alarmStore.Activate(types.ID(ar.MemberID), ar.Alarm)
+		m := a.options.AlarmStore.Activate(types.ID(ar.MemberID), ar.Alarm)
 		if m == nil {
 			break
 		}
 		resp.Alarms = append(resp.Alarms, m)
 		alarms.WithLabelValues(types.ID(ar.MemberID).String(), m.Alarm.String()).Inc()
 	case pb.AlarmRequest_DEACTIVATE:
-		m := a.alarmStore.Deactivate(types.ID(ar.MemberID), ar.Alarm)
+		m := a.options.AlarmStore.Deactivate(types.ID(ar.MemberID), ar.Alarm)
 		if m == nil {
 			break
 		}
@@ -266,7 +256,7 @@ func (a *applierV3Capped) LeaseGrant(_ *pb.LeaseGrantRequest) (*pb.LeaseGrantRes
 }
 
 func (a *applierV3backend) AuthEnable() (*pb.AuthEnableResponse, error) {
-	err := a.authStore.AuthEnable()
+	err := a.options.AuthStore.AuthEnable()
 	if err != nil {
 		return nil, err
 	}
@@ -274,19 +264,19 @@ func (a *applierV3backend) AuthEnable() (*pb.AuthEnableResponse, error) {
 }
 
 func (a *applierV3backend) AuthDisable() (*pb.AuthDisableResponse, error) {
-	a.authStore.AuthDisable()
+	a.options.AuthStore.AuthDisable()
 	return &pb.AuthDisableResponse{Header: a.newHeader()}, nil
 }
 
 func (a *applierV3backend) AuthStatus() (*pb.AuthStatusResponse, error) {
-	enabled := a.authStore.IsAuthEnabled()
-	authRevision := a.authStore.Revision()
+	enabled := a.options.AuthStore.IsAuthEnabled()
+	authRevision := a.options.AuthStore.Revision()
 	return &pb.AuthStatusResponse{Header: a.newHeader(), Enabled: enabled, AuthRevision: authRevision}, nil
 }
 
 func (a *applierV3backend) Authenticate(r *pb.InternalAuthenticateRequest) (*pb.AuthenticateResponse, error) {
-	ctx := context.WithValue(context.WithValue(context.Background(), auth.AuthenticateParamIndex{}, a.consistentIndex.ConsistentIndex()), auth.AuthenticateParamSimpleTokenPrefix{}, r.SimpleToken)
-	resp, err := a.authStore.Authenticate(ctx, r.Name, r.Password)
+	ctx := context.WithValue(context.WithValue(context.Background(), auth.AuthenticateParamIndex{}, a.options.ConsistentIndex.ConsistentIndex()), auth.AuthenticateParamSimpleTokenPrefix{}, r.SimpleToken)
+	resp, err := a.options.AuthStore.Authenticate(ctx, r.Name, r.Password)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -294,7 +284,7 @@ func (a *applierV3backend) Authenticate(r *pb.InternalAuthenticateRequest) (*pb.
 }
 
 func (a *applierV3backend) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) {
-	resp, err := a.authStore.UserAdd(r)
+	resp, err := a.options.AuthStore.UserAdd(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -302,7 +292,7 @@ func (a *applierV3backend) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddRes
 }
 
 func (a *applierV3backend) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) {
-	resp, err := a.authStore.UserDelete(r)
+	resp, err := a.options.AuthStore.UserDelete(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -310,7 +300,7 @@ func (a *applierV3backend) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUser
 }
 
 func (a *applierV3backend) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) {
-	resp, err := a.authStore.UserChangePassword(r)
+	resp, err := a.options.AuthStore.UserChangePassword(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -318,7 +308,7 @@ func (a *applierV3backend) UserChangePassword(r *pb.AuthUserChangePasswordReques
 }
 
 func (a *applierV3backend) UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error) {
-	resp, err := a.authStore.UserGrantRole(r)
+	resp, err := a.options.AuthStore.UserGrantRole(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -326,7 +316,7 @@ func (a *applierV3backend) UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.Au
 }
 
 func (a *applierV3backend) UserGet(r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error) {
-	resp, err := a.authStore.UserGet(r)
+	resp, err := a.options.AuthStore.UserGet(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -334,7 +324,7 @@ func (a *applierV3backend) UserGet(r *pb.AuthUserGetRequest) (*pb.AuthUserGetRes
 }
 
 func (a *applierV3backend) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error) {
-	resp, err := a.authStore.UserRevokeRole(r)
+	resp, err := a.options.AuthStore.UserRevokeRole(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -342,7 +332,7 @@ func (a *applierV3backend) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.
 }
 
 func (a *applierV3backend) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error) {
-	resp, err := a.authStore.RoleAdd(r)
+	resp, err := a.options.AuthStore.RoleAdd(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -350,7 +340,7 @@ func (a *applierV3backend) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddRes
 }
 
 func (a *applierV3backend) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error) {
-	resp, err := a.authStore.RoleGrantPermission(r)
+	resp, err := a.options.AuthStore.RoleGrantPermission(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -358,7 +348,7 @@ func (a *applierV3backend) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequ
 }
 
 func (a *applierV3backend) RoleGet(r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error) {
-	resp, err := a.authStore.RoleGet(r)
+	resp, err := a.options.AuthStore.RoleGet(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -366,7 +356,7 @@ func (a *applierV3backend) RoleGet(r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetRes
 }
 
 func (a *applierV3backend) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error) {
-	resp, err := a.authStore.RoleRevokePermission(r)
+	resp, err := a.options.AuthStore.RoleRevokePermission(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -374,7 +364,7 @@ func (a *applierV3backend) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRe
 }
 
 func (a *applierV3backend) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error) {
-	resp, err := a.authStore.RoleDelete(r)
+	resp, err := a.options.AuthStore.RoleDelete(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -382,7 +372,7 @@ func (a *applierV3backend) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRole
 }
 
 func (a *applierV3backend) UserList(r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error) {
-	resp, err := a.authStore.UserList(r)
+	resp, err := a.options.AuthStore.UserList(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
@@ -390,46 +380,32 @@ func (a *applierV3backend) UserList(r *pb.AuthUserListRequest) (*pb.AuthUserList
 }
 
 func (a *applierV3backend) RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error) {
-	resp, err := a.authStore.RoleList(r)
+	resp, err := a.options.AuthStore.RoleList(r)
 	if resp != nil {
 		resp.Header = a.newHeader()
 	}
 	return resp, err
 }
 
-type ApplierMembership struct {
-	lg             *zap.Logger
-	cluster        *membership.RaftCluster
-	snapshotServer SnapshotServer
-}
-
-func NewApplierMembership(lg *zap.Logger, cluster *membership.RaftCluster, snapshotServer SnapshotServer) *ApplierMembership {
-	return &ApplierMembership{
-		lg:             lg,
-		cluster:        cluster,
-		snapshotServer: snapshotServer,
-	}
-}
-
-func (a *ApplierMembership) ClusterVersionSet(r *membershippb.ClusterVersionSetRequest, shouldApplyV3 membership.ShouldApplyV3) {
-	prevVersion := a.cluster.Version()
+func (a *applierV3backend) ClusterVersionSet(r *membershippb.ClusterVersionSetRequest, shouldApplyV3 membership.ShouldApplyV3) {
+	prevVersion := a.options.Cluster.Version()
 	newVersion := semver.Must(semver.NewVersion(r.Ver))
-	a.cluster.SetVersion(newVersion, api.UpdateCapability, shouldApplyV3)
+	a.options.Cluster.SetVersion(newVersion, api.UpdateCapability, shouldApplyV3)
 	// Force snapshot after cluster version downgrade.
 	if prevVersion != nil && newVersion.LessThan(*prevVersion) {
-		lg := a.lg
+		lg := a.options.Logger
 		if lg != nil {
 			lg.Info("Cluster version downgrade detected, forcing snapshot",
 				zap.String("prev-cluster-version", prevVersion.String()),
 				zap.String("new-cluster-version", newVersion.String()),
 			)
 		}
-		a.snapshotServer.ForceSnapshot()
+		a.options.SnapshotServer.ForceSnapshot()
 	}
 }
 
-func (a *ApplierMembership) ClusterMemberAttrSet(r *membershippb.ClusterMemberAttrSetRequest, shouldApplyV3 membership.ShouldApplyV3) {
-	a.cluster.UpdateAttributes(
+func (a *applierV3backend) ClusterMemberAttrSet(r *membershippb.ClusterMemberAttrSetRequest, shouldApplyV3 membership.ShouldApplyV3) {
+	a.options.Cluster.UpdateAttributes(
 		types.ID(r.Member_ID),
 		membership.Attributes{
 			Name:       r.MemberAttributes.Name,
@@ -439,12 +415,12 @@ func (a *ApplierMembership) ClusterMemberAttrSet(r *membershippb.ClusterMemberAt
 	)
 }
 
-func (a *ApplierMembership) DowngradeInfoSet(r *membershippb.DowngradeInfoSetRequest, shouldApplyV3 membership.ShouldApplyV3) {
+func (a *applierV3backend) DowngradeInfoSet(r *membershippb.DowngradeInfoSetRequest, shouldApplyV3 membership.ShouldApplyV3) {
 	d := version.DowngradeInfo{Enabled: false}
 	if r.Enabled {
 		d = version.DowngradeInfo{Enabled: true, TargetVersion: r.Ver}
 	}
-	a.cluster.SetDowngradeInfo(&d, shouldApplyV3)
+	a.options.Cluster.SetDowngradeInfo(&d, shouldApplyV3)
 }
 
 type quotaApplierV3 struct {
@@ -485,9 +461,9 @@ func (a *quotaApplierV3) LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantRes
 
 func (a *applierV3backend) newHeader() *pb.ResponseHeader {
 	return &pb.ResponseHeader{
-		ClusterId: uint64(a.cluster.ID()),
-		MemberId:  uint64(a.raftStatus.MemberID()),
-		Revision:  a.kv.Rev(),
-		RaftTerm:  a.raftStatus.Term(),
+		ClusterId: uint64(a.options.Cluster.ID()),
+		MemberId:  uint64(a.options.RaftStatus.MemberID()),
+		Revision:  a.options.KV.Rev(),
+		RaftTerm:  a.options.RaftStatus.Term(),
 	}
 }
diff --git a/server/etcdserver/apply/apply_auth.go b/server/etcdserver/apply/apply_auth.go
index 3922deebd..d8fd2bca8 100644
--- a/server/etcdserver/apply/apply_auth.go
+++ b/server/etcdserver/apply/apply_auth.go
@@ -20,6 +20,7 @@ import (
 	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
 	"go.etcd.io/etcd/pkg/v3/traceutil"
 	"go.etcd.io/etcd/server/v3/auth"
+	"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
 	"go.etcd.io/etcd/server/v3/etcdserver/txn"
 	"go.etcd.io/etcd/server/v3/lease"
 )
@@ -40,7 +41,7 @@ func newAuthApplierV3(as auth.AuthStore, base applierV3, lessor lease.Lessor) *a
 	return &authApplierV3{applierV3: base, as: as, lessor: lessor}
 }
 
-func (aa *authApplierV3) Apply(r *pb.InternalRaftRequest, applyFunc applyFunc) *Result {
+func (aa *authApplierV3) Apply(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3, applyFunc applyFunc) *Result {
 	aa.mu.Lock()
 	defer aa.mu.Unlock()
 	if r.Header != nil {
@@ -56,7 +57,7 @@ func (aa *authApplierV3) Apply(r *pb.InternalRaftRequest, applyFunc applyFunc) *
 			return &Result{Err: err}
 		}
 	}
-	ret := aa.applierV3.Apply(r, applyFunc)
+	ret := aa.applierV3.Apply(r, shouldApplyV3, applyFunc)
 	aa.authInfo.Username = ""
 	aa.authInfo.Revision = 0
 	return ret
diff --git a/server/etcdserver/apply/uber_applier.go b/server/etcdserver/apply/uber_applier.go
index ec7e2aae6..2da2019d1 100644
--- a/server/etcdserver/apply/uber_applier.go
+++ b/server/etcdserver/apply/uber_applier.go
@@ -21,18 +21,14 @@ import (
 	"go.uber.org/zap"
 
 	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
-	"go.etcd.io/etcd/server/v3/auth"
 	"go.etcd.io/etcd/server/v3/etcdserver/api/membership"
 	"go.etcd.io/etcd/server/v3/etcdserver/api/v3alarm"
-	"go.etcd.io/etcd/server/v3/etcdserver/cindex"
 	"go.etcd.io/etcd/server/v3/etcdserver/txn"
-	"go.etcd.io/etcd/server/v3/lease"
-	"go.etcd.io/etcd/server/v3/storage/backend"
 	"go.etcd.io/etcd/server/v3/storage/mvcc"
 )
 
 type UberApplier interface {
-	Apply(r *pb.InternalRaftRequest) *Result
+	Apply(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3) *Result
 }
 
 type uberApplier struct {
@@ -48,27 +44,13 @@ type uberApplier struct {
 	applyV3base applierV3
 }
 
-func NewUberApplier(
-	lg *zap.Logger,
-	be backend.Backend,
-	kv mvcc.KV,
-	alarmStore *v3alarm.AlarmStore,
-	authStore auth.AuthStore,
-	lessor lease.Lessor,
-	cluster *membership.RaftCluster,
-	raftStatus RaftStatusGetter,
-	snapshotServer SnapshotServer,
-	consistentIndex cindex.ConsistentIndexer,
-	warningApplyDuration time.Duration,
-	txnModeWriteWithSharedBuffer bool,
-	quotaBackendBytesCfg int64,
-) UberApplier {
-	applyV3base := newApplierV3(lg, be, kv, alarmStore, authStore, lessor, cluster, raftStatus, snapshotServer, consistentIndex, txnModeWriteWithSharedBuffer, quotaBackendBytesCfg)
+func NewUberApplier(opts ApplierOptions) UberApplier {
+	applyV3base := newApplierV3(opts)
 
 	ua := &uberApplier{
-		lg:                   lg,
-		alarmStore:           alarmStore,
-		warningApplyDuration: warningApplyDuration,
+		lg:                   opts.Logger,
+		alarmStore:           opts.AlarmStore,
+		warningApplyDuration: opts.WarningApplyDuration,
 		applyV3:              applyV3base,
 		applyV3base:          applyV3base,
 	}
@@ -76,25 +58,12 @@ func NewUberApplier(
 	return ua
 }
 
-func newApplierV3(
-	lg *zap.Logger,
-	be backend.Backend,
-	kv mvcc.KV,
-	alarmStore *v3alarm.AlarmStore,
-	authStore auth.AuthStore,
-	lessor lease.Lessor,
-	cluster *membership.RaftCluster,
-	raftStatus RaftStatusGetter,
-	snapshotServer SnapshotServer,
-	consistentIndex cindex.ConsistentIndexer,
-	txnModeWriteWithSharedBuffer bool,
-	quotaBackendBytesCfg int64,
-) applierV3 {
-	applierBackend := newApplierV3Backend(lg, kv, alarmStore, authStore, lessor, cluster, raftStatus, snapshotServer, consistentIndex, txnModeWriteWithSharedBuffer)
+func newApplierV3(opts ApplierOptions) applierV3 {
+	applierBackend := newApplierV3Backend(opts)
 	return newAuthApplierV3(
-		authStore,
-		newQuotaApplierV3(lg, quotaBackendBytesCfg, be, applierBackend),
-		lessor,
+		opts.AuthStore,
+		newQuotaApplierV3(opts.Logger, opts.QuotaBackendBytesCfg, opts.Backend, applierBackend),
+		opts.Lessor,
 	)
 }
 
@@ -110,18 +79,18 @@ func (a *uberApplier) restoreAlarms() {
 	}
 }
 
-func (a *uberApplier) Apply(r *pb.InternalRaftRequest) *Result {
+func (a *uberApplier) Apply(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3) *Result {
 	// We first execute chain of Apply() calls down the hierarchy:
 	// (i.e. CorruptApplier -> CappedApplier -> Auth -> Quota -> Backend),
 	// then dispatch() unpacks the request to a specific method (like Put),
 	// that gets executed down the hierarchy again:
 	// i.e. CorruptApplier.Put(CappedApplier.Put(...(BackendApplier.Put(...)))).
-	return a.applyV3.Apply(r, a.dispatch)
+	return a.applyV3.Apply(r, shouldApplyV3, a.dispatch)
 }
 
 // dispatch translates the request (r) into appropriate call (like Put) on
 // the underlying applyV3 object.
-func (a *uberApplier) dispatch(r *pb.InternalRaftRequest) *Result {
+func (a *uberApplier) dispatch(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3) *Result {
 	op := "unknown"
 	ar := &Result{}
 	defer func(start time.Time) {
@@ -133,6 +102,31 @@ func (a *uberApplier) dispatch(r *pb.InternalRaftRequest) *Result {
 		}
 	}(time.Now())
 
+	switch {
+	case r.ClusterVersionSet != nil:
+		op = "ClusterVersionSet" // Implemented in 3.5.x
+		a.applyV3.ClusterVersionSet(r.ClusterVersionSet, shouldApplyV3)
+		return ar
+	case r.ClusterMemberAttrSet != nil:
+		op = "ClusterMemberAttrSet" // Implemented in 3.5.x
+		a.applyV3.ClusterMemberAttrSet(r.ClusterMemberAttrSet, shouldApplyV3)
+		return ar
+	case r.DowngradeInfoSet != nil:
+		op = "DowngradeInfoSet" // Implemented in 3.5.x
+		a.applyV3.DowngradeInfoSet(r.DowngradeInfoSet, shouldApplyV3)
+		return ar
+	case r.DowngradeVersionTest != nil:
+		op = "DowngradeVersionTest" // Implemented in 3.6 for test only
+		// do nothing, we are just to ensure etcdserver don't panic in case
+		// users(test cases) intentionally inject DowngradeVersionTestRequest
+		// into the WAL files.
+		return ar
+	default:
+	}
+	if !shouldApplyV3 {
+		return nil
+	}
+
 	switch {
 	case r.Range != nil:
 		op = "Range"
diff --git a/server/etcdserver/server.go b/server/etcdserver/server.go
index 008d23b55..452100d7f 100644
--- a/server/etcdserver/server.go
+++ b/server/etcdserver/server.go
@@ -62,7 +62,6 @@ import (
 	"go.etcd.io/etcd/server/v3/etcdserver/apply"
 	"go.etcd.io/etcd/server/v3/etcdserver/cindex"
 	"go.etcd.io/etcd/server/v3/etcdserver/errors"
-	"go.etcd.io/etcd/server/v3/etcdserver/txn"
 	serverversion "go.etcd.io/etcd/server/v3/etcdserver/version"
 	"go.etcd.io/etcd/server/v3/features"
 	"go.etcd.io/etcd/server/v3/lease"
@@ -1149,8 +1148,22 @@ func (s *EtcdServer) applySnapshot(ep *etcdProgress, toApply *toApply) {
 }
 
 func (s *EtcdServer) NewUberApplier() apply.UberApplier {
-	return apply.NewUberApplier(s.lg, s.be, s.KV(), s.alarmStore, s.authStore, s.lessor, s.cluster, s, s, s.consistIndex,
-		s.Cfg.WarningApplyDuration, s.Cfg.ServerFeatureGate.Enabled(features.TxnModeWriteWithSharedBuffer), s.Cfg.QuotaBackendBytes)
+	opts := apply.ApplierOptions{
+		Logger:                       s.lg,
+		KV:                           s.KV(),
+		AlarmStore:                   s.alarmStore,
+		AuthStore:                    s.authStore,
+		Lessor:                       s.lessor,
+		Cluster:                      s.cluster,
+		RaftStatus:                   s,
+		SnapshotServer:               s,
+		ConsistentIndex:              s.consistIndex,
+		TxnModeWriteWithSharedBuffer: s.Cfg.ServerFeatureGate.Enabled(features.TxnModeWriteWithSharedBuffer),
+		Backend:                      s.be,
+		QuotaBackendBytesCfg:         s.Cfg.QuotaBackendBytes,
+		WarningApplyDuration:         s.Cfg.WarningApplyDuration,
+	}
+	return apply.NewUberApplier(opts)
 }
 
 func verifySnapshotIndex(snapshot raftpb.Snapshot, cindex uint64) {
@@ -1971,7 +1984,7 @@ func (s *EtcdServer) applyEntryNormal(e *raftpb.Entry, shouldApplyV3 membership.
 		if !needResult && raftReq.Txn != nil {
 			removeNeedlessRangeReqs(raftReq.Txn)
 		}
-		ar = s.applyInternalRaftRequest(&raftReq, shouldApplyV3)
+		ar = s.uberApply.Apply(&raftReq, shouldApplyV3)
 	}
 
 	// do not re-toApply applied entries.
@@ -2007,42 +2020,6 @@ func (s *EtcdServer) applyEntryNormal(e *raftpb.Entry, shouldApplyV3 membership.
 	})
 }
 
-func (s *EtcdServer) applyInternalRaftRequest(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3) *apply.Result {
-	if r.ClusterVersionSet == nil && r.ClusterMemberAttrSet == nil && r.DowngradeInfoSet == nil && r.DowngradeVersionTest == nil {
-		if !shouldApplyV3 {
-			return nil
-		}
-		return s.uberApply.Apply(r)
-	}
-	membershipApplier := apply.NewApplierMembership(s.lg, s.cluster, s)
-	op := "unknown"
-	defer func(start time.Time) {
-		txn.ApplySecObserve("v3", op, true, time.Since(start))
-		txn.WarnOfExpensiveRequest(s.lg, s.Cfg.WarningApplyDuration, start, &pb.InternalRaftStringer{Request: r}, nil, nil)
-	}(time.Now())
-	switch {
-	case r.ClusterVersionSet != nil:
-		op = "ClusterVersionSet" // Implemented in 3.5.x
-		membershipApplier.ClusterVersionSet(r.ClusterVersionSet, shouldApplyV3)
-		return &apply.Result{}
-	case r.ClusterMemberAttrSet != nil:
-		op = "ClusterMemberAttrSet" // Implemented in 3.5.x
-		membershipApplier.ClusterMemberAttrSet(r.ClusterMemberAttrSet, shouldApplyV3)
-	case r.DowngradeInfoSet != nil:
-		op = "DowngradeInfoSet" // Implemented in 3.5.x
-		membershipApplier.DowngradeInfoSet(r.DowngradeInfoSet, shouldApplyV3)
-	case r.DowngradeVersionTest != nil:
-		op = "DowngradeVersionTest" // Implemented in 3.6 for test only
-		// do nothing, we are just to ensure etcdserver don't panic in case
-		// users(test cases) intentionally inject DowngradeVersionTestRequest
-		// into the WAL files.
-	default:
-		s.lg.Panic("not implemented apply", zap.Stringer("raft-request", r))
-		return nil
-	}
-	return &apply.Result{}
-}
-
 func noSideEffect(r *pb.InternalRaftRequest) bool {
 	return r.Range != nil || r.AuthUserGet != nil || r.AuthRoleGet != nil || r.AuthStatus != nil
 }
@@ -2069,7 +2046,7 @@ func removeNeedlessRangeReqs(txn *pb.TxnRequest) {
 // invoked with a ConfChange that has already passed through Raft
 func (s *EtcdServer) applyConfChange(cc raftpb.ConfChange, confState *raftpb.ConfState, shouldApplyV3 membership.ShouldApplyV3) (bool, error) {
 	lg := s.Logger()
-	if err := s.cluster.ValidateConfigurationChange(cc); err != nil {
+	if err := s.cluster.ValidateConfigurationChange(cc, shouldApplyV3); err != nil {
 		lg.Error("Validation on configuration change failed", zap.Bool("shouldApplyV3", bool(shouldApplyV3)), zap.Error(err))
 		cc.NodeID = raft.None
 		s.r.ApplyConfChange(cc)
diff --git a/server/etcdserver/txn/delete.go b/server/etcdserver/txn/delete.go
new file mode 100644
index 000000000..7d1218e8c
--- /dev/null
+++ b/server/etcdserver/txn/delete.go
@@ -0,0 +1,70 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package txn
+
+import (
+	"context"
+
+	"go.uber.org/zap"
+
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	"go.etcd.io/etcd/pkg/v3/traceutil"
+	"go.etcd.io/etcd/server/v3/storage/mvcc"
+)
+
+func DeleteRange(ctx context.Context, lg *zap.Logger, kv mvcc.KV, dr *pb.DeleteRangeRequest) (resp *pb.DeleteRangeResponse, trace *traceutil.Trace, err error) {
+	ctx, trace = ensureTrace(ctx, lg, "delete_range",
+		traceutil.Field{Key: "key", Value: string(dr.Key)},
+		traceutil.Field{Key: "range_end", Value: string(dr.RangeEnd)},
+	)
+	txnWrite := kv.Write(trace)
+	defer txnWrite.End()
+	resp, err = deleteRange(ctx, txnWrite, dr)
+	return resp, trace, err
+}
+
+func deleteRange(ctx context.Context, txnWrite mvcc.TxnWrite, dr *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) {
+	resp := &pb.DeleteRangeResponse{}
+	resp.Header = &pb.ResponseHeader{}
+	end := mkGteRange(dr.RangeEnd)
+
+	if dr.PrevKv {
+		rr, err := txnWrite.Range(ctx, dr.Key, end, mvcc.RangeOptions{})
+		if err != nil {
+			return nil, err
+		}
+		if rr != nil {
+			resp.PrevKvs = make([]*mvccpb.KeyValue, len(rr.KVs))
+			for i := range rr.KVs {
+				resp.PrevKvs[i] = &rr.KVs[i]
+			}
+		}
+	}
+
+	resp.Deleted, resp.Header.Revision = txnWrite.DeleteRange(dr.Key, end)
+	return resp, nil
+}
+
+// mkGteRange determines if the range end is a >= range. This works around grpc
+// sending empty byte strings as nil; >= is encoded in the range end as '\0'.
+// If it is a GTE range, then []byte{} is returned to indicate the empty byte
+// string (vs nil being no byte string).
+func mkGteRange(rangeEnd []byte) []byte {
+	if len(rangeEnd) == 1 && rangeEnd[0] == 0 {
+		return []byte{}
+	}
+	return rangeEnd
+}
diff --git a/server/etcdserver/txn/put.go b/server/etcdserver/txn/put.go
new file mode 100644
index 000000000..8c304cc59
--- /dev/null
+++ b/server/etcdserver/txn/put.go
@@ -0,0 +1,114 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package txn
+
+import (
+	"context"
+
+	"go.uber.org/zap"
+
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
+	"go.etcd.io/etcd/pkg/v3/traceutil"
+	"go.etcd.io/etcd/server/v3/etcdserver/errors"
+	"go.etcd.io/etcd/server/v3/lease"
+	"go.etcd.io/etcd/server/v3/storage/mvcc"
+)
+
+func Put(ctx context.Context, lg *zap.Logger, lessor lease.Lessor, kv mvcc.KV, p *pb.PutRequest) (resp *pb.PutResponse, trace *traceutil.Trace, err error) {
+	ctx, trace = ensureTrace(ctx, lg, "put",
+		traceutil.Field{Key: "key", Value: string(p.Key)},
+		traceutil.Field{Key: "req_size", Value: p.Size()},
+	)
+	err = checkLease(lessor, p)
+	if err != nil {
+		return nil, trace, err
+	}
+	txnWrite := kv.Write(trace)
+	defer txnWrite.End()
+	prevKV, err := checkAndGetPrevKV(trace, txnWrite, p)
+	if err != nil {
+		return nil, trace, err
+	}
+	return put(ctx, txnWrite, p, prevKV), trace, nil
+}
+
+func put(ctx context.Context, txnWrite mvcc.TxnWrite, p *pb.PutRequest, prevKV *mvcc.RangeResult) *pb.PutResponse {
+	trace := traceutil.Get(ctx)
+	resp := &pb.PutResponse{}
+	resp.Header = &pb.ResponseHeader{}
+	val, leaseID := p.Value, lease.LeaseID(p.Lease)
+
+	if p.IgnoreValue {
+		val = prevKV.KVs[0].Value
+	}
+	if p.IgnoreLease {
+		leaseID = lease.LeaseID(prevKV.KVs[0].Lease)
+	}
+	if p.PrevKv {
+		if prevKV != nil && len(prevKV.KVs) != 0 {
+			resp.PrevKv = &prevKV.KVs[0]
+		}
+	}
+
+	resp.Header.Revision = txnWrite.Put(p.Key, val, leaseID)
+	trace.AddField(traceutil.Field{Key: "response_revision", Value: resp.Header.Revision})
+	return resp
+}
+
+func checkPut(trace *traceutil.Trace, txnWrite mvcc.ReadView, lessor lease.Lessor, p *pb.PutRequest) error {
+	err := checkLease(lessor, p)
+	if err != nil {
+		return err
+	}
+	_, err = checkAndGetPrevKV(trace, txnWrite, p)
+	return err
+}
+
+func checkLease(lessor lease.Lessor, p *pb.PutRequest) error {
+	leaseID := lease.LeaseID(p.Lease)
+	if leaseID != lease.NoLease {
+		if l := lessor.Lookup(leaseID); l == nil {
+			return lease.ErrLeaseNotFound
+		}
+	}
+	return nil
+}
+
+func checkAndGetPrevKV(trace *traceutil.Trace, txnWrite mvcc.ReadView, p *pb.PutRequest) (prevKV *mvcc.RangeResult, err error) {
+	prevKV, err = getPrevKV(trace, txnWrite, p)
+	if err != nil {
+		return nil, err
+	}
+	if p.IgnoreValue || p.IgnoreLease {
+		if prevKV == nil || len(prevKV.KVs) == 0 {
+			// ignore_{lease,value} flag expects previous key-value pair
+			return nil, errors.ErrKeyNotFound
+		}
+	}
+	return prevKV, nil
+}
+
+func getPrevKV(trace *traceutil.Trace, txnWrite mvcc.ReadView, p *pb.PutRequest) (prevKV *mvcc.RangeResult, err error) {
+	if p.IgnoreValue || p.IgnoreLease || p.PrevKv {
+		trace.StepWithFunction(func() {
+			prevKV, err = txnWrite.Range(context.TODO(), p.Key, nil, mvcc.RangeOptions{})
+		}, "get previous kv pair")
+
+		if err != nil {
+			return nil, err
+		}
+	}
+	return prevKV, nil
+}
diff --git a/server/etcdserver/txn/range.go b/server/etcdserver/txn/range.go
new file mode 100644
index 000000000..8e9fef34a
--- /dev/null
+++ b/server/etcdserver/txn/range.go
@@ -0,0 +1,203 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package txn
+
+import (
+	"bytes"
+	"context"
+	"sort"
+	"time"
+
+	"go.uber.org/zap"
+
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	"go.etcd.io/etcd/pkg/v3/traceutil"
+	"go.etcd.io/etcd/server/v3/storage/mvcc"
+)
+
+func Range(ctx context.Context, lg *zap.Logger, kv mvcc.KV, r *pb.RangeRequest) (resp *pb.RangeResponse, trace *traceutil.Trace, err error) {
+	ctx, trace = ensureTrace(ctx, lg, "range")
+	defer func(start time.Time) {
+		success := err == nil
+		RangeSecObserve(success, time.Since(start))
+	}(time.Now())
+	txnRead := kv.Read(mvcc.ConcurrentReadTxMode, trace)
+	defer txnRead.End()
+	resp, err = executeRange(ctx, lg, txnRead, r)
+	return resp, trace, err
+}
+
+func executeRange(ctx context.Context, lg *zap.Logger, txnRead mvcc.TxnRead, r *pb.RangeRequest) (*pb.RangeResponse, error) {
+	trace := traceutil.Get(ctx)
+
+	resp := &pb.RangeResponse{}
+	resp.Header = &pb.ResponseHeader{}
+
+	limit := r.Limit
+	if r.SortOrder != pb.RangeRequest_NONE ||
+		r.MinModRevision != 0 || r.MaxModRevision != 0 ||
+		r.MinCreateRevision != 0 || r.MaxCreateRevision != 0 {
+		// fetch everything; sort and truncate afterwards
+		limit = 0
+	}
+	if limit > 0 {
+		// fetch one extra for 'more' flag
+		limit = limit + 1
+	}
+
+	ro := mvcc.RangeOptions{
+		Limit: limit,
+		Rev:   r.Revision,
+		Count: r.CountOnly,
+	}
+
+	rr, err := txnRead.Range(ctx, r.Key, mkGteRange(r.RangeEnd), ro)
+	if err != nil {
+		return nil, err
+	}
+
+	if r.MaxModRevision != 0 {
+		f := func(kv *mvccpb.KeyValue) bool { return kv.ModRevision > r.MaxModRevision }
+		pruneKVs(rr, f)
+	}
+	if r.MinModRevision != 0 {
+		f := func(kv *mvccpb.KeyValue) bool { return kv.ModRevision < r.MinModRevision }
+		pruneKVs(rr, f)
+	}
+	if r.MaxCreateRevision != 0 {
+		f := func(kv *mvccpb.KeyValue) bool { return kv.CreateRevision > r.MaxCreateRevision }
+		pruneKVs(rr, f)
+	}
+	if r.MinCreateRevision != 0 {
+		f := func(kv *mvccpb.KeyValue) bool { return kv.CreateRevision < r.MinCreateRevision }
+		pruneKVs(rr, f)
+	}
+
+	sortOrder := r.SortOrder
+	if r.SortTarget != pb.RangeRequest_KEY && sortOrder == pb.RangeRequest_NONE {
+		// Since current mvcc.Range implementation returns results
+		// sorted by keys in lexiographically ascending order,
+		// sort ASCEND by default only when target is not 'KEY'
+		sortOrder = pb.RangeRequest_ASCEND
+	} else if r.SortTarget == pb.RangeRequest_KEY && sortOrder == pb.RangeRequest_ASCEND {
+		// Since current mvcc.Range implementation returns results
+		// sorted by keys in lexiographically ascending order,
+		// don't re-sort when target is 'KEY' and order is ASCEND
+		sortOrder = pb.RangeRequest_NONE
+	}
+	if sortOrder != pb.RangeRequest_NONE {
+		var sorter sort.Interface
+		switch {
+		case r.SortTarget == pb.RangeRequest_KEY:
+			sorter = &kvSortByKey{&kvSort{rr.KVs}}
+		case r.SortTarget == pb.RangeRequest_VERSION:
+			sorter = &kvSortByVersion{&kvSort{rr.KVs}}
+		case r.SortTarget == pb.RangeRequest_CREATE:
+			sorter = &kvSortByCreate{&kvSort{rr.KVs}}
+		case r.SortTarget == pb.RangeRequest_MOD:
+			sorter = &kvSortByMod{&kvSort{rr.KVs}}
+		case r.SortTarget == pb.RangeRequest_VALUE:
+			sorter = &kvSortByValue{&kvSort{rr.KVs}}
+		default:
+			lg.Panic("unexpected sort target", zap.Int32("sort-target", int32(r.SortTarget)))
+		}
+		switch {
+		case sortOrder == pb.RangeRequest_ASCEND:
+			sort.Sort(sorter)
+		case sortOrder == pb.RangeRequest_DESCEND:
+			sort.Sort(sort.Reverse(sorter))
+		}
+	}
+
+	if r.Limit > 0 && len(rr.KVs) > int(r.Limit) {
+		rr.KVs = rr.KVs[:r.Limit]
+		resp.More = true
+	}
+	trace.Step("filter and sort the key-value pairs")
+	resp.Header.Revision = rr.Rev
+	resp.Count = int64(rr.Count)
+	resp.Kvs = make([]*mvccpb.KeyValue, len(rr.KVs))
+	for i := range rr.KVs {
+		if r.KeysOnly {
+			rr.KVs[i].Value = nil
+		}
+		resp.Kvs[i] = &rr.KVs[i]
+	}
+	trace.Step("assemble the response")
+	return resp, nil
+}
+
+func checkRange(rv mvcc.ReadView, req *pb.RangeRequest) error {
+	switch {
+	case req.Revision == 0:
+		return nil
+	case req.Revision > rv.Rev():
+		return mvcc.ErrFutureRev
+	case req.Revision < rv.FirstRev():
+		return mvcc.ErrCompacted
+	}
+	return nil
+}
+
+func pruneKVs(rr *mvcc.RangeResult, isPrunable func(*mvccpb.KeyValue) bool) {
+	j := 0
+	for i := range rr.KVs {
+		rr.KVs[j] = rr.KVs[i]
+		if !isPrunable(&rr.KVs[i]) {
+			j++
+		}
+	}
+	rr.KVs = rr.KVs[:j]
+}
+
+type kvSort struct{ kvs []mvccpb.KeyValue }
+
+func (s *kvSort) Swap(i, j int) {
+	t := s.kvs[i]
+	s.kvs[i] = s.kvs[j]
+	s.kvs[j] = t
+}
+func (s *kvSort) Len() int { return len(s.kvs) }
+
+type kvSortByKey struct{ *kvSort }
+
+func (s *kvSortByKey) Less(i, j int) bool {
+	return bytes.Compare(s.kvs[i].Key, s.kvs[j].Key) < 0
+}
+
+type kvSortByVersion struct{ *kvSort }
+
+func (s *kvSortByVersion) Less(i, j int) bool {
+	return (s.kvs[i].Version - s.kvs[j].Version) < 0
+}
+
+type kvSortByCreate struct{ *kvSort }
+
+func (s *kvSortByCreate) Less(i, j int) bool {
+	return (s.kvs[i].CreateRevision - s.kvs[j].CreateRevision) < 0
+}
+
+type kvSortByMod struct{ *kvSort }
+
+func (s *kvSortByMod) Less(i, j int) bool {
+	return (s.kvs[i].ModRevision - s.kvs[j].ModRevision) < 0
+}
+
+type kvSortByValue struct{ *kvSort }
+
+func (s *kvSortByValue) Less(i, j int) bool {
+	return bytes.Compare(s.kvs[i].Value, s.kvs[j].Value) < 0
+}
diff --git a/server/etcdserver/txn/txn.go b/server/etcdserver/txn/txn.go
index 51f70a06a..b0d568d28 100644
--- a/server/etcdserver/txn/txn.go
+++ b/server/etcdserver/txn/txn.go
@@ -18,8 +18,6 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"sort"
-	"time"
 
 	"go.uber.org/zap"
 
@@ -27,234 +25,12 @@ import (
 	"go.etcd.io/etcd/api/v3/mvccpb"
 	"go.etcd.io/etcd/pkg/v3/traceutil"
 	"go.etcd.io/etcd/server/v3/auth"
-	"go.etcd.io/etcd/server/v3/etcdserver/errors"
 	"go.etcd.io/etcd/server/v3/lease"
 	"go.etcd.io/etcd/server/v3/storage/mvcc"
 )
 
-func Put(ctx context.Context, lg *zap.Logger, lessor lease.Lessor, kv mvcc.KV, p *pb.PutRequest) (resp *pb.PutResponse, trace *traceutil.Trace, err error) {
-	trace = traceutil.Get(ctx)
-	// create put tracing if the trace in context is empty
-	if trace.IsEmpty() {
-		trace = traceutil.New("put",
-			lg,
-			traceutil.Field{Key: "key", Value: string(p.Key)},
-			traceutil.Field{Key: "req_size", Value: p.Size()},
-		)
-		ctx = context.WithValue(ctx, traceutil.TraceKey{}, trace)
-	}
-	leaseID := lease.LeaseID(p.Lease)
-	if leaseID != lease.NoLease {
-		if l := lessor.Lookup(leaseID); l == nil {
-			return nil, nil, lease.ErrLeaseNotFound
-		}
-	}
-	txnWrite := kv.Write(trace)
-	defer txnWrite.End()
-	resp, err = put(ctx, txnWrite, p)
-	return resp, trace, err
-}
-
-func put(ctx context.Context, txnWrite mvcc.TxnWrite, p *pb.PutRequest) (resp *pb.PutResponse, err error) {
-	trace := traceutil.Get(ctx)
-	resp = &pb.PutResponse{}
-	resp.Header = &pb.ResponseHeader{}
-	val, leaseID := p.Value, lease.LeaseID(p.Lease)
-
-	var rr *mvcc.RangeResult
-	if p.IgnoreValue || p.IgnoreLease || p.PrevKv {
-		trace.StepWithFunction(func() {
-			rr, err = txnWrite.Range(context.TODO(), p.Key, nil, mvcc.RangeOptions{})
-		}, "get previous kv pair")
-
-		if err != nil {
-			return nil, err
-		}
-	}
-	if p.IgnoreValue || p.IgnoreLease {
-		if rr == nil || len(rr.KVs) == 0 {
-			// ignore_{lease,value} flag expects previous key-value pair
-			return nil, errors.ErrKeyNotFound
-		}
-	}
-	if p.IgnoreValue {
-		val = rr.KVs[0].Value
-	}
-	if p.IgnoreLease {
-		leaseID = lease.LeaseID(rr.KVs[0].Lease)
-	}
-	if p.PrevKv {
-		if rr != nil && len(rr.KVs) != 0 {
-			resp.PrevKv = &rr.KVs[0]
-		}
-	}
-
-	resp.Header.Revision = txnWrite.Put(p.Key, val, leaseID)
-	trace.AddField(traceutil.Field{Key: "response_revision", Value: resp.Header.Revision})
-	return resp, nil
-}
-
-func DeleteRange(ctx context.Context, lg *zap.Logger, kv mvcc.KV, dr *pb.DeleteRangeRequest) (resp *pb.DeleteRangeResponse, trace *traceutil.Trace, err error) {
-	trace = traceutil.Get(ctx)
-	// create delete tracing if the trace in context is empty
-	if trace.IsEmpty() {
-		trace = traceutil.New("delete_range",
-			lg,
-			traceutil.Field{Key: "key", Value: string(dr.Key)},
-			traceutil.Field{Key: "range_end", Value: string(dr.RangeEnd)},
-		)
-		ctx = context.WithValue(ctx, traceutil.TraceKey{}, trace)
-	}
-	txnWrite := kv.Write(trace)
-	defer txnWrite.End()
-	resp, err = deleteRange(ctx, txnWrite, dr)
-	return resp, trace, err
-}
-
-func deleteRange(ctx context.Context, txnWrite mvcc.TxnWrite, dr *pb.DeleteRangeRequest) (*pb.DeleteRangeResponse, error) {
-	resp := &pb.DeleteRangeResponse{}
-	resp.Header = &pb.ResponseHeader{}
-	end := mkGteRange(dr.RangeEnd)
-
-	if dr.PrevKv {
-		rr, err := txnWrite.Range(ctx, dr.Key, end, mvcc.RangeOptions{})
-		if err != nil {
-			return nil, err
-		}
-		if rr != nil {
-			resp.PrevKvs = make([]*mvccpb.KeyValue, len(rr.KVs))
-			for i := range rr.KVs {
-				resp.PrevKvs[i] = &rr.KVs[i]
-			}
-		}
-	}
-
-	resp.Deleted, resp.Header.Revision = txnWrite.DeleteRange(dr.Key, end)
-	return resp, nil
-}
-
-func Range(ctx context.Context, lg *zap.Logger, kv mvcc.KV, r *pb.RangeRequest) (resp *pb.RangeResponse, trace *traceutil.Trace, err error) {
-	trace = traceutil.Get(ctx)
-	if trace.IsEmpty() {
-		trace = traceutil.New("range", lg)
-		ctx = context.WithValue(ctx, traceutil.TraceKey{}, trace)
-	}
-	defer func(start time.Time) {
-		success := err == nil
-		RangeSecObserve(success, time.Since(start))
-	}(time.Now())
-	txnRead := kv.Read(mvcc.ConcurrentReadTxMode, trace)
-	defer txnRead.End()
-	resp, err = executeRange(ctx, lg, txnRead, r)
-	return resp, trace, err
-}
-
-func executeRange(ctx context.Context, lg *zap.Logger, txnRead mvcc.TxnRead, r *pb.RangeRequest) (*pb.RangeResponse, error) {
-	trace := traceutil.Get(ctx)
-
-	resp := &pb.RangeResponse{}
-	resp.Header = &pb.ResponseHeader{}
-
-	limit := r.Limit
-	if r.SortOrder != pb.RangeRequest_NONE ||
-		r.MinModRevision != 0 || r.MaxModRevision != 0 ||
-		r.MinCreateRevision != 0 || r.MaxCreateRevision != 0 {
-		// fetch everything; sort and truncate afterwards
-		limit = 0
-	}
-	if limit > 0 {
-		// fetch one extra for 'more' flag
-		limit = limit + 1
-	}
-
-	ro := mvcc.RangeOptions{
-		Limit: limit,
-		Rev:   r.Revision,
-		Count: r.CountOnly,
-	}
-
-	rr, err := txnRead.Range(ctx, r.Key, mkGteRange(r.RangeEnd), ro)
-	if err != nil {
-		return nil, err
-	}
-
-	if r.MaxModRevision != 0 {
-		f := func(kv *mvccpb.KeyValue) bool { return kv.ModRevision > r.MaxModRevision }
-		pruneKVs(rr, f)
-	}
-	if r.MinModRevision != 0 {
-		f := func(kv *mvccpb.KeyValue) bool { return kv.ModRevision < r.MinModRevision }
-		pruneKVs(rr, f)
-	}
-	if r.MaxCreateRevision != 0 {
-		f := func(kv *mvccpb.KeyValue) bool { return kv.CreateRevision > r.MaxCreateRevision }
-		pruneKVs(rr, f)
-	}
-	if r.MinCreateRevision != 0 {
-		f := func(kv *mvccpb.KeyValue) bool { return kv.CreateRevision < r.MinCreateRevision }
-		pruneKVs(rr, f)
-	}
-
-	sortOrder := r.SortOrder
-	if r.SortTarget != pb.RangeRequest_KEY && sortOrder == pb.RangeRequest_NONE {
-		// Since current mvcc.Range implementation returns results
-		// sorted by keys in lexiographically ascending order,
-		// sort ASCEND by default only when target is not 'KEY'
-		sortOrder = pb.RangeRequest_ASCEND
-	} else if r.SortTarget == pb.RangeRequest_KEY && sortOrder == pb.RangeRequest_ASCEND {
-		// Since current mvcc.Range implementation returns results
-		// sorted by keys in lexiographically ascending order,
-		// don't re-sort when target is 'KEY' and order is ASCEND
-		sortOrder = pb.RangeRequest_NONE
-	}
-	if sortOrder != pb.RangeRequest_NONE {
-		var sorter sort.Interface
-		switch {
-		case r.SortTarget == pb.RangeRequest_KEY:
-			sorter = &kvSortByKey{&kvSort{rr.KVs}}
-		case r.SortTarget == pb.RangeRequest_VERSION:
-			sorter = &kvSortByVersion{&kvSort{rr.KVs}}
-		case r.SortTarget == pb.RangeRequest_CREATE:
-			sorter = &kvSortByCreate{&kvSort{rr.KVs}}
-		case r.SortTarget == pb.RangeRequest_MOD:
-			sorter = &kvSortByMod{&kvSort{rr.KVs}}
-		case r.SortTarget == pb.RangeRequest_VALUE:
-			sorter = &kvSortByValue{&kvSort{rr.KVs}}
-		default:
-			lg.Panic("unexpected sort target", zap.Int32("sort-target", int32(r.SortTarget)))
-		}
-		switch {
-		case sortOrder == pb.RangeRequest_ASCEND:
-			sort.Sort(sorter)
-		case sortOrder == pb.RangeRequest_DESCEND:
-			sort.Sort(sort.Reverse(sorter))
-		}
-	}
-
-	if r.Limit > 0 && len(rr.KVs) > int(r.Limit) {
-		rr.KVs = rr.KVs[:r.Limit]
-		resp.More = true
-	}
-	trace.Step("filter and sort the key-value pairs")
-	resp.Header.Revision = rr.Rev
-	resp.Count = int64(rr.Count)
-	resp.Kvs = make([]*mvccpb.KeyValue, len(rr.KVs))
-	for i := range rr.KVs {
-		if r.KeysOnly {
-			rr.KVs[i].Value = nil
-		}
-		resp.Kvs[i] = &rr.KVs[i]
-	}
-	trace.Step("assemble the response")
-	return resp, nil
-}
-
-func Txn(ctx context.Context, lg *zap.Logger, rt *pb.TxnRequest, txnModeWriteWithSharedBuffer bool, kv mvcc.KV, lessor lease.Lessor) (*pb.TxnResponse, *traceutil.Trace, error) {
-	trace := traceutil.Get(ctx)
-	if trace.IsEmpty() {
-		trace = traceutil.New("transaction", lg)
-		ctx = context.WithValue(ctx, traceutil.TraceKey{}, trace)
-	}
+func Txn(ctx context.Context, lg *zap.Logger, rt *pb.TxnRequest, txnModeWriteWithSharedBuffer bool, kv mvcc.KV, lessor lease.Lessor) (txnResp *pb.TxnResponse, trace *traceutil.Trace, err error) {
+	ctx, trace = ensureTrace(ctx, lg, "transaction")
 	isWrite := !IsTxnReadonly(rt)
 	// When the transaction contains write operations, we use ReadTx instead of
 	// ConcurrentReadTx to avoid extra overhead of copying buffer.
@@ -275,7 +51,7 @@ func Txn(ctx context.Context, lg *zap.Logger, rt *pb.TxnRequest, txnModeWriteWit
 	if isWrite {
 		trace.AddField(traceutil.Field{Key: "read_only", Value: false})
 	}
-	_, err := checkTxn(txnRead, rt, lessor, txnPath)
+	_, err = checkTxn(trace, txnRead, rt, lessor, txnPath)
 	if err != nil {
 		txnRead.End()
 		return nil, nil, err
@@ -292,7 +68,7 @@ func Txn(ctx context.Context, lg *zap.Logger, rt *pb.TxnRequest, txnModeWriteWit
 	} else {
 		txnWrite = mvcc.NewReadOnlyTxnWrite(txnRead)
 	}
-	txnResp, err := txn(ctx, lg, txnWrite, rt, isWrite, txnPath)
+	txnResp, err = txn(ctx, lg, txnWrite, rt, isWrite, txnPath)
 	txnWrite.End()
 
 	trace.AddField(
@@ -382,10 +158,11 @@ func executeTxn(ctx context.Context, lg *zap.Logger, txnWrite mvcc.TxnWrite, rt
 				traceutil.Field{Key: "req_type", Value: "put"},
 				traceutil.Field{Key: "key", Value: string(tv.RequestPut.Key)},
 				traceutil.Field{Key: "req_size", Value: tv.RequestPut.Size()})
-			resp, err := put(ctx, txnWrite, tv.RequestPut)
+			prevKV, err := getPrevKV(trace, txnWrite, tv.RequestPut)
 			if err != nil {
-				return 0, fmt.Errorf("applyTxn: failed Put: %w", err)
+				return 0, fmt.Errorf("applyTxn: failed to get prevKV on put: %w", err)
 			}
+			resp := put(ctx, txnWrite, tv.RequestPut, prevKV)
 			respi.(*pb.ResponseOp_ResponsePut).ResponsePut = resp
 			trace.StopSubTrace()
 		case *pb.RequestOp_RequestDeleteRange:
@@ -410,38 +187,7 @@ func executeTxn(ctx context.Context, lg *zap.Logger, txnWrite mvcc.TxnWrite, rt
 	return txns, nil
 }
 
-func checkPut(rv mvcc.ReadView, lessor lease.Lessor, req *pb.PutRequest) error {
-	if req.IgnoreValue || req.IgnoreLease {
-		// expects previous key-value, error if not exist
-		rr, err := rv.Range(context.TODO(), req.Key, nil, mvcc.RangeOptions{})
-		if err != nil {
-			return err
-		}
-		if rr == nil || len(rr.KVs) == 0 {
-			return errors.ErrKeyNotFound
-		}
-	}
-	if lease.LeaseID(req.Lease) != lease.NoLease {
-		if l := lessor.Lookup(lease.LeaseID(req.Lease)); l == nil {
-			return lease.ErrLeaseNotFound
-		}
-	}
-	return nil
-}
-
-func checkRange(rv mvcc.ReadView, req *pb.RangeRequest) error {
-	switch {
-	case req.Revision == 0:
-		return nil
-	case req.Revision > rv.Rev():
-		return mvcc.ErrFutureRev
-	case req.Revision < rv.FirstRev():
-		return mvcc.ErrCompacted
-	}
-	return nil
-}
-
-func checkTxn(rv mvcc.ReadView, rt *pb.TxnRequest, lessor lease.Lessor, txnPath []bool) (int, error) {
+func checkTxn(trace *traceutil.Trace, rv mvcc.ReadView, rt *pb.TxnRequest, lessor lease.Lessor, txnPath []bool) (int, error) {
 	txnCount := 0
 	reqs := rt.Success
 	if !txnPath[0] {
@@ -454,10 +200,10 @@ func checkTxn(rv mvcc.ReadView, rt *pb.TxnRequest, lessor lease.Lessor, txnPath
 		case *pb.RequestOp_RequestRange:
 			err = checkRange(rv, tv.RequestRange)
 		case *pb.RequestOp_RequestPut:
-			err = checkPut(rv, lessor, tv.RequestPut)
+			err = checkPut(trace, rv, lessor, tv.RequestPut)
 		case *pb.RequestOp_RequestDeleteRange:
 		case *pb.RequestOp_RequestTxn:
-			txns, err = checkTxn(rv, tv.RequestTxn, lessor, txnPath[1:])
+			txns, err = checkTxn(trace, rv, tv.RequestTxn, lessor, txnPath[1:])
 			txnCount += txns + 1
 			txnPath = txnPath[txns+1:]
 		default:
@@ -470,67 +216,6 @@ func checkTxn(rv mvcc.ReadView, rt *pb.TxnRequest, lessor lease.Lessor, txnPath
 	return txnCount, nil
 }
 
-// mkGteRange determines if the range end is a >= range. This works around grpc
-// sending empty byte strings as nil; >= is encoded in the range end as '\0'.
-// If it is a GTE range, then []byte{} is returned to indicate the empty byte
-// string (vs nil being no byte string).
-func mkGteRange(rangeEnd []byte) []byte {
-	if len(rangeEnd) == 1 && rangeEnd[0] == 0 {
-		return []byte{}
-	}
-	return rangeEnd
-}
-
-func pruneKVs(rr *mvcc.RangeResult, isPrunable func(*mvccpb.KeyValue) bool) {
-	j := 0
-	for i := range rr.KVs {
-		rr.KVs[j] = rr.KVs[i]
-		if !isPrunable(&rr.KVs[i]) {
-			j++
-		}
-	}
-	rr.KVs = rr.KVs[:j]
-}
-
-type kvSort struct{ kvs []mvccpb.KeyValue }
-
-func (s *kvSort) Swap(i, j int) {
-	t := s.kvs[i]
-	s.kvs[i] = s.kvs[j]
-	s.kvs[j] = t
-}
-func (s *kvSort) Len() int { return len(s.kvs) }
-
-type kvSortByKey struct{ *kvSort }
-
-func (s *kvSortByKey) Less(i, j int) bool {
-	return bytes.Compare(s.kvs[i].Key, s.kvs[j].Key) < 0
-}
-
-type kvSortByVersion struct{ *kvSort }
-
-func (s *kvSortByVersion) Less(i, j int) bool {
-	return (s.kvs[i].Version - s.kvs[j].Version) < 0
-}
-
-type kvSortByCreate struct{ *kvSort }
-
-func (s *kvSortByCreate) Less(i, j int) bool {
-	return (s.kvs[i].CreateRevision - s.kvs[j].CreateRevision) < 0
-}
-
-type kvSortByMod struct{ *kvSort }
-
-func (s *kvSortByMod) Less(i, j int) bool {
-	return (s.kvs[i].ModRevision - s.kvs[j].ModRevision) < 0
-}
-
-type kvSortByValue struct{ *kvSort }
-
-func (s *kvSortByValue) Less(i, j int) bool {
-	return bytes.Compare(s.kvs[i].Value, s.kvs[j].Value) < 0
-}
-
 func compareInt64(a, b int64) int {
 	switch {
 	case a < b:
@@ -721,3 +406,15 @@ func checkTxnReqsPermission(as auth.AuthStore, ai *auth.AuthInfo, reqs []*pb.Req
 
 	return nil
 }
+
+func ensureTrace(ctx context.Context, lg *zap.Logger, operation string, fields ...traceutil.Field) (context.Context, *traceutil.Trace) {
+	trace := traceutil.Get(ctx)
+	if trace.IsEmpty() {
+		trace = traceutil.New(operation,
+			lg,
+			fields...,
+		)
+		ctx = context.WithValue(ctx, traceutil.TraceKey{}, trace)
+	}
+	return ctx, trace
+}
diff --git a/server/features/etcd_features.go b/server/features/etcd_features.go
index 80a2d455c..230b37c70 100644
--- a/server/features/etcd_features.go
+++ b/server/features/etcd_features.go
@@ -35,11 +35,6 @@ const (
 	// of code conflicts because changes are more likely to be scattered
 	// across the file.
 
-	// DistributedTracing enables experimental distributed  tracing using OpenTelemetry Tracing.
-	// owner: @dashpole
-	// alpha: v3.5
-	// issue: https://github.com/etcd-io/etcd/issues/12460
-	DistributedTracing featuregate.Feature = "DistributedTracing"
 	// StopGRPCServiceOnDefrag enables etcd gRPC service to stop serving client requests on defragmentation.
 	// owner: @chaochn47
 	// alpha: v3.6
@@ -83,7 +78,6 @@ const (
 
 var (
 	DefaultEtcdServerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
-		DistributedTracing:           {Default: false, PreRelease: featuregate.Alpha},
 		StopGRPCServiceOnDefrag:      {Default: false, PreRelease: featuregate.Alpha},
 		InitialCorruptCheck:          {Default: false, PreRelease: featuregate.Alpha},
 		CompactHashCheck:             {Default: false, PreRelease: featuregate.Alpha},
diff --git a/server/go.mod b/server/go.mod
index 486f66eea..b30c64634 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -2,15 +2,15 @@ module go.etcd.io/etcd/server/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/coreos/go-systemd/v22 v22.5.0
 	github.com/dustin/go-humanize v1.0.1
 	github.com/gogo/protobuf v1.3.2
-	github.com/golang-jwt/jwt/v5 v5.2.1
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
+	github.com/golang-jwt/jwt/v5 v5.2.2
+	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8
 	github.com/golang/protobuf v1.5.4
 	github.com/google/btree v1.1.3
 	github.com/google/go-cmp v0.7.0
@@ -18,8 +18,8 @@ require (
 	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
 	github.com/jonboulle/clockwork v0.5.0
-	github.com/prometheus/client_golang v1.21.0
-	github.com/prometheus/client_model v0.6.1
+	github.com/prometheus/client_golang v1.22.0
+	github.com/prometheus/client_model v0.6.2
 	github.com/soheilhy/cmux v0.1.5
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
@@ -32,17 +32,17 @@ require (
 	go.etcd.io/etcd/client/v3 v3.6.0-alpha.0
 	go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0
 	go.etcd.io/raft/v3 v3.6.0
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0
-	go.opentelemetry.io/otel v1.34.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0
-	go.opentelemetry.io/otel/sdk v1.34.0
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
+	go.opentelemetry.io/otel v1.35.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
+	go.opentelemetry.io/otel/sdk v1.35.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/crypto v0.35.0
-	golang.org/x/net v0.36.0
-	golang.org/x/time v0.10.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb
-	google.golang.org/grpc v1.70.0
-	google.golang.org/protobuf v1.36.5
+	golang.org/x/crypto v0.37.0
+	golang.org/x/net v0.39.0
+	golang.org/x/time v0.11.0
+	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34
+	google.golang.org/grpc v1.72.0
+	google.golang.org/protobuf v1.36.6
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	sigs.k8s.io/yaml v1.4.0
 )
@@ -51,32 +51,31 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/gorilla/websocket v1.4.2 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/common v0.62.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/prometheus/common v0.63.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
-	go.opentelemetry.io/otel/metric v1.34.0 // indirect
-	go.opentelemetry.io/otel/trace v1.34.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 )
 
 replace (
diff --git a/server/go.sum b/server/go.sum
index e99343d71..346e85e74 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -17,8 +17,9 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8
 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -36,11 +37,11 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 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/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -55,8 +56,8 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
@@ -85,15 +86,16 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
-github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+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/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -127,24 +129,24 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
-go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
-go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
+go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -157,8 +159,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -174,30 +176,30 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -215,19 +217,19 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -239,7 +241,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/server/storage/mvcc/kvstore_compaction.go b/server/storage/mvcc/kvstore_compaction.go
index 5175e0927..9c555fce5 100644
--- a/server/storage/mvcc/kvstore_compaction.go
+++ b/server/storage/mvcc/kvstore_compaction.go
@@ -49,6 +49,7 @@ func (s *store) scheduleCompaction(compactMainRev, prevCompactRev int64) (KeyVal
 
 		tx := s.b.BatchTx()
 		tx.LockOutsideApply()
+		// gofail: var compactAfterAcquiredBatchTxLock struct{}
 		keys, values := tx.UnsafeRange(schema.Key, last, end, int64(batchNum))
 		for i := range keys {
 			rev = BytesToRev(keys[i])
@@ -63,6 +64,7 @@ func (s *store) scheduleCompaction(compactMainRev, prevCompactRev int64) (KeyVal
 			// gofail: var compactBeforeSetFinishedCompact struct{}
 			UnsafeSetFinishedCompact(tx, compactMainRev)
 			tx.Unlock()
+			dbCompactionPauseMs.Observe(float64(time.Since(start) / time.Millisecond))
 			// gofail: var compactAfterSetFinishedCompact struct{}
 			hash := h.Hash()
 			size, sizeInUse := s.b.Size(), s.b.SizeInUse()
diff --git a/server/storage/mvcc/watchable_store.go b/server/storage/mvcc/watchable_store.go
index 0a7cb0251..e309b5cfa 100644
--- a/server/storage/mvcc/watchable_store.go
+++ b/server/storage/mvcc/watchable_store.go
@@ -162,12 +162,12 @@ func (s *watchableStore) cancelWatcher(wa *watcher) {
 		} else if s.synced.delete(wa) {
 			watcherGauge.Dec()
 			break
-		} else if wa.compacted {
-			watcherGauge.Dec()
-			break
 		} else if wa.ch == nil {
 			// already canceled (e.g., cancel/close race)
 			break
+		} else if wa.compacted {
+			watcherGauge.Dec()
+			break
 		}
 
 		if !wa.victim {
diff --git a/server/storage/schema/membership.go b/server/storage/schema/membership.go
index c79ab8b76..726657ba9 100644
--- a/server/storage/schema/membership.go
+++ b/server/storage/schema/membership.go
@@ -37,6 +37,9 @@ type membershipBackend struct {
 	be backend.Backend
 }
 
+// NewMembershipBackend returns a new membership backend
+// Refer to https://github.com/etcd-io/etcd/pull/19343#discussion_r1958056718
+// revive:disable-next-line:unexported-return
 func NewMembershipBackend(lg *zap.Logger, be backend.Backend) *membershipBackend {
 	return &membershipBackend{
 		lg: lg,
diff --git a/server/storage/wal/repair.go b/server/storage/wal/repair.go
index 16277540f..f8f814f04 100644
--- a/server/storage/wal/repair.go
+++ b/server/storage/wal/repair.go
@@ -49,8 +49,7 @@ func Repair(lg *zap.Logger, dirpath string) bool {
 		switch {
 		case err == nil:
 			// update crc of the decoder when necessary
-			switch rec.Type {
-			case CrcType:
+			if rec.Type == CrcType {
 				crc := decoder.LastCRC()
 				// current crc of decoder must match the crc of the record.
 				// do no need to match 0 crc, since the decoder is a new one at this case.
diff --git a/tools/.golangci.yaml b/tools/.golangci.yaml
index 0cc4524e9..d5a87964e 100644
--- a/tools/.golangci.yaml
+++ b/tools/.golangci.yaml
@@ -31,6 +31,7 @@ linters:
     - staticcheck
     - stylecheck
     - testifylint
+    - thelper
     - unconvert # Remove unnecessary type conversions
     - unparam
     - unused
@@ -48,49 +49,30 @@ linters-settings: # please keep this alphabetized
     confidence: 0.8
     rules:
       - name: blank-imports
-        disabled: false
       - name: context-as-argument
-        disabled: false
       - name: context-keys-type
-        disabled: false
       - name: dot-imports
-        disabled: false
       - name: early-return
-        disabled: false
         arguments:
           - "preserveScope"
       - name: error-return
-        disabled: false
       - name: error-naming
-        disabled: false
       - name: error-strings
-        disabled: false
       - name: errorf
-        disabled: false
       - name: if-return
-        disabled: false
       - name: increment-decrement
-        disabled: false
       - name: indent-error-flow
-        disabled: false
       - name: package-comments
-        disabled: false
       - name: range
-        disabled: false
       - name: receiver-naming
-        disabled: false
       - name: superfluous-else
-        disabled: false
         arguments:
           - "preserveScope"
       - name: time-naming
-        disabled: false
+      - name: unnecessary-stmt
       - name: use-any
-        disabled: false
       - name: var-declaration
-        disabled: false
       - name: var-naming
-        disabled: false
         arguments:
           # The following is the configuration for var-naming rule, the first element is the allow list and the second element is the deny list.
           - [] # AllowList: leave it empty to use the default (empty, too). This means that we're not relaxing the rule in any way, i.e. elementId will raise a violation, it should be elementID, refer to the next line to see the list of denied initialisms.
@@ -121,5 +103,18 @@ linters-settings: # please keep this alphabetized
       # to always require f-functions for stretchr/testify, but not for golang standard lib.
       # Also refer to https://github.com/etcd-io/etcd/pull/18741#issuecomment-2422395914
       require-f-funcs: true
+  thelper:
+    test:
+      first: false
+      begin: false
+    benchmark:
+      first: false
+      begin: false
+    tb:
+      first: false
+      begin: false
+    fuzz:
+      first: false
+      begin: false
   usetesting:
     os-mkdir-temp: false
diff --git a/tools/.markdownlint.jsonc b/tools/.markdownlint.jsonc
index 0263558de..d6b36639c 100644
--- a/tools/.markdownlint.jsonc
+++ b/tools/.markdownlint.jsonc
@@ -64,25 +64,7 @@
       "maximum": 1
     },
   
-    // MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md013.md
-    "MD013": {
-      // Number of characters
-      "line_length": 80,
-      // Number of characters for headings
-      "heading_line_length": 80,
-      // Number of characters for code blocks
-      "code_block_line_length": 80,
-      // Include code blocks
-      "code_blocks": true,
-      // Include tables
-      "tables": true,
-      // Include headings
-      "headings": true,
-      // Strict length checking
-      "strict": false,
-      // Stern length checking
-      "stern": false
-    },
+    "MD013": false,
   
     // MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md014.md
     "MD014": true,
@@ -202,23 +184,13 @@
     },
   
     // MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md041.md
-    "MD041": {
-      // Heading level
-      "level": 1,
-      // RegExp for matching title in front matter
-      "front_matter_title": "^\\s*title\\s*[:=]"
-    },
+    "MD041": false,
   
     // MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md042.md
     "MD042": true,
   
     // MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md043.md
-    "MD043": {
-      // List of headings
-      "headings": [],
-      // Match case of headings
-      "match_case": false
-    },
+    "MD043": false,
   
     // MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md044.md
     "MD044": {
@@ -307,4 +279,4 @@
   
     // MD058/blanks-around-tables : Tables should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md058.md
     "MD058": true
-  }
\ No newline at end of file
+  }
diff --git a/tools/etcd-dump-logs/README.md b/tools/etcd-dump-logs/README.md
index 0922f2878..58ed80df6 100644
--- a/tools/etcd-dump-logs/README.md
+++ b/tools/etcd-dump-logs/README.md
@@ -52,7 +52,10 @@ Flags:
 	    IRRRange, IRRPut, IRRDeleteRange, IRRTxn,
 	    IRRCompaction, IRRLeaseGrant, IRRLeaseRevoke
   -start-index uint
-    	The index to start dumping
+    	The index to start dumping (inclusive)
+      If unspecified, dumps from the index of the last snapshot.
+  -end-index uint
+      The index to stop dumping (exclusive)
   -start-snap string
     	The base name of snapshot file to start dumping
   -stream-decoder string
@@ -146,11 +149,11 @@ Entry types () count is : 8
 ```
 ####  etcd-dump-logs -start-index <INDEX NUMBER> [data dir]
 
-Only shows WAL log entries after the specified start-index number, exclusively.
+Only shows WAL log entries after the specified start-index number, inclusively.
 
 ```
-$ etcd-dump-logs -start-index 30  /tmp/datadir
-Start dumping log entries from index 30.
+$ etcd-dump-logs -start-index 31  /tmp/datadir
+Start dumping log entries from index 31.
 WAL metadata:
 nodeID=0 clusterID=0 term=0 commitIndex=0 vote=0
 WAL entries:
@@ -162,4 +165,23 @@ term	     index	type	data
   27	        34	norm	???
 Entry types () count is : 4
 ```
+
+####  etcd-dump-logs -start-index <INDEX NUMBER> -end-index <INDEX NUMBER> [data dir]
+
+Only shows WAL log entries from the specified start-index number (inclusively) to the specified end-index number (exclusively).
+
+```
+$ etcd-dump-logs -start-index 930 -end-index 932  /tmp/datadir
+Start dumping log entries from index 930.
+WAL metadata:
+nodeID=0 clusterID=0 term=5 commitIndex=2448 vote=0
+WAL entries: 2
+lastIndex=931
+term	     index	type	data
+   3	       930	norm	header:<ID:11010058442592651283 > put:<key:"key7" value:"923" >
+   3	       931	norm	header:<ID:6577953459306661672 > put:<key:"key8" value:"924" >
+
+Entry types (Normal,ConfigChange) count is : 2
+```
+
 [decoder_correctoutputformat.sh]: ./testdecoder/decoder_correctoutputformat.sh
diff --git a/tools/etcd-dump-logs/main.go b/tools/etcd-dump-logs/main.go
index f0bba4467..f0d2a29c4 100644
--- a/tools/etcd-dump-logs/main.go
+++ b/tools/etcd-dump-logs/main.go
@@ -23,6 +23,7 @@ import (
 	"fmt"
 	"io"
 	"log"
+	"math"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -51,7 +52,8 @@ const (
 func main() {
 	snapfile := flag.String("start-snap", "", "The base name of snapshot file to start dumping")
 	waldir := flag.String("wal-dir", "", "If set, dumps WAL from the informed path, rather than following the standard 'data_dir/member/wal/' location")
-	index := flag.Uint64("start-index", 0, "The index to start dumping")
+	startIndex := flag.Uint64("start-index", 0, "The index to start dumping (inclusive). If unspecified, dumps from the index of the last snapshot.")
+	endIndex := flag.Uint64("end-index", math.MaxUint64, "The index to stop dumping (exclusive)")
 	// Default entry types are Normal and ConfigChange
 	entrytype := flag.String("entry-type", defaultEntryTypes, `If set, filters output by entry type. Must be one or more than one of:
 ConfigChange, Normal, Request, InternalRaftRequest,
@@ -70,7 +72,7 @@ and output a hex encoded line of binary for each input line`)
 	}
 	dataDir := flag.Args()[0]
 
-	if *snapfile != "" && *index != 0 {
+	if *snapfile != "" && *startIndex != 0 {
 		log.Fatal("start-snap and start-index flags cannot be used together.")
 	}
 
@@ -82,7 +84,7 @@ and output a hex encoded line of binary for each input line`)
 	})
 
 	if !*raw {
-		ents := readUsingReadAll(lg, startFromIndex, index, snapfile, dataDir, waldir)
+		ents := readUsingReadAll(lg, startFromIndex, startIndex, endIndex, snapfile, dataDir, waldir)
 
 		fmt.Printf("WAL entries: %d\n", len(ents))
 		if len(ents) > 0 {
@@ -107,20 +109,25 @@ and output a hex encoded line of binary for each input line`)
 		if wd == "" {
 			wd = walDir(dataDir)
 		}
-		readRaw(index, wd, os.Stdout)
+		readRaw(startIndex, wd, os.Stdout)
 	}
 }
 
-func readUsingReadAll(lg *zap.Logger, startFromIndex bool, index *uint64, snapfile *string, dataDir string, waldir *string) []raftpb.Entry {
+func readUsingReadAll(lg *zap.Logger, startFromIndex bool, startIndex *uint64, endIndex *uint64, snapfile *string, dataDir string, waldir *string) []raftpb.Entry {
 	var (
 		walsnap  walpb.Snapshot
 		snapshot *raftpb.Snapshot
 		err      error
 	)
 
+	endAtIndex := *endIndex < math.MaxUint64
 	if startFromIndex {
-		fmt.Printf("Start dumping log entries from index %d.\n", *index)
-		walsnap.Index = *index
+		fmt.Printf("Start dumping log entries from index %d.\n", *startIndex)
+		// ReadAll() reads entries from the index after walsnap.Index, so we need to move walsnap.Index back one.
+		if *startIndex > 0 {
+			*startIndex--
+		}
+		walsnap.Index = *startIndex
 	} else {
 		if *snapfile == "" {
 			ss := snap.New(lg, snapDir(dataDir))
@@ -160,12 +167,29 @@ func readUsingReadAll(lg *zap.Logger, startFromIndex bool, index *uint64, snapfi
 	wmetadata, state, ents, err := w.ReadAll()
 	w.Close()
 	if err != nil && (!startFromIndex || !errors.Is(err, wal.ErrSnapshotNotFound)) {
-		log.Fatalf("Failed reading WAL: %v", err)
+		// ReadAll might return ErrSliceOutOfRange and the first series of entries if the server is offline for a while and receives a snapshot from leader.
+		// It is ok to ignore ErrSliceOutOfRange if just requesting a specific range of entries
+		if !endAtIndex || !errors.Is(err, wal.ErrSliceOutOfRange) {
+			log.Fatalf("Failed reading WAL: %v", err)
+		}
+		log.Printf("Failed reading all WAL: %v", err)
 	}
 	id, cid := parseWALMetadata(wmetadata)
 	vid := types.ID(state.Vote)
 	fmt.Printf("WAL metadata:\nnodeID=%s clusterID=%s term=%d commitIndex=%d vote=%s\n",
 		id, cid, state.Term, state.Commit, vid)
+	if endAtIndex {
+		entries := make([]raftpb.Entry, 0)
+		for _, e := range ents {
+			// WAL might contain entries with e.Index >= *endIndex from prev term, then e.Index < *endIndex in the next term.
+			// We cannot break when e.Index >= *endIndex.
+			if e.Index >= *endIndex {
+				continue
+			}
+			entries = append(entries, e)
+		}
+		return entries
+	}
 	return ents
 }
 
diff --git a/tools/mod/go.mod b/tools/mod/go.mod
index 1b790c2bd..6e8cf4f67 100644
--- a/tools/mod/go.mod
+++ b/tools/mod/go.mod
@@ -2,29 +2,30 @@ module go.etcd.io/etcd/tools/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
-	github.com/alexfalkowski/gocovmerge v1.4.0
+	github.com/alexfalkowski/gocovmerge v1.7.0
 	github.com/appscodelabs/license-bill-of-materials v0.0.0-20220707232035-6018e0c5287c
 	github.com/chzchzchz/goword v0.0.0-20170907005317-a9744cb52b03
 	github.com/cloudflare/cfssl v1.6.5
 	github.com/gogo/protobuf v1.3.2
-	github.com/golangci/golangci-lint v1.64.6
+	github.com/golangci/golangci-lint v1.64.8
 	github.com/google/addlicense v1.1.1
 	github.com/google/yamlfmt v0.16.0
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
 	go.etcd.io/gofail v0.2.0
 	go.etcd.io/protodoc v0.0.0-20180829002748-484ab544e116
 	go.etcd.io/raft/v3 v3.6.0
-	gotest.tools/gotestsum v1.12.0
+	gotest.tools/gotestsum v1.12.1
 	gotest.tools/v3 v3.5.2
-	honnef.co/go/tools v0.6.0
+	honnef.co/go/tools v0.6.1
 )
 
 require (
 	4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
 	4d63.com/gochecknoglobals v0.2.2 // indirect
+	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/4meepo/tagalign v1.4.2 // indirect
 	github.com/Abirdcfly/dupword v0.1.3 // indirect
 	github.com/Antonboom/errname v1.0.0 // indirect
@@ -35,7 +36,7 @@ require (
 	github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
 	github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect
 	github.com/Masterminds/semver/v3 v3.3.0 // indirect
-	github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
+	github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
 	github.com/akhenakh/hunspellgo v0.0.0-20160221122622-9db38fa26e19 // indirect
 	github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
 	github.com/alexkohler/nakedret/v2 v2.0.5 // indirect
@@ -63,19 +64,19 @@ require (
 	github.com/ckaznocha/intrange v0.3.0 // indirect
 	github.com/curioswitch/go-reassign v0.3.0 // indirect
 	github.com/daixiang0/gci v0.13.5 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/denis-tingaikin/go-header v0.5.0 // indirect
 	github.com/dnephin/pflag v1.0.7 // indirect
 	github.com/ettle/strcase v0.2.0 // indirect
 	github.com/fatih/color v1.18.0 // indirect
 	github.com/fatih/structtag v1.2.0 // indirect
 	github.com/firefart/nonamedreturns v1.0.5 // indirect
-	github.com/fsnotify/fsnotify v1.7.0 // indirect
+	github.com/fsnotify/fsnotify v1.8.0 // indirect
 	github.com/fzipp/gocyclo v0.6.0 // indirect
 	github.com/ghostiam/protogetter v0.3.9 // indirect
 	github.com/go-critic/go-critic v0.12.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
-	github.com/go-sql-driver/mysql v1.7.1 // indirect
+	github.com/go-sql-driver/mysql v1.8.1 // indirect
 	github.com/go-toolsmith/astcast v1.1.0 // indirect
 	github.com/go-toolsmith/astcopy v1.1.0 // indirect
 	github.com/go-toolsmith/astequal v1.2.0 // indirect
@@ -88,14 +89,14 @@ require (
 	github.com/gobwas/glob v0.2.3 // indirect
 	github.com/gofrs/flock v0.12.1 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
-	github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
+	github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
 	github.com/golangci/go-printf-func-name v0.1.0 // indirect
 	github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
 	github.com/golangci/misspell v0.6.0 // indirect
 	github.com/golangci/plugin-module-register v0.1.1 // indirect
 	github.com/golangci/revgrep v0.8.0 // indirect
 	github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
-	github.com/google/certificate-transparency-go v1.1.7 // indirect
+	github.com/google/certificate-transparency-go v1.1.8 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/gordonklaus/ineffassign v0.1.0 // indirect
@@ -106,20 +107,18 @@ require (
 	github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
 	github.com/hashicorp/go-version v1.7.0 // indirect
 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hexops/gotextdiff v1.0.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jgautheron/goconst v1.7.1 // indirect
 	github.com/jingyugao/rowserrcheck v1.1.1 // indirect
 	github.com/jjti/go-spancheck v0.6.4 // indirect
 	github.com/jmhodges/clock v1.2.0 // indirect
-	github.com/jmoiron/sqlx v1.3.5 // indirect
+	github.com/jmoiron/sqlx v1.4.0 // indirect
 	github.com/julz/importas v0.2.0 // indirect
 	github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
 	github.com/kisielk/errcheck v1.9.0 // indirect
 	github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 // indirect
 	github.com/kkHAIKE/contextcheck v1.1.6 // indirect
-	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/kulti/thelper v0.6.3 // indirect
 	github.com/kunwardeep/paralleltest v1.0.10 // indirect
 	github.com/lasiar/canonicalheader v1.1.2 // indirect
@@ -131,14 +130,13 @@ require (
 	github.com/leonklingele/grouper v1.1.2 // indirect
 	github.com/lib/pq v1.10.9 // indirect
 	github.com/macabu/inamedparam v0.1.3 // indirect
-	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/maratori/testableexamples v1.0.0 // indirect
 	github.com/maratori/testpackage v1.1.1 // indirect
 	github.com/matoous/godox v1.1.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
-	github.com/mattn/go-sqlite3 v1.14.22 // indirect
+	github.com/mattn/go-sqlite3 v1.14.24 // indirect
 	github.com/mgechev/revive v1.7.0 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -151,11 +149,11 @@ require (
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/polyfloyd/go-errorlint v1.7.1 // indirect
-	github.com/prometheus/client_golang v1.21.0 // indirect
-	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.62.0 // indirect
+	github.com/prometheus/client_golang v1.22.0 // indirect
+	github.com/prometheus/client_model v0.6.2 // indirect
+	github.com/prometheus/common v0.63.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
 	github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
@@ -168,27 +166,28 @@ require (
 	github.com/ryancurrah/gomodguard v1.3.5 // indirect
 	github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
+	github.com/sagikazarmark/locafero v0.7.0 // indirect
 	github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
 	github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
 	github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
 	github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect
-	github.com/securego/gosec/v2 v2.22.1 // indirect
+	github.com/securego/gosec/v2 v2.22.2 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/sivchari/containedctx v1.0.3 // indirect
 	github.com/sivchari/tenv v1.12.1 // indirect
 	github.com/sonatard/noctx v0.1.0 // indirect
+	github.com/sourcegraph/conc v0.3.0 // indirect
 	github.com/sourcegraph/go-diff v0.7.0 // indirect
 	github.com/spf13/afero v1.12.0 // indirect
-	github.com/spf13/cast v1.5.0 // indirect
+	github.com/spf13/cast v1.7.1 // indirect
 	github.com/spf13/cobra v1.9.1 // indirect
-	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
-	github.com/spf13/viper v1.12.0 // indirect
+	github.com/spf13/viper v1.20.0 // indirect
 	github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
 	github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/stretchr/testify v1.10.0 // indirect
-	github.com/subosito/gotenv v1.4.1 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/tdakkota/asciicheck v0.4.1 // indirect
 	github.com/tetafro/godot v1.5.0 // indirect
 	github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect
@@ -200,36 +199,36 @@ require (
 	github.com/ultraware/whitespace v0.2.0 // indirect
 	github.com/uudashr/gocognit v1.2.0 // indirect
 	github.com/uudashr/iface v1.3.1 // indirect
-	github.com/weppos/publicsuffix-go v0.30.0 // indirect
+	github.com/weppos/publicsuffix-go v0.30.3-0.20240510084413-5f1d03393b3d // indirect
 	github.com/xen0n/gosmopolitan v1.2.2 // indirect
 	github.com/yagipy/maintidx v1.0.0 // indirect
 	github.com/yeya24/promlinter v0.3.0 // indirect
 	github.com/ykadowak/zerologlint v0.1.5 // indirect
-	github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 // indirect
-	github.com/zmap/zlint/v3 v3.5.0 // indirect
+	github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c // indirect
+	github.com/zmap/zlint/v3 v3.6.0 // indirect
 	gitlab.com/bosi/decorder v0.4.2 // indirect
 	go-simpler.org/musttag v0.13.0 // indirect
 	go-simpler.org/sloglint v0.9.0 // indirect
 	go.uber.org/automaxprocs v1.6.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
-	golang.org/x/crypto v0.35.0 // indirect
+	golang.org/x/crypto v0.37.0 // indirect
+	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
 	golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
-	golang.org/x/mod v0.23.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sync v0.11.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/term v0.29.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	golang.org/x/tools v0.30.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/grpc v1.70.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
-	gopkg.in/ini.v1 v1.67.0 // indirect
+	golang.org/x/mod v0.24.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sync v0.13.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/term v0.31.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	golang.org/x/tools v0.32.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/grpc v1.72.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/klog/v2 v2.100.1 // indirect
+	k8s.io/klog/v2 v2.130.1 // indirect
 	mvdan.cc/gofumpt v0.7.0 // indirect
 	mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
 )
diff --git a/tools/mod/go.sum b/tools/mod/go.sum
index abfc6ebd6..35e24ec7f 100644
--- a/tools/mod/go.sum
+++ b/tools/mod/go.sum
@@ -2,6 +2,9 @@
 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
+cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
 github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
 github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
@@ -22,8 +25,9 @@ github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnh
 github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg=
 github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
 github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
-github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA=
-github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ=
+github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
+github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
+github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
 github.com/akhenakh/hunspellgo v0.0.0-20160221122622-9db38fa26e19 h1:bYOD6QJnBJY79MJQR1i9cyQePG5oNDZXDKL2bhN/uvE=
 github.com/akhenakh/hunspellgo v0.0.0-20160221122622-9db38fa26e19/go.mod h1:HcqyLXmWoESd/vPSbCPqvgw5l5cMM5PtoqFOnXLjSeM=
 github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
@@ -32,8 +36,8 @@ github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsr
 github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
-github.com/alexfalkowski/gocovmerge v1.4.0 h1:sKiQYXR4CYdKOxwcZbVVHka0Pv8qADVule33pxcMz0M=
-github.com/alexfalkowski/gocovmerge v1.4.0/go.mod h1:v1ugjLQktGsqJ8WPOooeKvd6eJlucF/wmHBpPp+kals=
+github.com/alexfalkowski/gocovmerge v1.7.0 h1:R/zzCbFZ91WDea7UcoQdEflfv406ARu94eeLCYcuP+M=
+github.com/alexfalkowski/gocovmerge v1.7.0/go.mod h1:pnseUyIoFOj8DWSE1n7lZyGceAf3Y/JM8KebVaQFAVA=
 github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU=
 github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU=
 github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
@@ -71,6 +75,7 @@ github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY
 github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M=
 github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
 github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
+github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/catenacyber/perfsprint v0.8.2 h1:+o9zVmCSVa7M4MvabsWvESEhpsMkhfE7k0sHNGL95yw=
 github.com/catenacyber/perfsprint v0.8.2/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM=
 github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
@@ -87,6 +92,7 @@ github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd
 github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo=
 github.com/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI=
 github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4=
+github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
 github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
 github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -95,8 +101,9 @@ github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPw
 github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c=
 github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
 github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
 github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
@@ -105,32 +112,28 @@ github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk=
 github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE=
 github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
 github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
-github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
-github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
 github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=
 github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
-github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
 github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
 github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ=
 github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
 github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=
 github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w=
-github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
-github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
-github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
@@ -162,16 +165,19 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
 github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
-github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM=
-github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
+github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=
+github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
 github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU=
 github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=
 github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
 github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
-github.com/golangci/golangci-lint v1.64.6 h1:jOLaQN41IV7bMzXuNC4UnQGll7N1xY6eFDXkXEPGKAs=
-github.com/golangci/golangci-lint v1.64.6/go.mod h1:Wz9q+6EVuqGQ94GQ96RB2mjpcZYTOGhBhbt4O7REPu4=
+github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I=
+github.com/golangci/golangci-lint v1.64.8/go.mod h1:5cEsUQBSr6zi8XI8OjmcY2Xmliqc4iYL7YoPrL+zLJ4=
 github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
 github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
 github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
@@ -182,19 +188,19 @@ github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNF
 github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ=
 github.com/google/addlicense v1.1.1 h1:jpVf9qPbU8rz5MxKo7d+RMcNHkqxi4YJi/laauX4aAE=
 github.com/google/addlicense v1.1.1/go.mod h1:Sm/DHu7Jk+T5miFHHehdIjbi4M5+dJDRS3Cq0rncIxA=
-github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw=
-github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE=
+github.com/google/certificate-transparency-go v1.1.8 h1:LGYKkgZF7satzgTak9R4yzfJXEeYVAjV6/EAEJOf1to=
+github.com/google/certificate-transparency-go v1.1.8/go.mod h1:bV/o8r0TBKRf1X//iiiSgWrvII4d7/8OiA+3vG26gI8=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 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-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
-github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/go-github/v50 v50.2.0/go.mod h1:VBY8FB6yPIjrtKhozXv4FQupxKLS6H4m6xFZlT43q8Q=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
 github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
@@ -227,8 +233,6 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -241,8 +245,8 @@ github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpR
 github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk=
 github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs=
 github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI=
-github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
-github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
 github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
 github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
 github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI=
@@ -255,10 +259,7 @@ github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw
 github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
 github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
 github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
-github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
-github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -284,13 +285,10 @@ github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA
 github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ=
 github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
 github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk=
 github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I=
-github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
-github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI=
 github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE=
 github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04=
@@ -299,20 +297,16 @@ github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
 github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
 github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
 github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY=
 github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -351,18 +345,19 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA=
 github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8=
 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.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
-github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo=
@@ -380,7 +375,6 @@ github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtz
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -390,6 +384,8 @@ github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9f
 github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
+github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
+github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
 github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
 github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
 github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
@@ -398,8 +394,8 @@ github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tM
 github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
 github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ=
 github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8=
-github.com/securego/gosec/v2 v2.22.1 h1:IcBt3TpI5Y9VN1YlwjSpM2cHu0i3Iw52QM+PQeg7jN8=
-github.com/securego/gosec/v2 v2.22.1/go.mod h1:4bb95X4Jz7VSEPdVjC0hD7C/yR6kdeUBvCPOy9gDQ0g=
+github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g=
+github.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE=
 github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
 github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
 github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -413,21 +409,21 @@ github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY=
 github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw=
 github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM=
 github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
 github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
 github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
 github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
-github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
-github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
-github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
+github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
+github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
 github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
 github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4=
@@ -449,8 +445,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
-github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
 github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
 github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
@@ -477,11 +473,10 @@ github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYR
 github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
 github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U=
 github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg=
-github.com/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
 github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
-github.com/weppos/publicsuffix-go v0.30.0 h1:QHPZ2GRu/YE7cvejH9iyavPOkVCB4dNxp2ZvtT+vQLY=
-github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY=
-github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE=
+github.com/weppos/publicsuffix-go v0.30.2-0.20230730094716-a20f9abcc222/go.mod h1:s41lQh6dIsDWIC1OWh7ChWJXLH0zkJ9KHZVqA7vHyuQ=
+github.com/weppos/publicsuffix-go v0.30.3-0.20240510084413-5f1d03393b3d h1:q80YKUcDWRNvvQcziH63e3ammTWARwrhohBCunHaYAg=
+github.com/weppos/publicsuffix-go v0.30.3-0.20240510084413-5f1d03393b3d/go.mod h1:vLdXKydr/OJssAXmjY0XBgLXUfivBMrNRIBljgtqCnw=
 github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU=
 github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg=
 github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
@@ -503,11 +498,11 @@ github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54t
 github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk=
 github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
 github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ=
-github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 h1:DZH5n7L3L8RxKdSyJHZt7WePgwdhHnPhQFdQSJaHF+o=
-github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300/go.mod h1:mOd4yUMgn2fe2nV9KXsa9AyQBFZGzygVPovsZR+Rl5w=
+github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c h1:U1b4THKcgOpJ+kILupuznNwPiURtwVW3e9alJvji9+s=
+github.com/zmap/zcrypto v0.0.0-20231219022726-a1f61fb1661c/go.mod h1:GSDpFDD4TASObxvfZfvpZZ3OWHIUHMlhVWlkOe4ewVk=
 github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8=
-github.com/zmap/zlint/v3 v3.5.0 h1:Eh2B5t6VKgVH0DFmTwOqE50POvyDhUaU9T2mJOe1vfQ=
-github.com/zmap/zlint/v3 v3.5.0/go.mod h1:JkNSrsDJ8F4VRtBZcYUQSvnWFL7utcjDIn+FE64mlBI=
+github.com/zmap/zlint/v3 v3.6.0 h1:vTEaDRtYN0d/1Ax60T+ypvbLQUHwHxbvYRnUMVr35ug=
+github.com/zmap/zlint/v3 v3.6.0/go.mod h1:NVgiIWssgzp0bNl8P4Gz94NHV2ep/4Jyj9V69uTmZyg=
 gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
 gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
 go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
@@ -537,17 +532,17 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
@@ -558,19 +553,16 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
 golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
-golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -580,19 +572,18 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
 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=
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -602,9 +593,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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=
@@ -618,31 +608,27 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
@@ -650,10 +636,11 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
-golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -667,8 +654,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 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-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
@@ -684,50 +672,49 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
 golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
-golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
-golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
-golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/gotestsum v1.12.0 h1:CmwtaGDkHxrZm4Ib0Vob89MTfpc3GrEFMJKovliPwGk=
-gotest.tools/gotestsum v1.12.0/go.mod h1:fAvqkSptospfSbQw26CTYzNwnsE/ztqLeyhP0h67ARY=
-gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
+gotest.tools/gotestsum v1.12.1 h1:dvcxFBTFR1QsQmrCQa4k/vDXow9altdYz4CjdW+XeBE=
+gotest.tools/gotestsum v1.12.1/go.mod h1:mwDmLbx9DIvr09dnAoGgQPLaSXszNpXpWo2bsQge5BE=
 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
-honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8=
-honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
-k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
-k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
+honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
+k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
+k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
 mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
 mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=
diff --git a/tools/rw-heatmaps/cmd/root.go b/tools/rw-heatmaps/cmd/root.go
index 49b98a9fc..722bddb20 100644
--- a/tools/rw-heatmaps/cmd/root.go
+++ b/tools/rw-heatmaps/cmd/root.go
@@ -33,6 +33,8 @@ var (
 	ErrMissingInputFileArg = fmt.Errorf("missing input file argument")
 	// ErrInvalidOutputFormat is returned when the output format is invalid.
 	ErrInvalidOutputFormat = fmt.Errorf("invalid output format, must be one of png, jpg, jpeg, tiff")
+	// ErrInvalidChartTYpe is returned when the chart type is invalid.
+	ErrInvalidChartType = fmt.Errorf("invalid chart type, must be one of line, heatmap")
 )
 
 // NewRootCommand returns the root command for the rw-heatmaps tool.
@@ -56,6 +58,10 @@ func NewRootCommand() *cobra.Command {
 				}
 			}
 
+			if o.chartType == "line" {
+				return chart.PlotLineCharts(datasets, o.title, o.outputImageFile, o.outputFormat)
+			}
+
 			return chart.PlotHeatMaps(datasets, o.title, o.outputImageFile, o.outputFormat, o.zeroCentered)
 		},
 	}
@@ -70,6 +76,7 @@ type options struct {
 	outputImageFile string
 	outputFormat    string
 	zeroCentered    bool
+	chartType       string
 }
 
 // newOptions returns a new options for the command with the default values applied.
@@ -77,6 +84,7 @@ func newOptions() options {
 	return options{
 		outputFormat: "jpg",
 		zeroCentered: true,
+		chartType:    "heatmap",
 	}
 }
 
@@ -86,6 +94,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
 	fs.StringVarP(&o.outputImageFile, "output-image-file", "o", o.outputImageFile, "output image filename (required)")
 	fs.StringVarP(&o.outputFormat, "output-format", "f", o.outputFormat, "output image file format")
 	fs.BoolVar(&o.zeroCentered, "zero-centered", o.zeroCentered, "plot the improvement graph with white color represents 0.0")
+	fs.StringVarP(&o.chartType, "chart-type", "c", o.chartType, `type of chart to plot ["line", or "heatmap"]`)
 }
 
 // Validate returns an error if the options are invalid.
@@ -101,5 +110,10 @@ func (o *options) Validate() error {
 	default:
 		return ErrInvalidOutputFormat
 	}
+	switch o.chartType {
+	case "line", "heatmap":
+	default:
+		return ErrInvalidChartType
+	}
 	return nil
 }
diff --git a/tools/rw-heatmaps/go.mod b/tools/rw-heatmaps/go.mod
index 8d031cd04..e09195c46 100644
--- a/tools/rw-heatmaps/go.mod
+++ b/tools/rw-heatmaps/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/tools/rw-heatmaps/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/spf13/cobra v1.9.1
@@ -19,8 +19,8 @@ require (
 	github.com/go-pdf/fpdf v0.8.0 // indirect
 	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
 	golang.org/x/image v0.18.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
 )
diff --git a/tools/rw-heatmaps/go.sum b/tools/rw-heatmaps/go.sum
index 2face21be..b0a31597a 100644
--- a/tools/rw-heatmaps/go.sum
+++ b/tools/rw-heatmaps/go.sum
@@ -25,8 +25,9 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
@@ -36,8 +37,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
+golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
 golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
 golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -52,8 +53,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 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.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
diff --git a/tools/rw-heatmaps/pkg/chart/line_chart.go b/tools/rw-heatmaps/pkg/chart/line_chart.go
new file mode 100644
index 000000000..b7421433c
--- /dev/null
+++ b/tools/rw-heatmaps/pkg/chart/line_chart.go
@@ -0,0 +1,226 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package chart
+
+import (
+	"cmp"
+	"fmt"
+	"image/color"
+	"slices"
+	"sort"
+
+	"gonum.org/v1/plot"
+	"gonum.org/v1/plot/font"
+	"gonum.org/v1/plot/plotter"
+	"gonum.org/v1/plot/plotutil"
+	"gonum.org/v1/plot/vg"
+	"gonum.org/v1/plot/vg/draw"
+	"gonum.org/v1/plot/vg/vgimg"
+
+	"go.etcd.io/etcd/tools/rw-heatmaps/v3/pkg/dataset"
+)
+
+// PlotLineCharts creates a new line chart.
+func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputFormat string) error {
+	plot.DefaultFont = font.Font{
+		Typeface: "Liberation",
+		Variant:  "Sans",
+	}
+
+	canvas := plotLineChart(datasets, title)
+	return saveCanvas(canvas, "readwrite", outputImageFile, outputFormat)
+}
+
+func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas {
+	ratiosLength := func() int {
+		max := slices.MaxFunc(datasets, func(a, b *dataset.DataSet) int {
+			return cmp.Compare(len(a.GetSortedRatios()), len(b.GetSortedRatios()))
+		})
+		return len(max.GetSortedRatios())
+	}()
+
+	// Make a nx1 grid of line charts.
+	const cols = 1
+	rows := ratiosLength
+
+	// Set the width and height of the canvas.
+	width, height := 30*vg.Centimeter, 15*font.Length(ratiosLength)*vg.Centimeter
+
+	canvas := vgimg.New(width, height)
+	dc := draw.New(canvas)
+
+	// Create a tiled layout for the plots.
+	t := draw.Tiles{
+		Rows:      rows,
+		Cols:      cols,
+		PadX:      vg.Millimeter * 4,
+		PadY:      vg.Millimeter * 4,
+		PadTop:    vg.Millimeter * 15,
+		PadBottom: vg.Millimeter * 2,
+		PadLeft:   vg.Millimeter * 2,
+		PadRight:  vg.Millimeter * 2,
+	}
+
+	plots := make([][]*plot.Plot, rows)
+	legends := make([]plot.Legend, rows)
+	for i := range plots {
+		plots[i] = make([]*plot.Plot, cols)
+	}
+
+	// Load records into the grid.
+	ratios := slices.MaxFunc(datasets, func(a, b *dataset.DataSet) int {
+		return cmp.Compare(len(a.GetSortedRatios()), len(b.GetSortedRatios()))
+	}).GetSortedRatios()
+
+	for row, ratio := range ratios {
+		var records [][]dataset.DataRecord
+		var fileNames []string
+		for _, d := range datasets {
+			records = append(records, d.Records[ratio])
+			fileNames = append(fileNames, d.FileName)
+		}
+		p, l := plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records, fileNames)
+		plots[row] = []*plot.Plot{p}
+		legends[row] = l
+	}
+
+	// Fill the canvas with the plots and legends.
+	canvases := plot.Align(plots, t, dc)
+	for i := 0; i < rows; i++ {
+		// Continue if there is no plot in the current cell (incomplete data).
+		if plots[i][0] == nil {
+			continue
+		}
+
+		l := legends[i]
+		r := l.Rectangle(canvases[i][0])
+		legendWidth := r.Max.X - r.Min.X
+		// Adjust the legend down a little.
+		l.YOffs = plots[i][0].Title.TextStyle.FontExtents().Height * 3
+		l.Draw(canvases[i][0])
+
+		c := draw.Crop(canvases[i][0], 0, -legendWidth-vg.Millimeter, 0, 0)
+		plots[i][0].Draw(c)
+	}
+
+	// Add the title and parameter legend.
+	l := plot.NewLegend()
+	l.Add(title)
+	for _, d := range datasets {
+		l.Add(fmt.Sprintf("%s: %s", d.FileName, d.Param))
+	}
+	l.Top = true
+	l.Left = true
+	l.Draw(dc)
+
+	return canvas
+}
+
+func plotIndividualLineChart(title string, records [][]dataset.DataRecord, fileNames []string) (*plot.Plot, plot.Legend) {
+	p := plot.New()
+	p.Title.Text = title
+	p.X.Label.Text = "Connections Amount"
+	p.X.Scale = plot.LogScale{}
+	p.X.Tick.Marker = pow2Ticks{}
+	p.Y.Label.Text = "QPS (Requests/sec)"
+	p.Y.Scale = plot.LogScale{}
+	p.Y.Tick.Marker = pow2Ticks{}
+
+	legend := plot.NewLegend()
+
+	values := getSortedValueSizes(records...)
+	for i, rs := range records {
+		rec := make(map[int64][]dataset.DataRecord)
+		for _, r := range rs {
+			rec[r.ValueSize] = append(rec[r.ValueSize], r)
+		}
+		if len(records) > 1 {
+			addValues(p, &legend, values, rec, i, fileNames[i])
+		} else {
+			addValues(p, &legend, values, rec, i, "")
+		}
+	}
+
+	return p, legend
+}
+
+func getSortedValueSizes(records ...[]dataset.DataRecord) []int {
+	valueMap := make(map[int64]struct{})
+	for _, rs := range records {
+		for _, r := range rs {
+			valueMap[r.ValueSize] = struct{}{}
+		}
+	}
+
+	var values []int
+	for v := range valueMap {
+		values = append(values, int(v))
+	}
+	sort.Ints(values)
+
+	return values
+}
+
+func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][]dataset.DataRecord, index int, fileName string) {
+	for i, value := range values {
+		r := rec[int64(value)]
+		readPts := make(plotter.XYs, len(r))
+		writePts := make(plotter.XYs, len(r))
+		for i, record := range r {
+			writePts[i].X = float64(record.ConnSize)
+			readPts[i].X = writePts[i].X
+			readPts[i].Y = record.AvgRead
+			writePts[i].Y = record.AvgWrite
+		}
+
+		readLine, s, err := plotter.NewLinePoints(readPts)
+		if err != nil {
+			panic(err)
+		}
+		if index == 0 {
+			readLine.Color = plotutil.Color(0)
+		} else {
+			readLine.Color = plotutil.Color(2)
+		}
+		readLine.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1))
+		readLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)}
+		s.Color = readLine.Color
+		p.Add(readLine, s)
+
+		writeLine, s, err := plotter.NewLinePoints(writePts)
+		if err != nil {
+			panic(err)
+		}
+		if index == 0 {
+			writeLine.Color = plotutil.Color(0)
+		} else {
+			writeLine.Color = plotutil.Color(2)
+		}
+		writeLine.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1))
+		s.Color = writeLine.Color
+		p.Add(writeLine, s)
+
+		if index == 0 {
+			l, _, _ := plotter.NewLinePoints(writePts)
+			l.Color = color.RGBA{0, 0, 0, 255}
+			l.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1))
+			legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(l))
+		}
+		if i == len(values)-1 {
+			legend.Add(fmt.Sprintf("read %s", fileName), plot.Thumbnailer(readLine))
+			legend.Add(fmt.Sprintf("write %s", fileName), plot.Thumbnailer(writeLine))
+		}
+	}
+}
diff --git a/tools/testgrid-analysis/go.mod b/tools/testgrid-analysis/go.mod
index 078d8759f..773b0545f 100644
--- a/tools/testgrid-analysis/go.mod
+++ b/tools/testgrid-analysis/go.mod
@@ -2,13 +2,13 @@ module go.etcd.io/etcd/tools/testgrid-analysis/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 require (
 	github.com/GoogleCloudPlatform/testgrid v0.0.173
 	github.com/google/go-github/v60 v60.0.0
 	github.com/spf13/cobra v1.9.1
-	google.golang.org/protobuf v1.36.5
+	google.golang.org/protobuf v1.36.6
 )
 
 require (
@@ -16,11 +16,11 @@ require (
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
-	go.opentelemetry.io/otel v1.34.0 // indirect
-	go.opentelemetry.io/otel/sdk v1.34.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/grpc v1.70.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.35.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/grpc v1.72.0 // indirect
 )
diff --git a/tools/testgrid-analysis/go.sum b/tools/testgrid-analysis/go.sum
index 70c5eed0c..9d0e4133f 100644
--- a/tools/testgrid-analysis/go.sum
+++ b/tools/testgrid-analysis/go.sum
@@ -1180,16 +1180,16 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
@@ -1336,8 +1336,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1484,8 +1484,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1516,8 +1516,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1841,8 +1841,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230720185612-659f7aaaa771/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:zBEcrKX2ZOcEkHWxBPAIvYUWOKKMIhYcmNiUIu2ji3I=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1888,8 +1888,8 @@ google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGO
 google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
 google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 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=
@@ -1909,8 +1909,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=



Test patch:
diff --git a/client/v3/client_test.go b/client/v3/client_test.go
index 5841dc020..474a48203 100644
--- a/client/v3/client_test.go
+++ b/client/v3/client_test.go
@@ -317,6 +317,75 @@ func TestAuthTokenBundleNoOverwrite(t *testing.T) {
 	}
 }
 
+func TestNewWithOnlyJWT(t *testing.T) {
+	// This call in particular changes working directory to the tmp dir of
+	// the test. The `etcd-auth-test:1` can be created in local directory,
+	// not exceeding the longest allowed path on OsX.
+	testutil.BeforeTest(t)
+
+	// Create a mock AuthServer to handle Authenticate RPCs.
+	lis, err := net.Listen("unix", "etcd-auth-test:1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer lis.Close()
+	addr := "unix://" + lis.Addr().String()
+	srv := grpc.NewServer()
+	// Having a token removes the need to ever call Authenticate on the
+	// server. If that happens then this will cause a connection failure.
+	etcdserverpb.RegisterAuthServer(srv, mockFailingAuthServer{})
+	go srv.Serve(lis)
+	defer srv.Stop()
+
+	c, err := NewClient(t, Config{
+		DialTimeout: 5 * time.Second,
+		Endpoints:   []string{addr},
+		Token:       "foo",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer c.Close()
+
+	meta, err := c.authTokenBundle.PerRPCCredentials().GetRequestMetadata(context.Background(), "")
+	if err != nil {
+		t.Errorf("Error building request metadata: %s", err)
+	}
+
+	if tok, ok := meta[rpctypes.TokenFieldNameGRPC]; !ok {
+		t.Error("Token was not successfuly set in the auth bundle")
+	} else if tok != "foo" {
+		t.Errorf("Incorrect token set in auth bundle, got '%s', expected 'foo'", tok)
+	}
+}
+
+func TestNewOnlyJWTExclusivity(t *testing.T) {
+	testutil.BeforeTest(t)
+
+	// Create a mock AuthServer to handle Authenticate RPCs.
+	lis, err := net.Listen("unix", "etcd-auth-test:1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer lis.Close()
+	addr := "unix://" + lis.Addr().String()
+	srv := grpc.NewServer()
+	// Having a token removes the need to ever call Authenticate on the
+	// server. If that happens then this will cause a connection failure.
+	etcdserverpb.RegisterAuthServer(srv, mockFailingAuthServer{})
+	go srv.Serve(lis)
+	defer srv.Stop()
+
+	_, err = NewClient(t, Config{
+		DialTimeout: 5 * time.Second,
+		Endpoints:   []string{addr},
+		Token:       "foo",
+		Username:    "user",
+		Password:    "pass",
+	})
+	require.ErrorIs(t, ErrMutuallyExclusiveCfg, err)
+}
+
 func TestSyncFiltersMembers(t *testing.T) {
 	c, _ := NewClient(t, Config{Endpoints: []string{"http://254.0.0.1:12345"}})
 	defer c.Close()
@@ -476,6 +545,14 @@ func (mm mockMaintenance) Downgrade(ctx context.Context, action DowngradeAction,
 	return nil, nil
 }
 
+type mockFailingAuthServer struct {
+	*etcdserverpb.UnimplementedAuthServer
+}
+
+func (mockFailingAuthServer) Authenticate(context.Context, *etcdserverpb.AuthenticateRequest) (*etcdserverpb.AuthenticateResponse, error) {
+	return nil, errors.New("this auth server always fails")
+}
+
 type mockAuthServer struct {
 	*etcdserverpb.UnimplementedAuthServer
 }
diff --git a/client/v3/config_test.go b/client/v3/config_test.go
index 1fe2fb2d3..eb3c8bcaa 100644
--- a/client/v3/config_test.go
+++ b/client/v3/config_test.go
@@ -71,6 +71,25 @@ func TestNewClientConfig(t *testing.T) {
 				Password:             "changeme",
 			},
 		},
+		{
+			name: "JWT specified",
+			spec: ConfigSpec{
+				Endpoints:        []string{"http://192.168.0.12:2379"},
+				DialTimeout:      1 * time.Second,
+				KeepAliveTime:    4 * time.Second,
+				KeepAliveTimeout: 6 * time.Second,
+				Auth: &AuthConfig{
+					Token: "test",
+				},
+			},
+			expectedConf: Config{
+				Endpoints:            []string{"http://192.168.0.12:2379"},
+				DialTimeout:          1 * time.Second,
+				DialKeepAliveTime:    4 * time.Second,
+				DialKeepAliveTimeout: 6 * time.Second,
+				Token:                "test",
+			},
+		},
 		{
 			name: "default secure transport",
 			spec: ConfigSpec{
diff --git a/etcdutl/snapshot/v3_snapshot_test.go b/etcdutl/snapshot/v3_snapshot_test.go
index 1971eefe2..50f14008c 100644
--- a/etcdutl/snapshot/v3_snapshot_test.go
+++ b/etcdutl/snapshot/v3_snapshot_test.go
@@ -15,7 +15,6 @@
 package snapshot
 
 import (
-	"context"
 	"errors"
 	"path/filepath"
 	"strconv"
@@ -136,7 +135,7 @@ func TestSnapshotStatusTotalKey(t *testing.T) {
 							Key:   []byte(key),
 							Value: val,
 						}
-						_, err := srv.Put(context.TODO(), &req)
+						_, err := srv.Put(t.Context(), &req)
 						require.NoError(t, err)
 					}
 				}
@@ -150,10 +149,10 @@ func TestSnapshotStatusTotalKey(t *testing.T) {
 				key := []byte("key1")
 				for i := 0; i < 3; i++ {
 					if i < 2 {
-						_, err := srv.Put(context.TODO(), &etcdserverpb.PutRequest{Key: key, Value: []byte(strconv.Itoa(i))})
+						_, err := srv.Put(t.Context(), &etcdserverpb.PutRequest{Key: key, Value: []byte(strconv.Itoa(i))})
 						require.NoError(t, err)
 					} else {
-						_, err := srv.DeleteRange(context.TODO(), &etcdserverpb.DeleteRangeRequest{Key: key})
+						_, err := srv.DeleteRange(t.Context(), &etcdserverpb.DeleteRangeRequest{Key: key})
 						require.NoError(t, err)
 					}
 				}
@@ -166,9 +165,9 @@ func TestSnapshotStatusTotalKey(t *testing.T) {
 				// key1: create -> delete -> re-create -> delete
 				key := []byte("key1")
 				for i := 0; i < 2; i++ {
-					_, err := srv.Put(context.TODO(), &etcdserverpb.PutRequest{Key: key, Value: make([]byte, 1)})
+					_, err := srv.Put(t.Context(), &etcdserverpb.PutRequest{Key: key, Value: make([]byte, 1)})
 					require.NoError(t, err)
-					_, err = srv.DeleteRange(context.TODO(), &etcdserverpb.DeleteRangeRequest{Key: key})
+					_, err = srv.DeleteRange(t.Context(), &etcdserverpb.DeleteRangeRequest{Key: key})
 					require.NoError(t, err)
 				}
 			},
@@ -181,10 +180,10 @@ func TestSnapshotStatusTotalKey(t *testing.T) {
 				key := []byte("key1")
 				for i := 0; i < 5; i++ {
 					if i%2 == 0 {
-						_, err := srv.Put(context.TODO(), &etcdserverpb.PutRequest{Key: key, Value: make([]byte, 1)})
+						_, err := srv.Put(t.Context(), &etcdserverpb.PutRequest{Key: key, Value: make([]byte, 1)})
 						require.NoError(t, err)
 					} else {
-						_, err := srv.DeleteRange(context.TODO(), &etcdserverpb.DeleteRangeRequest{Key: key})
+						_, err := srv.DeleteRange(t.Context(), &etcdserverpb.DeleteRangeRequest{Key: key})
 						require.NoError(t, err)
 					}
 				}
@@ -195,11 +194,11 @@ func TestSnapshotStatusTotalKey(t *testing.T) {
 			name: "mixed deletions",
 			prepare: func(srv *etcdserver.EtcdServer) {
 				// Put("key1") -> Put("key2")-> Delete("key1")
-				_, err := srv.Put(context.TODO(), &etcdserverpb.PutRequest{Key: []byte("key1"), Value: make([]byte, 1)})
+				_, err := srv.Put(t.Context(), &etcdserverpb.PutRequest{Key: []byte("key1"), Value: make([]byte, 1)})
 				require.NoError(t, err)
-				_, err = srv.Put(context.TODO(), &etcdserverpb.PutRequest{Key: []byte("key2"), Value: make([]byte, 1)})
+				_, err = srv.Put(t.Context(), &etcdserverpb.PutRequest{Key: []byte("key2"), Value: make([]byte, 1)})
 				require.NoError(t, err)
-				_, err = srv.DeleteRange(context.TODO(), &etcdserverpb.DeleteRangeRequest{Key: []byte("key1")})
+				_, err = srv.DeleteRange(t.Context(), &etcdserverpb.DeleteRangeRequest{Key: []byte("key1")})
 				require.NoError(t, err)
 			},
 			expected: 1,
@@ -228,7 +227,7 @@ func insertKeys(t *testing.T, numKeys, valueSize int) func(*etcdserver.EtcdServe
 				Key:   []byte(strconv.Itoa(i)),
 				Value: val,
 			}
-			_, err := srv.Put(context.TODO(), &req)
+			_, err := srv.Put(t.Context(), &req)
 			require.NoError(t, err)
 		}
 	}
diff --git a/pkg/adt/interval_tree_test.go b/pkg/adt/interval_tree_test.go
index 8eb0246ad..012e044e0 100644
--- a/pkg/adt/interval_tree_test.go
+++ b/pkg/adt/interval_tree_test.go
@@ -269,6 +269,36 @@ func TestIntervalTreeDelete(t *testing.T) {
 	require.Truef(t, reflect.DeepEqual(expectedAfterDelete11, visitsAfterDelete11), "level order after deleting '11' expected %v, got %v", expectedAfterDelete11, visitsAfterDelete11)
 }
 
+func TestIntervalTreeFind(t *testing.T) {
+	ivt := NewIntervalTree()
+	ivl1 := NewInt64Interval(3, 6)
+	val := 123
+	assert.Nilf(t, ivt.Find(ivl1), "find for %v expected nil on empty tree", ivl1)
+	// insert interval [3, 6) into tree
+	ivt.Insert(ivl1, val)
+	// check cases of expected find matches and non-matches
+	assert.NotNilf(t, ivt.Find(ivl1), "find expected not-nil on exact-matched interval %v", ivl1)
+	assert.Equalf(t, ivl1, ivt.Find(ivl1).Ivl, "find expected to return exact-matched interval %v", ivl1)
+	ivl2 := NewInt64Interval(3, 7)
+	assert.Nilf(t, ivt.Find(ivl2), "find expected nil on matched start, different end %v", ivl2)
+	ivl3 := NewInt64Interval(2, 6)
+	assert.Nilf(t, ivt.Find(ivl3), "find expected nil on different start, matched end %v", ivl3)
+	ivl4 := NewInt64Interval(10, 20)
+	assert.Nilf(t, ivt.Find(ivl4), "find expected nil on different start, different end %v", ivl4)
+	// insert the additional intervals into the tree, and check they can each be found.
+	ivls := []Interval{ivl2, ivl3, ivl4}
+	for _, ivl := range ivls {
+		ivt.Insert(ivl, val)
+		assert.NotNilf(t, ivt.Find(ivl), "find expected not-nil on exact-matched interval %v", ivl)
+		assert.Equalf(t, ivl, ivt.Find(ivl).Ivl, "find expected to return exact-matched interval %v", ivl)
+	}
+	// check additional intervals no longer found after deletion
+	for _, ivl := range ivls {
+		assert.Truef(t, ivt.Delete(ivl), "expected successful delete on %v", ivl)
+		assert.Nilf(t, ivt.Find(ivl), "find expected nil after deleted interval %v", ivl)
+	}
+}
+
 func TestIntervalTreeIntersects(t *testing.T) {
 	ivt := NewIntervalTree()
 	ivt.Insert(NewStringInterval("1", "3"), 123)
@@ -341,8 +371,14 @@ func TestIntervalTreeRandom(t *testing.T) {
 			require.NotEmptyf(t, ivt.Stab(NewInt64Point(v)), "expected %v stab non-zero for [%+v)", v, xy)
 			require.Truef(t, ivt.Intersects(NewInt64Point(v)), "did not get %d as expected for [%+v)", v, xy)
 		}
-		assert.Truef(t, ivt.Delete(NewInt64Interval(ab.x, ab.y)), "did not delete %v as expected", ab)
+		ivl := NewInt64Interval(ab.x, ab.y)
+		iv := ivt.Find(ivl)
+		assert.NotNilf(t, iv, "expected find non-nil on %v", ab)
+		assert.Equalf(t, ivl, iv.Ivl, "find did not get matched interval %v", ab)
+		assert.Truef(t, ivt.Delete(ivl), "did not delete %v as expected", ab)
 		delete(ivs, ab)
+		ivAfterDel := ivt.Find(ivl)
+		assert.Nilf(t, ivAfterDel, "expected find nil after deletion on %v", ab)
 	}
 
 	assert.Equalf(t, 0, ivt.Len(), "got ivt.Len() = %v, expected 0", ivt.Len())
diff --git a/pkg/featuregate/feature_gate_test.go b/pkg/featuregate/feature_gate_test.go
index 5dc5a8653..d70f20cf9 100644
--- a/pkg/featuregate/feature_gate_test.go
+++ b/pkg/featuregate/feature_gate_test.go
@@ -219,7 +219,7 @@ func TestFeatureGateFlag(t *testing.T) {
 				t.Errorf("%d: Parse() Expected nil, Got %v", i, err)
 			}
 			for k, v := range test.expect {
-				actual := f.enabled.Load().(map[Feature]bool)[k]
+				actual := f.Enabled(k)
 				assert.Equalf(t, actual, v, "%d: expected %s=%v, Got %v", i, k, v, actual)
 			}
 		})
diff --git a/server/embed/config_test.go b/server/embed/config_test.go
index 872d649ea..526b42799 100644
--- a/server/embed/config_test.go
+++ b/server/embed/config_test.go
@@ -111,7 +111,6 @@ func TestConfigFileFeatureGates(t *testing.T) {
 		{
 			name: "default",
 			expectedFeatures: map[featuregate.Feature]bool{
-				features.DistributedTracing:           false,
 				features.StopGRPCServiceOnDefrag:      false,
 				features.InitialCorruptCheck:          false,
 				features.TxnModeWriteWithSharedBuffer: true,
@@ -139,12 +138,12 @@ func TestConfigFileFeatureGates(t *testing.T) {
 		},
 		{
 			name:                                "ok to set different experimental flag and feature gate flag",
-			serverFeatureGatesJSON:              "DistributedTracing=true",
+			serverFeatureGatesJSON:              "InitialCorruptCheck=true",
 			experimentalStopGRPCServiceOnDefrag: "true",
 			expectedFeatures: map[featuregate.Feature]bool{
-				features.DistributedTracing:           true,
 				features.StopGRPCServiceOnDefrag:      true,
 				features.TxnModeWriteWithSharedBuffer: true,
+				features.InitialCorruptCheck:          true,
 			},
 		},
 		{
diff --git a/server/etcdmain/config_test.go b/server/etcdmain/config_test.go
index 9a702260d..56e300bdf 100644
--- a/server/etcdmain/config_test.go
+++ b/server/etcdmain/config_test.go
@@ -480,7 +480,6 @@ func TestParseFeatureGateFlags(t *testing.T) {
 			name: "default",
 			expectedFeatures: map[featuregate.Feature]bool{
 				features.StopGRPCServiceOnDefrag: false,
-				features.DistributedTracing:      false,
 			},
 		},
 		{
@@ -495,11 +494,11 @@ func TestParseFeatureGateFlags(t *testing.T) {
 			name: "ok to set different experimental flag and feature gate flag",
 			args: []string{
 				"--experimental-stop-grpc-service-on-defrag=true",
-				"--feature-gates=DistributedTracing=true",
+				"--feature-gates=InitialCorruptCheck=true",
 			},
 			expectedFeatures: map[featuregate.Feature]bool{
 				features.StopGRPCServiceOnDefrag: true,
-				features.DistributedTracing:      true,
+				features.InitialCorruptCheck:     true,
 			},
 		},
 		{
@@ -509,17 +508,17 @@ func TestParseFeatureGateFlags(t *testing.T) {
 			},
 			expectedFeatures: map[featuregate.Feature]bool{
 				features.StopGRPCServiceOnDefrag: true,
-				features.DistributedTracing:      false,
+				features.InitialCorruptCheck:     false,
 			},
 		},
 		{
 			name: "can set feature gate from feature gate flag",
 			args: []string{
-				"--feature-gates=StopGRPCServiceOnDefrag=true,DistributedTracing=true",
+				"--feature-gates=StopGRPCServiceOnDefrag=true,InitialCorruptCheck=true",
 			},
 			expectedFeatures: map[featuregate.Feature]bool{
 				features.StopGRPCServiceOnDefrag: true,
-				features.DistributedTracing:      true,
+				features.InitialCorruptCheck:     true,
 			},
 		},
 	}
diff --git a/server/etcdserver/api/membership/cluster_test.go b/server/etcdserver/api/membership/cluster_test.go
index c07093878..ff4171946 100644
--- a/server/etcdserver/api/membership/cluster_test.go
+++ b/server/etcdserver/api/membership/cluster_test.go
@@ -277,7 +277,15 @@ func TestClusterValidateAndAssignIDs(t *testing.T) {
 	}
 }
 
+func TestClusterValidateConfigurationChangeV3(t *testing.T) {
+	testClusterValidateConfigurationChange(t, true)
+}
+
 func TestClusterValidateConfigurationChangeV2(t *testing.T) {
+	testClusterValidateConfigurationChange(t, false)
+}
+
+func testClusterValidateConfigurationChange(t *testing.T, shouldApplyV3 ShouldApplyV3) {
 	cl := NewCluster(zaptest.NewLogger(t), WithMaxLearners(1))
 	be := newMembershipBackend()
 	cl.SetBackend(be)
@@ -458,7 +466,7 @@ func TestClusterValidateConfigurationChangeV2(t *testing.T) {
 		},
 	}
 	for i, tt := range tests {
-		err := cl.ValidateConfigurationChange(tt.cc)
+		err := cl.ValidateConfigurationChange(tt.cc, shouldApplyV3)
 		if !errors.Is(err, tt.werr) {
 			t.Errorf("#%d: validateConfigurationChange error = %v, want %v", i, err, tt.werr)
 		}
@@ -652,8 +660,8 @@ func TestNodeToMember(t *testing.T) {
 	}
 }
 
-func newTestCluster(t testing.TB, membs []*Member) *RaftCluster {
-	c := &RaftCluster{lg: zaptest.NewLogger(t), members: make(map[types.ID]*Member), removed: make(map[types.ID]bool)}
+func newTestCluster(tb testing.TB, membs []*Member) *RaftCluster {
+	c := &RaftCluster{lg: zaptest.NewLogger(tb), members: make(map[types.ID]*Member), removed: make(map[types.ID]bool)}
 	for _, m := range membs {
 		c.members[m.ID] = m
 	}
diff --git a/server/etcdserver/api/v2store/store_ttl_test.go b/server/etcdserver/api/v2store/store_ttl_test.go
index a4569cc31..aa98b4505 100644
--- a/server/etcdserver/api/v2store/store_ttl_test.go
+++ b/server/etcdserver/api/v2store/store_ttl_test.go
@@ -132,7 +132,7 @@ func TestStoreUpdateDirTTL(t *testing.T) {
 	assert.False(t, e.Node.Dir)
 	assert.Equal(t, eidx, e.EtcdIndex)
 	e, _ = s.Get("/foo/bar", false, false)
-	assert.Equal(t, "", *e.Node.Value)
+	assert.Empty(t, *e.Node.Value)
 	assert.Equal(t, eidx, e.EtcdIndex)
 
 	fc.Advance(600 * time.Millisecond)
diff --git a/server/etcdserver/api/v3compactor/periodic_test.go b/server/etcdserver/api/v3compactor/periodic_test.go
index 5604dabc5..e8658f420 100644
--- a/server/etcdserver/api/v3compactor/periodic_test.go
+++ b/server/etcdserver/api/v3compactor/periodic_test.go
@@ -16,6 +16,7 @@ package v3compactor
 
 import (
 	"errors"
+	"fmt"
 	"reflect"
 	"testing"
 	"time"
@@ -49,7 +50,7 @@ func TestPeriodicHourly(t *testing.T) {
 	}
 
 	// very first compaction
-	a, err := compactable.Wait(1)
+	a, err := waitWithRetry(t, compactable)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -67,7 +68,7 @@ func TestPeriodicHourly(t *testing.T) {
 			fc.Advance(tb.getRetryInterval())
 		}
 
-		a, err = compactable.Wait(1)
+		a, err = waitWithRetry(t, compactable)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -100,7 +101,7 @@ func TestPeriodicMinutes(t *testing.T) {
 	}
 
 	// very first compaction
-	a, err := compactable.Wait(1)
+	a, err := waitWithRetry(t, compactable)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -117,7 +118,7 @@ func TestPeriodicMinutes(t *testing.T) {
 			fc.Advance(tb.getRetryInterval())
 		}
 
-		a, err := compactable.Wait(1)
+		a, err := waitWithRetry(t, compactable)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -162,7 +163,7 @@ func TestPeriodicPause(t *testing.T) {
 	fc.Advance(tb.getRetryInterval())
 
 	// T=3h6m
-	a, err := compactable.Wait(1)
+	a, err := waitWithRetry(t, compactable)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -197,7 +198,7 @@ func TestPeriodicSkipRevNotChange(t *testing.T) {
 	}
 
 	// very first compaction
-	a, err := compactable.Wait(1)
+	a, err := waitWithRetry(t, compactable)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -228,7 +229,7 @@ func TestPeriodicSkipRevNotChange(t *testing.T) {
 		fc.Advance(tb.getRetryInterval())
 	}
 
-	a, err = compactable.Wait(1)
+	a, err = waitWithRetry(t, compactable)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -244,3 +245,23 @@ func waitOneAction(t *testing.T, r testutil.Recorder) {
 		t.Errorf("expect 1 action, got %v instead", len(actions))
 	}
 }
+
+func waitWithRetry(t *testing.T, compactable *fakeCompactable) ([]testutil.Action, error) {
+	t.Helper()
+
+	var lastErr error
+	var actions []testutil.Action
+
+	expectedActions, maxRetries := 1, 5
+	for retry := 0; retry < maxRetries; retry++ {
+		actions, lastErr = compactable.Wait(expectedActions)
+		if lastErr == nil || len(actions) >= expectedActions {
+			return actions, nil
+		}
+		// Exponential backoff
+		backoffTime := time.Duration(10*(1<<retry)) * time.Millisecond
+		t.Logf("Retry %d: waiting %v before next attempt (last error: %v)", retry+1, backoffTime, lastErr)
+		time.Sleep(backoffTime)
+	}
+	return nil, fmt.Errorf("after %d retries, last error: %w", maxRetries, lastErr)
+}
diff --git a/server/etcdserver/apply/apply_auth_test.go b/server/etcdserver/apply/apply_auth_test.go
index cbd189326..fc87aaec8 100644
--- a/server/etcdserver/apply/apply_auth_test.go
+++ b/server/etcdserver/apply/apply_auth_test.go
@@ -44,7 +44,7 @@ func dummyIndexWaiter(_ uint64) <-chan struct{} {
 	return ch
 }
 
-func dummyApplyFunc(_ *pb.InternalRaftRequest) *Result {
+func dummyApplyFunc(_ *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3) *Result {
 	return &Result{}
 }
 
@@ -98,18 +98,18 @@ func defaultAuthApplierV3(t *testing.T) *authApplierV3 {
 	consistentIndex := cindex.NewConsistentIndex(be)
 	return newAuthApplierV3(
 		authStore,
-		newApplierV3Backend(
-			lg,
-			kv,
-			alarmStore,
-			authStore,
-			lessor,
-			cluster,
-			&fakeRaftStatusGetter{},
-			&fakeSnapshotServer{},
-			consistentIndex,
-			false,
-		),
+		newApplierV3Backend(ApplierOptions{
+			Logger:                       lg,
+			KV:                           kv,
+			AlarmStore:                   alarmStore,
+			ConsistentIndex:              consistentIndex,
+			AuthStore:                    authStore,
+			Lessor:                       lessor,
+			Cluster:                      cluster,
+			RaftStatus:                   &fakeRaftStatusGetter{},
+			SnapshotServer:               &fakeSnapshotServer{},
+			TxnModeWriteWithSharedBuffer: false,
+		}),
 		lessor)
 }
 
@@ -217,7 +217,7 @@ func TestAuthApplierV3_Apply(t *testing.T) {
 	mustCreateRolesAndEnableAuth(t, authApplier)
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			result := authApplier.Apply(tc.request, dummyApplyFunc)
+			result := authApplier.Apply(tc.request, membership.ApplyBoth, dummyApplyFunc)
 			require.Equalf(t, result, tc.expectResult, "Apply: got %v, expect: %v", result, tc.expectResult)
 		})
 	}
@@ -384,7 +384,7 @@ func TestAuthApplierV3_AdminPermission(t *testing.T) {
 			if tc.adminPermissionNeeded {
 				tc.request.Header = &pb.RequestHeader{Username: userReadOnly}
 			}
-			result := authApplier.Apply(tc.request, dummyApplyFunc)
+			result := authApplier.Apply(tc.request, membership.ApplyBoth, dummyApplyFunc)
 			require.Equalf(t, errors.Is(result.Err, auth.ErrPermissionDenied), tc.adminPermissionNeeded, "Admin permission needed")
 		})
 	}
diff --git a/server/etcdserver/apply/uber_applier_test.go b/server/etcdserver/apply/uber_applier_test.go
index 086db23c6..b2e321edb 100644
--- a/server/etcdserver/apply/uber_applier_test.go
+++ b/server/etcdserver/apply/uber_applier_test.go
@@ -60,21 +60,22 @@ func defaultUberApplier(t *testing.T) UberApplier {
 		bcrypt.DefaultCost,
 	)
 	consistentIndex := cindex.NewConsistentIndex(be)
-	return NewUberApplier(
-		lg,
-		be,
-		kv,
-		alarmStore,
-		authStore,
-		lessor,
-		cluster,
-		&fakeRaftStatusGetter{},
-		&fakeSnapshotServer{},
-		consistentIndex,
-		1*time.Hour,
-		false,
-		16*1024*1024, // 16MB
-	)
+	opts := ApplierOptions{
+		Logger:                       lg,
+		KV:                           kv,
+		AlarmStore:                   alarmStore,
+		AuthStore:                    authStore,
+		Lessor:                       lessor,
+		Cluster:                      cluster,
+		RaftStatus:                   &fakeRaftStatusGetter{},
+		SnapshotServer:               &fakeSnapshotServer{},
+		ConsistentIndex:              consistentIndex,
+		TxnModeWriteWithSharedBuffer: false,
+		Backend:                      be,
+		QuotaBackendBytesCfg:         16 * 1024 * 1024, // 16MB
+		WarningApplyDuration:         time.Hour,
+	}
+	return NewUberApplier(opts)
 }
 
 // TestUberApplier_Alarm_Corrupt tests the applier returns ErrCorrupt after alarm CORRUPT is activated
@@ -129,13 +130,13 @@ func TestUberApplier_Alarm_Corrupt(t *testing.T) {
 			MemberID: memberID,
 			Alarm:    pb.AlarmType_CORRUPT,
 		},
-	})
+	}, membership.ApplyBoth)
 	require.NotNil(t, result)
 	require.NoError(t, result.Err)
 
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			result = ua.Apply(tc.request)
+			result = ua.Apply(tc.request, membership.ApplyBoth)
 			require.NotNil(t, result)
 			require.Equalf(t, tc.expectError, result.Err, "Apply: got %v, expect: %v", result.Err, tc.expectError)
 		})
@@ -231,13 +232,13 @@ func TestUberApplier_Alarm_Quota(t *testing.T) {
 			MemberID: memberID,
 			Alarm:    pb.AlarmType_NOSPACE,
 		},
-	})
+	}, membership.ApplyBoth)
 	require.NotNil(t, result)
 	require.NoError(t, result.Err)
 
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			result = ua.Apply(tc.request)
+			result = ua.Apply(tc.request, membership.ApplyBoth)
 			require.NotNil(t, result)
 			require.Equalf(t, tc.expectError, result.Err, "Apply: got %v, expect: %v", result.Err, tc.expectError)
 		})
@@ -254,11 +255,11 @@ func TestUberApplier_Alarm_Deactivate(t *testing.T) {
 			MemberID: memberID,
 			Alarm:    pb.AlarmType_NOSPACE,
 		},
-	})
+	}, membership.ApplyBoth)
 	require.NotNil(t, result)
 	require.NoError(t, result.Err)
 
-	result = ua.Apply(&pb.InternalRaftRequest{Put: &pb.PutRequest{Key: []byte(key)}})
+	result = ua.Apply(&pb.InternalRaftRequest{Put: &pb.PutRequest{Key: []byte(key)}}, membership.ApplyBoth)
 	require.NotNil(t, result)
 	require.Equalf(t, errors.ErrNoSpace, result.Err, "Apply: got %v, expect: %v", result.Err, errors.ErrNoSpace)
 
@@ -269,11 +270,11 @@ func TestUberApplier_Alarm_Deactivate(t *testing.T) {
 			MemberID: memberID,
 			Alarm:    pb.AlarmType_NOSPACE,
 		},
-	})
+	}, membership.ApplyBoth)
 	require.NotNil(t, result)
 	require.NoError(t, result.Err)
 
-	result = ua.Apply(&pb.InternalRaftRequest{Put: &pb.PutRequest{Key: []byte(key)}})
+	result = ua.Apply(&pb.InternalRaftRequest{Put: &pb.PutRequest{Key: []byte(key)}}, membership.ApplyBoth)
 	require.NotNil(t, result)
 	assert.NoError(t, result.Err)
 }
diff --git a/server/etcdserver/server_test.go b/server/etcdserver/server_test.go
index d9caa34e4..feb511d95 100644
--- a/server/etcdserver/server_test.go
+++ b/server/etcdserver/server_test.go
@@ -149,7 +149,7 @@ func TestApplyRepeat(t *testing.T) {
 
 type uberApplierMock struct{}
 
-func (uberApplierMock) Apply(r *pb.InternalRaftRequest) *apply2.Result {
+func (uberApplierMock) Apply(r *pb.InternalRaftRequest, shouldApplyV3 membership.ShouldApplyV3) *apply2.Result {
 	return &apply2.Result{}
 }
 
@@ -1514,12 +1514,12 @@ func (n *nodeConfChangeCommitterRecorder) ApplyConfChange(conf raftpb.ConfChange
 	return &raftpb.ConfState{}
 }
 
-func newTestCluster(t testing.TB) *membership.RaftCluster {
-	return membership.NewCluster(zaptest.NewLogger(t))
+func newTestCluster(tb testing.TB) *membership.RaftCluster {
+	return membership.NewCluster(zaptest.NewLogger(tb))
 }
 
-func newTestClusterWithBackend(t testing.TB, membs []*membership.Member, be backend.Backend) *membership.RaftCluster {
-	lg := zaptest.NewLogger(t)
+func newTestClusterWithBackend(tb testing.TB, membs []*membership.Member, be backend.Backend) *membership.RaftCluster {
+	lg := zaptest.NewLogger(tb)
 	c := membership.NewCluster(lg)
 	c.SetBackend(schema.NewMembershipBackend(lg, be))
 	for _, m := range membs {
diff --git a/server/lease/lessor_bench_test.go b/server/lease/lessor_bench_test.go
index 8e6ff791b..b28ba45c8 100644
--- a/server/lease/lessor_bench_test.go
+++ b/server/lease/lessor_bench_test.go
@@ -60,9 +60,9 @@ func demote(le *lessor) {
 }
 
 // return new lessor and tearDown to release resource
-func setUp(t testing.TB) (le *lessor, tearDown func()) {
+func setUp(tb testing.TB) (le *lessor, tearDown func()) {
 	lg := zap.NewNop()
-	be, _ := betesting.NewDefaultTmpBackend(t)
+	be, _ := betesting.NewDefaultTmpBackend(tb)
 	// MinLeaseTTL is negative, so we can grant expired lease in benchmark.
 	// ExpiredLeasesRetryInterval should small, so benchmark of findExpired will recheck expired lease.
 	le = newLessor(lg, be, nil, LessorConfig{MinLeaseTTL: -1000, ExpiredLeasesRetryInterval: 10 * time.Microsecond})
diff --git a/server/storage/backend/batch_tx_test.go b/server/storage/backend/batch_tx_test.go
index 279f199a5..25af69f11 100644
--- a/server/storage/backend/batch_tx_test.go
+++ b/server/storage/backend/batch_tx_test.go
@@ -393,9 +393,9 @@ func checkUnsafeForEach(t *testing.T, tx backend.UnsafeReader, expectedKeys, exp
 
 // runWriteback is used test the txWriteBuffer.writeback function, which is called inside tx.Unlock().
 // The parameters are chosen based on defaultBatchLimit = 10000
-func runWriteback(t testing.TB, kss, vss [][]string, isSeq bool) {
-	b, _ := betesting.NewTmpBackend(t, time.Hour, 10000)
-	defer betesting.Close(t, b)
+func runWriteback(tb testing.TB, kss, vss [][]string, isSeq bool) {
+	b, _ := betesting.NewTmpBackend(tb, time.Hour, 10000)
+	defer betesting.Close(tb, b)
 
 	tx := b.BatchTx()
 
diff --git a/server/storage/backend/hooks_test.go b/server/storage/backend/hooks_test.go
index 72bc26793..52b3ed8ec 100644
--- a/server/storage/backend/hooks_test.go
+++ b/server/storage/backend/hooks_test.go
@@ -94,11 +94,11 @@ func TestBackendAutoCommitBatchIntervalHook(t *testing.T) {
 	waitUntil(ctx, t, func() bool { return getCommitsKey(t, be) == ">ccc" })
 }
 
-func waitUntil(ctx context.Context, t testing.TB, f func() bool) {
+func waitUntil(ctx context.Context, tb testing.TB, f func() bool) {
 	for !f() {
 		select {
 		case <-ctx.Done():
-			t.Fatalf("Context cancelled/timedout without condition met: %v", ctx.Err())
+			tb.Fatalf("Context cancelled/timedout without condition met: %v", ctx.Err())
 		default:
 		}
 		time.Sleep(10 * time.Millisecond)
@@ -112,28 +112,28 @@ func prepareBuckenAndKey(tx backend.BatchTx) {
 	tx.UnsafePut(bucket, key, []byte(">"))
 }
 
-func newTestHooksBackend(t testing.TB, baseConfig backend.BackendConfig) backend.Backend {
+func newTestHooksBackend(tb testing.TB, baseConfig backend.BackendConfig) backend.Backend {
 	cfg := baseConfig
 	cfg.Hooks = backend.NewHooks(func(tx backend.UnsafeReadWriter) {
 		k, v := tx.UnsafeRange(bucket, key, nil, 1)
-		t.Logf("OnPreCommit executed: %v %v", string(k[0]), string(v[0]))
-		assert.Len(t, k, 1)
-		assert.Len(t, v, 1)
+		tb.Logf("OnPreCommit executed: %v %v", string(k[0]), string(v[0]))
+		assert.Len(tb, k, 1)
+		assert.Len(tb, v, 1)
 		tx.UnsafePut(bucket, key, append(v[0], byte('c')))
 	})
 
-	be, _ := betesting.NewTmpBackendFromCfg(t, cfg)
-	t.Cleanup(func() {
-		betesting.Close(t, be)
+	be, _ := betesting.NewTmpBackendFromCfg(tb, cfg)
+	tb.Cleanup(func() {
+		betesting.Close(tb, be)
 	})
 	return be
 }
 
-func getCommitsKey(t testing.TB, be backend.Backend) string {
+func getCommitsKey(tb testing.TB, be backend.Backend) string {
 	rtx := be.BatchTx()
 	rtx.Lock()
 	defer rtx.Unlock()
 	_, v := rtx.UnsafeRange(bucket, key, nil, 1)
-	assert.Len(t, v, 1)
+	assert.Len(tb, v, 1)
 	return string(v[0])
 }
diff --git a/server/storage/backend/testing/betesting.go b/server/storage/backend/testing/betesting.go
index e42908f93..eae54de9a 100644
--- a/server/storage/backend/testing/betesting.go
+++ b/server/storage/backend/testing/betesting.go
@@ -26,28 +26,28 @@ import (
 	"go.etcd.io/etcd/server/v3/storage/backend"
 )
 
-func NewTmpBackendFromCfg(t testing.TB, bcfg backend.BackendConfig) (backend.Backend, string) {
-	dir, err := os.MkdirTemp(t.TempDir(), "etcd_backend_test")
+func NewTmpBackendFromCfg(tb testing.TB, bcfg backend.BackendConfig) (backend.Backend, string) {
+	dir, err := os.MkdirTemp(tb.TempDir(), "etcd_backend_test")
 	if err != nil {
 		panic(err)
 	}
 	tmpPath := filepath.Join(dir, "database")
 	bcfg.Path = tmpPath
-	bcfg.Logger = zaptest.NewLogger(t)
+	bcfg.Logger = zaptest.NewLogger(tb)
 	return backend.New(bcfg), tmpPath
 }
 
 // NewTmpBackend creates a backend implementation for testing.
-func NewTmpBackend(t testing.TB, batchInterval time.Duration, batchLimit int) (backend.Backend, string) {
-	bcfg := backend.DefaultBackendConfig(zaptest.NewLogger(t))
+func NewTmpBackend(tb testing.TB, batchInterval time.Duration, batchLimit int) (backend.Backend, string) {
+	bcfg := backend.DefaultBackendConfig(zaptest.NewLogger(tb))
 	bcfg.BatchInterval, bcfg.BatchLimit = batchInterval, batchLimit
-	return NewTmpBackendFromCfg(t, bcfg)
+	return NewTmpBackendFromCfg(tb, bcfg)
 }
 
-func NewDefaultTmpBackend(t testing.TB) (backend.Backend, string) {
-	return NewTmpBackendFromCfg(t, backend.DefaultBackendConfig(zaptest.NewLogger(t)))
+func NewDefaultTmpBackend(tb testing.TB) (backend.Backend, string) {
+	return NewTmpBackendFromCfg(tb, backend.DefaultBackendConfig(zaptest.NewLogger(tb)))
 }
 
-func Close(t testing.TB, b backend.Backend) {
-	assert.NoError(t, b.Close())
+func Close(tb testing.TB, b backend.Backend) {
+	assert.NoError(tb, b.Close())
 }
diff --git a/server/storage/mvcc/watchable_store_test.go b/server/storage/mvcc/watchable_store_test.go
index 50de7466a..e888447e0 100644
--- a/server/storage/mvcc/watchable_store_test.go
+++ b/server/storage/mvcc/watchable_store_test.go
@@ -17,10 +17,12 @@ package mvcc
 import (
 	"fmt"
 	"reflect"
+	"strings"
 	"sync"
 	"testing"
 	"time"
 
+	"github.com/prometheus/client_golang/prometheus/testutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/zap/zaptest"
@@ -73,6 +75,175 @@ func TestNewWatcherCancel(t *testing.T) {
 	}
 }
 
+func TestNewWatcherCountGauge(t *testing.T) {
+	expectWatchGauge := func(watchers int) {
+		expected := fmt.Sprintf(`# HELP etcd_debugging_mvcc_watcher_total Total number of watchers.
+# TYPE etcd_debugging_mvcc_watcher_total gauge
+etcd_debugging_mvcc_watcher_total %d
+`, watchers)
+		err := testutil.CollectAndCompare(watcherGauge, strings.NewReader(expected), "etcd_debugging_mvcc_watcher_total")
+		if err != nil {
+			t.Error(err)
+		}
+	}
+
+	t.Run("regular watch", func(t *testing.T) {
+		b, _ := betesting.NewDefaultTmpBackend(t)
+		s := New(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
+		defer cleanup(s, b)
+
+		// watcherGauge is a package variable and its value may change depending on
+		// the execution of other tests
+		initialGaugeState := int(testutil.ToFloat64(watcherGauge))
+
+		testKey := []byte("foo")
+		testValue := []byte("bar")
+		s.Put(testKey, testValue, lease.NoLease)
+
+		// we expect the gauge state to still be in its initial state
+		expectWatchGauge(initialGaugeState)
+
+		w := s.NewWatchStream()
+		defer w.Close()
+
+		wt, _ := w.Watch(0, testKey, nil, 0)
+
+		// after creating watch, the gauge state should have increased
+		expectWatchGauge(initialGaugeState + 1)
+
+		if err := w.Cancel(wt); err != nil {
+			t.Error(err)
+		}
+
+		// after cancelling watch, the gauge state should have decreased
+		expectWatchGauge(initialGaugeState)
+
+		w.Cancel(wt)
+
+		// cancelling the watch twice shouldn't decrement the counter twice
+		expectWatchGauge(initialGaugeState)
+	})
+
+	t.Run("compacted watch", func(t *testing.T) {
+		b, _ := betesting.NewDefaultTmpBackend(t)
+		s := New(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
+		defer cleanup(s, b)
+
+		// watcherGauge is a package variable and its value may change depending on
+		// the execution of other tests
+		initialGaugeState := int(testutil.ToFloat64(watcherGauge))
+
+		testKey := []byte("foo")
+		testValue := []byte("bar")
+
+		s.Put(testKey, testValue, lease.NoLease)
+		rev := s.Put(testKey, testValue, lease.NoLease)
+
+		// compact up to the revision of the key we just put
+		_, err := s.Compact(traceutil.TODO(), rev)
+		if err != nil {
+			t.Error(err)
+		}
+
+		// we expect the gauge state to still be in its initial state
+		expectWatchGauge(initialGaugeState)
+
+		w := s.NewWatchStream()
+		defer w.Close()
+
+		wt, _ := w.Watch(0, testKey, nil, rev-1)
+
+		// wait for the watcher to be marked as compacted
+		select {
+		case resp := <-w.Chan():
+			if resp.CompactRevision == 0 {
+				t.Errorf("resp.Compacted = %v, want %v", resp.CompactRevision, rev)
+			}
+		case <-time.After(time.Second):
+			t.Fatalf("failed to receive response (timeout)")
+		}
+
+		// after creating watch, the gauge state should have increased
+		expectWatchGauge(initialGaugeState + 1)
+
+		if err := w.Cancel(wt); err != nil {
+			t.Error(err)
+		}
+
+		// after cancelling watch, the gauge state should have decreased
+		expectWatchGauge(initialGaugeState)
+
+		w.Cancel(wt)
+
+		// cancelling the watch twice shouldn't decrement the counter twice
+		expectWatchGauge(initialGaugeState)
+	})
+
+	t.Run("compacted watch, close/cancel race", func(t *testing.T) {
+		b, _ := betesting.NewDefaultTmpBackend(t)
+		s := New(zaptest.NewLogger(t), b, &lease.FakeLessor{}, StoreConfig{})
+		defer cleanup(s, b)
+
+		// watcherGauge is a package variable and its value may change depending on
+		// the execution of other tests
+		initialGaugeState := int(testutil.ToFloat64(watcherGauge))
+
+		testKey := []byte("foo")
+		testValue := []byte("bar")
+
+		s.Put(testKey, testValue, lease.NoLease)
+		rev := s.Put(testKey, testValue, lease.NoLease)
+
+		// compact up to the revision of the key we just put
+		_, err := s.Compact(traceutil.TODO(), rev)
+		if err != nil {
+			t.Error(err)
+		}
+
+		// we expect the gauge state to still be in its initial state
+		expectWatchGauge(initialGaugeState)
+
+		w := s.NewWatchStream()
+
+		wt, _ := w.Watch(0, testKey, nil, rev-1)
+
+		// wait for the watcher to be marked as compacted
+		select {
+		case resp := <-w.Chan():
+			if resp.CompactRevision == 0 {
+				t.Errorf("resp.Compacted = %v, want %v", resp.CompactRevision, rev)
+			}
+		case <-time.After(time.Second):
+			t.Fatalf("failed to receive response (timeout)")
+		}
+
+		// after creating watch, the gauge state should have increased
+		expectWatchGauge(initialGaugeState + 1)
+
+		// now race cancelling and closing the watcher and watch stream.
+		// in rare scenarios the watcher cancel function can be invoked
+		// multiple times, leading to a potentially negative gauge state,
+		// see: https://github.com/etcd-io/etcd/issues/19577
+		wg := sync.WaitGroup{}
+		wg.Add(2)
+
+		go func() {
+			w.Cancel(wt)
+			wg.Done()
+		}()
+
+		go func() {
+			w.Close()
+			wg.Done()
+		}()
+
+		wg.Wait()
+
+		// the gauge should be decremented to its original state
+		expectWatchGauge(initialGaugeState)
+	})
+}
+
 // TestCancelUnsynced tests if running CancelFunc removes watchers from unsynced.
 func TestCancelUnsynced(t *testing.T) {
 	b, _ := betesting.NewDefaultTmpBackend(t)
diff --git a/server/storage/wal/testing/waltesting.go b/server/storage/wal/testing/waltesting.go
index bd1dbaade..7613a3f71 100644
--- a/server/storage/wal/testing/waltesting.go
+++ b/server/storage/wal/testing/waltesting.go
@@ -28,32 +28,32 @@ import (
 	"go.etcd.io/raft/v3/raftpb"
 )
 
-func NewTmpWAL(t testing.TB, reqs []etcdserverpb.InternalRaftRequest) (*wal.WAL, string) {
-	t.Helper()
-	dir, err := os.MkdirTemp(t.TempDir(), "etcd_wal_test")
+func NewTmpWAL(tb testing.TB, reqs []etcdserverpb.InternalRaftRequest) (*wal.WAL, string) {
+	tb.Helper()
+	dir, err := os.MkdirTemp(tb.TempDir(), "etcd_wal_test")
 	if err != nil {
 		panic(err)
 	}
 	tmpPath := filepath.Join(dir, "wal")
-	lg := zaptest.NewLogger(t)
+	lg := zaptest.NewLogger(tb)
 	w, err := wal.Create(lg, tmpPath, nil)
 	if err != nil {
-		t.Fatalf("Failed to create WAL: %v", err)
+		tb.Fatalf("Failed to create WAL: %v", err)
 	}
 	err = w.Close()
 	if err != nil {
-		t.Fatalf("Failed to close WAL: %v", err)
+		tb.Fatalf("Failed to close WAL: %v", err)
 	}
 	if len(reqs) != 0 {
 		w, err = wal.Open(lg, tmpPath, walpb.Snapshot{})
 		if err != nil {
-			t.Fatalf("Failed to open WAL: %v", err)
+			tb.Fatalf("Failed to open WAL: %v", err)
 		}
 
 		var state raftpb.HardState
 		_, state, _, err = w.ReadAll()
 		if err != nil {
-			t.Fatalf("Failed to read WAL: %v", err)
+			tb.Fatalf("Failed to read WAL: %v", err)
 		}
 		var entries []raftpb.Entry
 		for _, req := range reqs {
@@ -66,27 +66,27 @@ func NewTmpWAL(t testing.TB, reqs []etcdserverpb.InternalRaftRequest) (*wal.WAL,
 		}
 		err = w.Save(state, entries)
 		if err != nil {
-			t.Fatalf("Failed to save WAL: %v", err)
+			tb.Fatalf("Failed to save WAL: %v", err)
 		}
 		err = w.Close()
 		if err != nil {
-			t.Fatalf("Failed to close WAL: %v", err)
+			tb.Fatalf("Failed to close WAL: %v", err)
 		}
 	}
 
 	w, err = wal.OpenForRead(lg, tmpPath, walpb.Snapshot{})
 	if err != nil {
-		t.Fatalf("Failed to open WAL: %v", err)
+		tb.Fatalf("Failed to open WAL: %v", err)
 	}
 	return w, tmpPath
 }
 
-func Reopen(t testing.TB, walPath string) *wal.WAL {
-	t.Helper()
-	lg := zaptest.NewLogger(t)
+func Reopen(tb testing.TB, walPath string) *wal.WAL {
+	tb.Helper()
+	lg := zaptest.NewLogger(tb)
 	w, err := wal.OpenForRead(lg, walPath, walpb.Snapshot{})
 	if err != nil {
-		t.Fatalf("Failed to open WAL: %v", err)
+		tb.Fatalf("Failed to open WAL: %v", err)
 	}
 	return w
 }
diff --git a/tests/antithesis/Dockerfile.config b/tests/antithesis/Dockerfile.config
new file mode 100644
index 000000000..e34c18321
--- /dev/null
+++ b/tests/antithesis/Dockerfile.config
@@ -0,0 +1,3 @@
+from scratch
+
+COPY docker-compose.yml /docker-compose.yml
diff --git a/tests/antithesis/Makefile b/tests/antithesis/Makefile
new file mode 100644
index 000000000..36ad8b19b
--- /dev/null
+++ b/tests/antithesis/Makefile
@@ -0,0 +1,24 @@
+REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel)
+USER_ID := $(shell id -u)
+GROUP_ID := $(shell id -g)
+
+.PHONY: antithesis-build-client-docker-image
+antithesis-build-client-docker-image:
+	docker build --build-arg GO_VERSION=$(shell cat $(REPOSITORY_ROOT)/.go-version) -f $(REPOSITORY_ROOT)/tests/antithesis/test-template/Dockerfile $(REPOSITORY_ROOT) -t etcd-client:latest
+
+.PHONY: antithesis-docker-compose-up
+antithesis-docker-compose-up:
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker-compose up
+
+.PHONY: antithesis-run-container-tests
+antithesis-run-container-tests:
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker-compose exec client /opt/antithesis/test/v1/robustness/singleton_driver_main
+
+.PHONY: antithesis-run-local-tests
+antithesis-run-local-tests:
+	go run ./test-template/robustness/main.go --local
+
+.PHONY: antithesis-clean
+antithesis-clean:
+	export USER_ID=$(USER_ID) && export GROUP_ID=$(GROUP_ID) && docker-compose down
+	rm -rf /tmp/etcddata0 /tmp/etcddata1 /tmp/etcddata2 /tmp/etcdreport
diff --git a/tests/antithesis/README.md b/tests/antithesis/README.md
new file mode 100644
index 000000000..5834b7db3
--- /dev/null
+++ b/tests/antithesis/README.md
@@ -0,0 +1,97 @@
+This directory enables integration of Antithesis with etcd. There are 4 containers running in this system: 3 that make up an etcd cluster (etcd0, etcd1, etcd2) and one that "[makes the system go](https://antithesis.com/docs/getting_started/basic_test_hookup/)" (client).
+
+## Quickstart
+
+### 1. Build and Tag the Docker Image
+
+Run this command from the `antithesis/test-template` directory:
+
+```bash
+make antithesis-build-client-docker-image
+```
+
+### 2. (Optional) Check the Image Locally
+
+You can verify your new image is built:
+
+```bash
+docker images | grep etcd-client
+```
+
+It should show something like:
+
+```
+etcd-client        latest    <IMAGE_ID>    <DATE>
+```
+
+### 3. Use in Docker Compose
+
+Run the following command from the root directory for Antithesis tests (`tests/antithesis`):
+
+```bash
+make antithesis-docker-compose-up
+```
+
+The client will continuously check the health of the etcd nodes and print logs similar to:
+
+```
+[+] Running 4/4
+ ✔ Container etcd0   Created                                                                                                                                                 0.0s 
+ ✔ Container etcd2   Created                                                                                                                                                 0.0s 
+ ✔ Container etcd1   Created                                                                                                                                                 0.0s 
+ ✔ Container client  Recreated                                                                                                                                               0.1s 
+Attaching to client, etcd0, etcd1, etcd2
+etcd2   | {"level":"info","ts":"2025-04-14T07:23:25.134294Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_ADVERTISE_CLIENT_URLS","variable-value":"http://etcd2.etcd:2379"}
+etcd2   | {"level":"info","ts":"2025-04-14T07:23:25.138501Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_INITIAL_ADVERTISE_PEER_URLS","variable-value":"http://etcd2:2380"}
+etcd2   | {"level":"info","ts":"2025-04-14T07:23:25.138646Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_INITIAL_CLUSTER","variable-value":"etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"}
+etcd0   | {"level":"info","ts":"2025-04-14T07:23:25.138434Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_ADVERTISE_CLIENT_URLS","variable-value":"http://etcd0.etcd:2379"}
+etcd0   | {"level":"info","ts":"2025-04-14T07:23:25.138582Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_INITIAL_ADVERTISE_PEER_URLS","variable-value":"http://etcd0:2380"}
+etcd0   | {"level":"info","ts":"2025-04-14T07:23:25.138592Z","caller":"flags/flag.go:113","msg":"recognized and used environment variable","variable-name":"ETCD_INITIAL_CLUSTER","variable-value":"etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"}
+
+...
+...
+(skipping some repeated logs for brevity)
+...
+...
+
+etcd2   | {"level":"info","ts":"2025-04-14T07:23:25.484698Z","caller":"etcdmain/main.go:50","msg":"successfully notified init daemon"}
+etcd1   | {"level":"info","ts":"2025-04-14T07:23:25.484092Z","caller":"embed/serve.go:210","msg":"serving client traffic insecurely; this is strongly discouraged!","traffic":"grpc+http","address":"[::]:2379"}
+etcd0   | {"level":"info","ts":"2025-04-14T07:23:25.484563Z","caller":"etcdmain/main.go:50","msg":"successfully notified init daemon"}
+etcd2   | {"level":"info","ts":"2025-04-14T07:23:25.485101Z","caller":"v3rpc/health.go:61","msg":"grpc service status changed","service":"","status":"SERVING"}
+etcd1   | {"level":"info","ts":"2025-04-14T07:23:25.484130Z","caller":"etcdmain/main.go:44","msg":"notifying init daemon"}
+etcd2   | {"level":"info","ts":"2025-04-14T07:23:25.485782Z","caller":"embed/serve.go:210","msg":"serving client traffic insecurely; this is strongly discouraged!","traffic":"grpc+http","address":"[::]:2379"}
+etcd1   | {"level":"info","ts":"2025-04-14T07:23:25.484198Z","caller":"etcdmain/main.go:50","msg":"successfully notified init daemon"}
+client  | Client [entrypoint]: starting...
+client  | Client [entrypoint]: checking cluster health...
+client  | Client [entrypoint]: connection successful with etcd0
+client  | Client [entrypoint]: connection successful with etcd1
+client  | Client [entrypoint]: connection successful with etcd2
+client  | Client [entrypoint]: cluster is healthy!
+```
+
+And it will stay running indefinitely.
+
+### 4. Running the tests
+
+```bash
+make antithesis-run-container-tests
+```
+
+Alternatively, with the etcd cluster from step 3, to run the tests locally without rebuilding the client image:
+
+```bash
+make antithesis-run-local-tests
+```
+
+### 5. Prepare for next run
+
+Unfortunatelly robustness tests don't support running on non empty database.
+So for now you need to cleanup the storage before repeating the run or you will get "non empty database at start, required by model used for linearizability validation" error.
+
+```bash
+make antithesis-clean
+```
+
+## Troubleshooting
+
+- **Image Pull Errors**: If Docker can’t pull `etcd-client:latest`, make sure you built it locally (see the “Build and Tag” step) or push it to a registry that Compose can access.
diff --git a/tests/antithesis/docker-compose.yml b/tests/antithesis/docker-compose.yml
new file mode 100644
index 000000000..9b8eb0628
--- /dev/null
+++ b/tests/antithesis/docker-compose.yml
@@ -0,0 +1,103 @@
+---
+services:
+  # This is needed for creating non-root data folders on host.
+  # By default, if the folders don't exist when mounting, compose creates them with root as owner.
+  # With root owner, accessing the WAL files from local tests will fail due to an unauthorized access error.
+  init:
+    image: 'docker.io/library/ubuntu:latest'
+    user: root
+    group_add:
+      - '${GROUP_ID:-root}'
+    volumes:
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}0:/var/etcddata0
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}1:/var/etcddata1
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}2:/var/etcddata2
+      - ${ETCD_ROBUSTNESS_REPORT_PATH:-/tmp/etcdreport}:/var/report
+    command: chown -R ${USER_ID:-root}:${GROUP_ID:-root} /var/etcddata0 /var/etcddata1 /var/etcddata2 /var/report
+
+  etcd0:
+    image: 'gcr.io/etcd-development/etcd:v3.5.21'
+    container_name: etcd0
+    hostname: etcd0
+    environment:
+      ETCD_NAME: "etcd0"
+      ETCD_INITIAL_ADVERTISE_PEER_URLS: "http://etcd0:2380"
+      ETCD_LISTEN_PEER_URLS: "http://0.0.0.0:2380"
+      ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
+      ETCD_ADVERTISE_CLIENT_URLS: "http://etcd0.etcd:2379"
+      ETCD_INITIAL_CLUSTER_TOKEN: "etcd-cluster-1"
+      ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
+      ETCD_INITIAL_CLUSTER_STATE: "new"
+      ETCD_DATA_DIR: "/var/etcd/data"
+    user: "${USER_ID:-root}:${GROUP_ID:-root}"
+    depends_on:
+      init:
+        condition: service_completed_successfully
+    ports:
+      - 12379:2379
+    volumes:
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}0:/var/etcd/data
+
+  etcd1:
+    image: 'gcr.io/etcd-development/etcd:v3.5.21'
+    container_name: etcd1
+    hostname: etcd1
+    environment:
+      ETCD_NAME: "etcd1"
+      ETCD_INITIAL_ADVERTISE_PEER_URLS: "http://etcd1:2380"
+      ETCD_LISTEN_PEER_URLS: "http://0.0.0.0:2380"
+      ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
+      ETCD_ADVERTISE_CLIENT_URLS: "http://etcd1.etcd:2379"
+      ETCD_INITIAL_CLUSTER_TOKEN: "etcd-cluster-1"
+      ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
+      ETCD_INITIAL_CLUSTER_STATE: "new"
+      ETCD_DATA_DIR: "/var/etcd/data"
+    user: "${USER_ID:-root}:${GROUP_ID:-root}"
+    depends_on:
+      init:
+        condition: service_completed_successfully
+    ports:
+      - 22379:2379
+    volumes:
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}1:/var/etcd/data
+
+  etcd2:
+    image: 'gcr.io/etcd-development/etcd:v3.5.21'
+    container_name: etcd2
+    hostname: etcd2
+    environment:
+      ETCD_NAME: "etcd2"
+      ETCD_INITIAL_ADVERTISE_PEER_URLS: "http://etcd2:2380"
+      ETCD_LISTEN_PEER_URLS: "http://0.0.0.0:2380"
+      ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
+      ETCD_ADVERTISE_CLIENT_URLS: "http://etcd2.etcd:2379"
+      ETCD_INITIAL_CLUSTER_TOKEN: "etcd-cluster-1"
+      ETCD_INITIAL_CLUSTER: "etcd0=http://etcd0:2380,etcd1=http://etcd1:2380,etcd2=http://etcd2:2380"
+      ETCD_INITIAL_CLUSTER_STATE: "new"
+      ETCD_DATA_DIR: "/var/etcd/data"
+    user: "${USER_ID:-root}:${GROUP_ID:-root}"
+    depends_on:
+      init:
+        condition: service_completed_successfully
+    ports:
+      - 32379:2379
+    volumes:
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}2:/var/etcd/data
+
+  client:
+    image: 'etcd-client:latest'
+    container_name: client
+    entrypoint: ["/opt/antithesis/entrypoint/entrypoint"]
+    user: "${USER_ID:-root}:${GROUP_ID:-root}"
+    depends_on:
+      etcd0:
+        condition: service_started
+      etcd1:
+        condition: service_started
+      etcd2:
+        condition: service_started
+    volumes:
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}0:/var/etcddata0
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}1:/var/etcddata1
+      - ${ETCD_ROBUSTNESS_DATA_PATH:-/tmp/etcddata}2:/var/etcddata2
+      - ${ETCD_ROBUSTNESS_REPORT_PATH:-/tmp/etcdreport}:/var/report
diff --git a/tests/antithesis/test-template/Dockerfile b/tests/antithesis/test-template/Dockerfile
new file mode 100644
index 000000000..a5a4a3d58
--- /dev/null
+++ b/tests/antithesis/test-template/Dockerfile
@@ -0,0 +1,10 @@
+ARG GO_VERSION=1.24.3
+ARG ARCH=amd64
+
+FROM golang:$GO_VERSION
+WORKDIR /build
+COPY . .
+
+WORKDIR /build/tests
+RUN go build -o /opt/antithesis/entrypoint/entrypoint -race ./antithesis/test-template/entrypoint/main.go
+RUN go build -o /opt/antithesis/test/v1/robustness/singleton_driver_main -race ./antithesis/test-template/robustness/main.go
diff --git a/tests/antithesis/test-template/entrypoint/main.go b/tests/antithesis/test-template/entrypoint/main.go
new file mode 100644
index 000000000..cf2f46f75
--- /dev/null
+++ b/tests/antithesis/test-template/entrypoint/main.go
@@ -0,0 +1,91 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build cgo && amd64
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/antithesishq/antithesis-sdk-go/lifecycle"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+// Sleep duration
+const SLEEP = 10
+
+// CheckHealth checks health of all etcd nodes
+func CheckHealth() bool {
+	nodeOptions := []string{"etcd0", "etcd1", "etcd2"}
+
+	// iterate over each node and check health
+	for _, node := range nodeOptions {
+		cli, err := clientv3.New(clientv3.Config{
+			Endpoints:   []string{fmt.Sprintf("http://%s:2379", node)},
+			DialTimeout: 5 * time.Second,
+		})
+		if err != nil {
+			fmt.Printf("Client [entrypoint]: connection failed with %s\n", node)
+			fmt.Printf("Client [entrypoint]: error: %v\n", err)
+			return false
+		}
+
+		defer func() {
+			cErr := cli.Close()
+			if cErr != nil {
+				fmt.Printf("Client [entrypoint]: error closing connection: %v\n", cErr)
+			}
+		}()
+
+		// fetch the key setting-up to confirm that the node is available
+		_, err = cli.Get(context.Background(), "setting-up")
+		if err != nil {
+			fmt.Printf("Client [entrypoint]: connection failed with %s\n", node)
+			fmt.Printf("Client [entrypoint]: error: %v\n", err)
+			return false
+		}
+
+		fmt.Printf("Client [entrypoint]: connection successful with %s\n", node)
+	}
+
+	return true
+}
+
+func main() {
+	fmt.Println("Client [entrypoint]: starting...")
+
+	// run loop until all nodes are healthy
+	for {
+		fmt.Println("Client [entrypoint]: checking cluster health...")
+		if CheckHealth() {
+			fmt.Println("Client [entrypoint]: cluster is healthy!")
+			break
+		}
+		fmt.Printf("Client [entrypoint]: cluster is not healthy. retrying in %d seconds...\n", SLEEP)
+		time.Sleep(SLEEP * time.Second)
+	}
+
+	// signal that the setup looks complete
+	lifecycle.SetupComplete(
+		map[string]string{
+			"Message": "ETCD cluster is healthy",
+		},
+	)
+
+	select {}
+}
diff --git a/tests/antithesis/test-template/robustness/main.go b/tests/antithesis/test-template/robustness/main.go
new file mode 100644
index 000000000..8d3edde03
--- /dev/null
+++ b/tests/antithesis/test-template/robustness/main.go
@@ -0,0 +1,188 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build cgo && amd64
+
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"os"
+	"sync"
+	"time"
+
+	"github.com/anishathalye/porcupine"
+	"github.com/antithesishq/antithesis-sdk-go/assert"
+	"go.uber.org/zap"
+	"golang.org/x/time/rate"
+
+	"go.etcd.io/etcd/tests/v3/robustness/client"
+	"go.etcd.io/etcd/tests/v3/robustness/identity"
+	robustnessrand "go.etcd.io/etcd/tests/v3/robustness/random"
+	"go.etcd.io/etcd/tests/v3/robustness/report"
+	"go.etcd.io/etcd/tests/v3/robustness/traffic"
+	"go.etcd.io/etcd/tests/v3/robustness/validate"
+)
+
+var profile = traffic.Profile{
+	MinimalQPS:                     100,
+	MaximalQPS:                     1000,
+	BurstableQPS:                   1000,
+	ClientCount:                    3,
+	MaxNonUniqueRequestConcurrency: 3,
+}
+
+const (
+	defaultetcd0 = "etcd0:2379"
+	defaultetcd1 = "etcd1:2379"
+	defaultetcd2 = "etcd0:2379"
+
+	localetcd0 = "127.0.0.1:12379"
+	localetcd1 = "127.0.0.1:22379"
+	localetcd2 = "127.0.0.1:32379"
+	// mounted by the client in docker compose
+	defaultEtcdDataPath = "/var/etcddata%d"
+	// used by default when running the client locally
+	defaultLocalEtcdDataPath = "/tmp/etcddata%d"
+	localEtcdDataPathEnv     = "ETCD_ROBUSTNESS_DATA_PATH"
+
+	defaultReportPath = "/var/report/history.html"
+	localReportPath   = "history.html"
+)
+
+func main() {
+	local := flag.Bool("local", false, "run tests locally and connect to etcd instances via localhost")
+	flag.Parse()
+
+	etcdDataPath := defaultEtcdDataPath
+	hosts := []string{defaultetcd0, defaultetcd1, defaultetcd2}
+	reportPath := defaultReportPath
+	if *local {
+		hosts = []string{localetcd0, localetcd1, localetcd2}
+		etcdDataPath = defaultLocalEtcdDataPath
+		localpath := os.Getenv(localEtcdDataPathEnv)
+		if localpath != "" {
+			etcdDataPath = localpath
+		}
+		reportPath = localReportPath
+	}
+
+	persistedRequestdirs := []string{
+		fmt.Sprintf(etcdDataPath, 0),
+		fmt.Sprintf(etcdDataPath, 1),
+		fmt.Sprintf(etcdDataPath, 2),
+	}
+
+	ctx := context.Background()
+	baseTime := time.Now()
+	duration := time.Duration(robustnessrand.RandRange(5, 15) * int64(time.Second))
+	testRobustness(ctx, hosts, persistedRequestdirs, reportPath, baseTime, duration)
+}
+
+func testRobustness(ctx context.Context, hosts, persistedRequestdirs []string, reportPath string, baseTime time.Time, duration time.Duration) {
+	lg, err := zap.NewProduction()
+	if err != nil {
+		panic(err)
+	}
+	lg.Info("Start traffic generation", zap.Duration("duration", duration))
+	reports := runTraffic(ctx, lg, hosts, baseTime, duration)
+	lg.Info("Completed traffic generation")
+
+	etcdDataDirs, err := report.PersistedRequestsDirs(lg, persistedRequestdirs)
+	if err != nil {
+		lg.Error("Failed to create PersistedRequestdirs to access etcd WAL files")
+		panic(err)
+	}
+	validateConfig := validate.Config{ExpectRevisionUnique: traffic.EtcdAntithesis.ExpectUniqueRevision()}
+	result, err := validate.ValidateAndReturnVisualize(lg, validateConfig, reports, etcdDataDirs, 5*time.Minute)
+	if err != nil {
+		lg.Info("Validation error", zap.Error(err))
+		assert.Unreachable("Validation error", map[string]any{"error": err})
+		return
+	}
+	if result.Linearization.Linearizable == porcupine.Unknown {
+		assert.Unreachable("Linearization timeout", nil)
+	} else {
+		assert.Always(result.Linearization.Linearizable == porcupine.Ok, "Linearization validation passes", nil)
+	}
+	err = result.Linearization.Visualize(lg, reportPath)
+	if err != nil {
+		lg.Error("Failed to save visualization", zap.Error(err))
+	}
+	assert.Always(result.WatchError == nil, "Watch validation passes", map[string]any{"error": result.WatchError})
+	assert.Always(result.SerializableError == nil, "Serializable validation passes", map[string]any{"error": result.WatchError})
+	lg.Info("Completed robustness validation")
+}
+
+func runTraffic(ctx context.Context, lg *zap.Logger, hosts []string, baseTime time.Time, duration time.Duration) []report.ClientReport {
+	limiter := rate.NewLimiter(rate.Limit(profile.MaximalQPS), profile.BurstableQPS)
+	ids := identity.NewIDProvider()
+	storage := identity.NewLeaseIDStorage()
+	concurrencyLimiter := traffic.NewConcurrencyLimiter(profile.MaxNonUniqueRequestConcurrency)
+
+	r, err := traffic.CheckEmptyDatabaseAtStart(ctx, lg, hosts, ids, baseTime)
+	if err != nil {
+		lg.Fatal("Failed empty database at start check", zap.Error(err))
+	}
+
+	finish := closeAfter(ctx, duration)
+	reports := []report.ClientReport{r}
+	var mux sync.Mutex
+	var wg sync.WaitGroup
+	for i := 0; i < profile.ClientCount; i++ {
+		c := connect([]string{hosts[i%len(hosts)]}, ids, baseTime)
+		defer c.Close()
+		wg.Add(1)
+		go func(c *client.RecordingClient) {
+			defer wg.Done()
+			defer c.Close()
+
+			traffic.EtcdAntithesis.RunTrafficLoop(ctx, c, limiter,
+				ids,
+				storage,
+				concurrencyLimiter,
+				finish,
+			)
+			mux.Lock()
+			reports = append(reports, c.Report())
+			mux.Unlock()
+		}(c)
+	}
+	wg.Wait()
+	return reports
+}
+
+func connect(endpoints []string, ids identity.Provider, baseTime time.Time) *client.RecordingClient {
+	cli, err := client.NewRecordingClient(endpoints, ids, baseTime)
+	if err != nil {
+		// Antithesis Assertion: client should always be able to connect to an etcd host
+		assert.Unreachable("Client failed to connect to an etcd host", map[string]any{"endpoints": endpoints, "error": err})
+		os.Exit(1)
+	}
+	return cli
+}
+
+func closeAfter(ctx context.Context, t time.Duration) <-chan struct{} {
+	out := make(chan struct{})
+	go func() {
+		select {
+		case <-time.After(t):
+		case <-ctx.Done():
+		}
+		close(out)
+	}()
+	return out
+}
diff --git a/tests/common/alarm_test.go b/tests/common/alarm_test.go
index 67d83ddf5..af47316b6 100644
--- a/tests/common/alarm_test.go
+++ b/tests/common/alarm_test.go
@@ -30,7 +30,7 @@ import (
 
 func TestAlarm(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t,
 		config.WithClusterSize(1),
@@ -97,7 +97,7 @@ func TestAlarm(t *testing.T) {
 
 func TestAlarmlistOnMemberRestart(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t,
 		config.WithClusterSize(1),
diff --git a/tests/common/auth_test.go b/tests/common/auth_test.go
index 7031bbfe0..fb995fa3c 100644
--- a/tests/common/auth_test.go
+++ b/tests/common/auth_test.go
@@ -33,6 +33,9 @@ var (
 	tokenTTL         = time.Second * 3
 	defaultAuthToken = fmt.Sprintf("jwt,pub-key=%s,priv-key=%s,sign-method=RS256,ttl=%s",
 		mustAbsPath("../fixtures/server.crt"), mustAbsPath("../fixtures/server.key.insecure"), tokenTTL)
+	defaultKeyPath    = mustAbsPath("../fixtures/server.key.insecure")
+	verifyJWTOnlyAuth = fmt.Sprintf("jwt,pub-key=%s,sign-method=RS256,ttl=%s",
+		mustAbsPath("../fixtures/server.crt"), tokenTTL)
 )
 
 const (
@@ -45,7 +48,7 @@ const (
 
 func TestAuthEnable(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -57,7 +60,7 @@ func TestAuthEnable(t *testing.T) {
 
 func TestAuthDisable(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -87,7 +90,7 @@ func TestAuthDisable(t *testing.T) {
 
 func TestAuthGracefulDisable(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -137,7 +140,7 @@ func TestAuthGracefulDisable(t *testing.T) {
 
 func TestAuthStatus(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -157,7 +160,7 @@ func TestAuthStatus(t *testing.T) {
 
 func TestAuthRoleUpdate(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -197,7 +200,7 @@ func TestAuthRoleUpdate(t *testing.T) {
 
 func TestAuthUserDeleteDuringOps(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -228,7 +231,7 @@ func TestAuthUserDeleteDuringOps(t *testing.T) {
 
 func TestAuthRoleRevokeDuringOps(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -285,7 +288,7 @@ func TestAuthRoleRevokeDuringOps(t *testing.T) {
 
 func TestAuthWriteKey(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -375,7 +378,7 @@ func TestAuthTxn(t *testing.T) {
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
 			testRunner.BeforeTest(t)
-			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.cfg))
 			defer clus.Close()
@@ -421,7 +424,7 @@ func TestAuthTxn(t *testing.T) {
 
 func TestAuthPrefixPerm(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -455,7 +458,7 @@ func TestAuthPrefixPerm(t *testing.T) {
 
 func TestAuthLeaseKeepAlive(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -481,7 +484,7 @@ func TestAuthLeaseKeepAlive(t *testing.T) {
 
 func TestAuthRevokeWithDelete(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -512,7 +515,7 @@ func TestAuthRevokeWithDelete(t *testing.T) {
 
 func TestAuthLeaseTimeToLiveExpired(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -550,7 +553,7 @@ func TestAuthLeaseGrantLeases(t *testing.T) {
 	}
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -575,7 +578,7 @@ func TestAuthLeaseGrantLeases(t *testing.T) {
 
 func TestAuthMemberAdd(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -593,7 +596,7 @@ func TestAuthMemberAdd(t *testing.T) {
 
 func TestAuthMemberRemove(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clusterSize := 3
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: clusterSize}))
@@ -633,7 +636,7 @@ func TestAuthMemberRemove(t *testing.T) {
 
 func TestAuthTestInvalidMgmt(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -650,7 +653,7 @@ func TestAuthTestInvalidMgmt(t *testing.T) {
 
 func TestAuthLeaseRevoke(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -674,7 +677,7 @@ func TestAuthLeaseRevoke(t *testing.T) {
 
 func TestAuthRoleGet(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -700,7 +703,7 @@ func TestAuthRoleGet(t *testing.T) {
 
 func TestAuthUserGet(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -726,7 +729,7 @@ func TestAuthUserGet(t *testing.T) {
 
 func TestAuthRoleList(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1}))
 	defer clus.Close()
@@ -743,7 +746,7 @@ func TestAuthRoleList(t *testing.T) {
 
 func TestAuthJWTExpire(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken}))
 	defer clus.Close()
@@ -762,10 +765,29 @@ func TestAuthJWTExpire(t *testing.T) {
 	})
 }
 
+func TestAuthJWTOnly(t *testing.T) {
+	testRunner.BeforeTest(t)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: verifyJWTOnlyAuth}))
+	defer clus.Close()
+	cc := testutils.MustClient(clus.Client())
+	testutils.ExecuteUntil(ctx, t, func() {
+		authRev, err := setupAuthAndGetRevision(cc, []authRole{testRole}, []authUser{rootUser, testUser})
+		require.NoErrorf(t, err, "failed to enable auth")
+
+		token, err := createSignedJWT(defaultKeyPath, "RS256", testUserName, authRev)
+		require.NoErrorf(t, err, "failed to create test user JWT")
+
+		testUserAuthClient := testutils.MustClient(clus.Client(WithAuthToken(token)))
+		require.NoError(t, testUserAuthClient.Put(ctx, "foo", "bar", config.PutOptions{}))
+	})
+}
+
 // TestAuthRevisionConsistency ensures auth revision is the same after member restarts
 func TestAuthRevisionConsistency(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken}))
 	defer clus.Close()
@@ -801,7 +823,7 @@ func TestAuthRevisionConsistency(t *testing.T) {
 // TestAuthTestCacheReload ensures permissions are persisted and will be reloaded after member restarts
 func TestAuthTestCacheReload(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken}))
 	defer clus.Close()
@@ -826,7 +848,7 @@ func TestAuthTestCacheReload(t *testing.T) {
 // TestAuthLeaseTimeToLive gated lease time to live with RBAC control
 func TestAuthLeaseTimeToLive(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(config.ClusterConfig{ClusterSize: 1, AuthToken: defaultAuthToken}))
 	defer clus.Close()
diff --git a/tests/common/auth_util.go b/tests/common/auth_util.go
index b157ef4fc..6b347f979 100644
--- a/tests/common/auth_util.go
+++ b/tests/common/auth_util.go
@@ -17,8 +17,11 @@ package common
 import (
 	"context"
 	"fmt"
+	"os"
 	"testing"
+	"time"
 
+	"github.com/golang-jwt/jwt/v5"
 	"github.com/stretchr/testify/require"
 
 	"go.etcd.io/etcd/api/v3/authpb"
@@ -93,6 +96,29 @@ func createUsers(c interfaces.Client, users []authUser) error {
 	return nil
 }
 
+func createSignedJWT(keyPath, alg, username string, authRevision uint64) (string, error) {
+	signMethod := jwt.GetSigningMethod(alg)
+
+	keyBytes, err := os.ReadFile(keyPath)
+	if err != nil {
+		return "", err
+	}
+
+	key, err := jwt.ParseRSAPrivateKeyFromPEM(keyBytes)
+	if err != nil {
+		return "", err
+	}
+
+	tk := jwt.NewWithClaims(signMethod,
+		jwt.MapClaims{
+			"username": username,
+			"revision": authRevision,
+			"exp":      time.Now().Add(time.Minute).Unix(),
+		})
+
+	return tk.SignedString(key)
+}
+
 func setupAuth(c interfaces.Client, roles []authRole, users []authUser) error {
 	// create roles
 	if err := createRoles(c, roles); err != nil {
@@ -107,6 +133,29 @@ func setupAuth(c interfaces.Client, roles []authRole, users []authUser) error {
 	return c.AuthEnable(context.TODO())
 }
 
+func setupAuthAndGetRevision(c interfaces.Client, roles []authRole, users []authUser) (uint64, error) {
+	// create roles
+	if err := createRoles(c, roles); err != nil {
+		return 0, err
+	}
+
+	if err := createUsers(c, users); err != nil {
+		return 0, err
+	}
+
+	// This needs to happen before enabling auth for the TestAuthJWTOnly
+	// test case because once auth is enabled we can no longer mint a valid
+	// auth token without the revision, which we won't be able to obtain
+	// without a valid auth token.
+	authrev, err := c.AuthStatus(context.TODO())
+	if err != nil {
+		return 0, err
+	}
+
+	// enable auth
+	return authrev.AuthRevision, c.AuthEnable(context.TODO())
+}
+
 func requireRolePermissionEqual(t *testing.T, expectRole authRole, actual []*authpb.Permission) {
 	require.Len(t, actual, 1)
 	require.Equal(t, expectRole.permission, clientv3.PermissionType(actual[0].PermType))
diff --git a/tests/common/compact_test.go b/tests/common/compact_test.go
index 412c46215..ff83cbe22 100644
--- a/tests/common/compact_test.go
+++ b/tests/common/compact_test.go
@@ -43,7 +43,7 @@ func TestCompact(t *testing.T) {
 	}
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t)
 			defer clus.Close()
diff --git a/tests/common/defrag_test.go b/tests/common/defrag_test.go
index d0af0e68c..6ad5bba79 100644
--- a/tests/common/defrag_test.go
+++ b/tests/common/defrag_test.go
@@ -27,7 +27,7 @@ import (
 
 func TestDefragOnline(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	options := config.DefragOption{Timeout: 10 * time.Second}
 	clus := testRunner.NewCluster(ctx, t)
diff --git a/tests/common/e2e_test.go b/tests/common/e2e_test.go
index 11c4f94a3..e9e2f1921 100644
--- a/tests/common/e2e_test.go
+++ b/tests/common/e2e_test.go
@@ -78,6 +78,10 @@ func WithAuth(userName, password string) config.ClientOption {
 	return e2e.WithAuth(userName, password)
 }
 
+func WithAuthToken(token string) config.ClientOption {
+	return e2e.WithAuthToken(token)
+}
+
 func WithEndpoints(endpoints []string) config.ClientOption {
 	return e2e.WithEndpoints(endpoints)
 }
diff --git a/tests/common/endpoint_test.go b/tests/common/endpoint_test.go
index 570b59d7f..5681e54aa 100644
--- a/tests/common/endpoint_test.go
+++ b/tests/common/endpoint_test.go
@@ -28,7 +28,7 @@ import (
 
 func TestEndpointStatus(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t)
 	defer clus.Close()
@@ -41,7 +41,7 @@ func TestEndpointStatus(t *testing.T) {
 
 func TestEndpointHashKV(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t)
 	defer clus.Close()
@@ -72,7 +72,7 @@ func TestEndpointHashKV(t *testing.T) {
 
 func TestEndpointHealth(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t)
 	defer clus.Close()
diff --git a/tests/common/integration_test.go b/tests/common/integration_test.go
index c4cabeeb1..f41055c44 100644
--- a/tests/common/integration_test.go
+++ b/tests/common/integration_test.go
@@ -56,6 +56,10 @@ func WithAuth(userName, password string) config.ClientOption {
 	return integration.WithAuth(userName, password)
 }
 
+func WithAuthToken(token string) config.ClientOption {
+	return integration.WithAuthToken(token)
+}
+
 func WithEndpoints(endpoints []string) config.ClientOption {
 	return integration.WithEndpoints(endpoints)
 }
diff --git a/tests/common/kv_test.go b/tests/common/kv_test.go
index 593ea1e19..323d263db 100644
--- a/tests/common/kv_test.go
+++ b/tests/common/kv_test.go
@@ -31,7 +31,7 @@ func TestKVPut(t *testing.T) {
 	testRunner.BeforeTest(t)
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -55,7 +55,7 @@ func TestKVGet(t *testing.T) {
 	testRunner.BeforeTest(t)
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -111,7 +111,7 @@ func TestKVDelete(t *testing.T) {
 	testRunner.BeforeTest(t)
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -201,7 +201,7 @@ func TestKVGetNoQuorum(t *testing.T) {
 	}
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t)
 			defer clus.Close()
diff --git a/tests/common/lease_test.go b/tests/common/lease_test.go
index 1cebc5775..4d6c136f2 100644
--- a/tests/common/lease_test.go
+++ b/tests/common/lease_test.go
@@ -31,7 +31,7 @@ func TestLeaseGrantTimeToLive(t *testing.T) {
 
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -74,7 +74,7 @@ func TestLeaseGrantAndList(t *testing.T) {
 
 		for _, nc := range nestedCases {
 			t.Run(tc.name+"/"+nc.name, func(t *testing.T) {
-				ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+				ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 				defer cancel()
 				t.Logf("Creating cluster...")
 				clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
@@ -122,7 +122,7 @@ func TestLeaseGrantTimeToLiveExpired(t *testing.T) {
 
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -175,7 +175,7 @@ func TestLeaseGrantKeepAliveOnce(t *testing.T) {
 
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 15*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -220,7 +220,7 @@ func TestLeaseGrantRevoke(t *testing.T) {
 
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
diff --git a/tests/common/maintenance_auth_test.go b/tests/common/maintenance_auth_test.go
index 21a72580e..1a0e7b22b 100644
--- a/tests/common/maintenance_auth_test.go
+++ b/tests/common/maintenance_auth_test.go
@@ -203,7 +203,7 @@ func setupAuthForMaintenanceTest(c intf.Client) error {
 
 func testMaintenanceOperationWithAuth(t *testing.T, expectConnectError, expectOperationError bool, f func(context.Context, intf.Client) error, opts ...config.ClientOption) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 
 	clus := testRunner.NewCluster(ctx, t)
diff --git a/tests/common/member_test.go b/tests/common/member_test.go
index a3e025140..10037a42e 100644
--- a/tests/common/member_test.go
+++ b/tests/common/member_test.go
@@ -34,7 +34,7 @@ func TestMemberList(t *testing.T) {
 
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -114,7 +114,7 @@ func TestMemberAdd(t *testing.T) {
 					if quorumTc.waitForQuorum {
 						ctxTimeout += etcdserver.HealthInterval
 					}
-					ctx, cancel := context.WithTimeout(context.Background(), ctxTimeout)
+					ctx, cancel := context.WithTimeout(t.Context(), ctxTimeout)
 					defer cancel()
 					c := clusterTc.config
 					c.StrictReconfigCheck = quorumTc.strictReconfigCheck
@@ -189,7 +189,7 @@ func TestMemberRemove(t *testing.T) {
 				continue
 			}
 			t.Run(quorumTc.name+"/"+clusterTc.name, func(t *testing.T) {
-				ctx, cancel := context.WithTimeout(context.Background(), 14*time.Second)
+				ctx, cancel := context.WithTimeout(t.Context(), 14*time.Second)
 				defer cancel()
 				c := clusterTc.config
 				c.StrictReconfigCheck = quorumTc.strictReconfigCheck
diff --git a/tests/common/role_test.go b/tests/common/role_test.go
index 7dda25d30..2eb1af1e6 100644
--- a/tests/common/role_test.go
+++ b/tests/common/role_test.go
@@ -31,7 +31,7 @@ func TestRoleAdd_Simple(t *testing.T) {
 	testRunner.BeforeTest(t)
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -47,7 +47,7 @@ func TestRoleAdd_Simple(t *testing.T) {
 
 func TestRoleAdd_Error(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterSize(1))
 	defer clus.Close()
@@ -64,7 +64,7 @@ func TestRoleAdd_Error(t *testing.T) {
 
 func TestRootRole(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterSize(1))
 	defer clus.Close()
@@ -86,7 +86,7 @@ func TestRootRole(t *testing.T) {
 
 func TestRoleGrantRevokePermission(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterSize(1))
 	defer clus.Close()
@@ -109,7 +109,7 @@ func TestRoleGrantRevokePermission(t *testing.T) {
 
 func TestRoleDelete(t *testing.T) {
 	testRunner.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	clus := testRunner.NewCluster(ctx, t, config.WithClusterSize(1))
 	defer clus.Close()
diff --git a/tests/common/status_test.go b/tests/common/status_test.go
index 519bb07e4..ec6e1c219 100644
--- a/tests/common/status_test.go
+++ b/tests/common/status_test.go
@@ -30,7 +30,7 @@ func TestStatus(t *testing.T) {
 
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
diff --git a/tests/common/txn_test.go b/tests/common/txn_test.go
index b9c1b688c..771155584 100644
--- a/tests/common/txn_test.go
+++ b/tests/common/txn_test.go
@@ -59,7 +59,7 @@ func TestTxnSucc(t *testing.T) {
 	}
 	for _, cfg := range clusterTestCases() {
 		t.Run(cfg.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(cfg.config))
 			defer clus.Close()
@@ -99,7 +99,7 @@ func TestTxnFail(t *testing.T) {
 	}
 	for _, cfg := range clusterTestCases() {
 		t.Run(cfg.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(cfg.config))
 			defer clus.Close()
diff --git a/tests/common/unit_test.go b/tests/common/unit_test.go
index 4b172e7a3..48e3a51cd 100644
--- a/tests/common/unit_test.go
+++ b/tests/common/unit_test.go
@@ -37,6 +37,10 @@ func WithAuth(userName, password string) config.ClientOption {
 	return func(any) {}
 }
 
+func WithAuthToken(token string) config.ClientOption {
+	return func(any) {}
+}
+
 func WithEndpoints(endpoints []string) config.ClientOption {
 	return func(any) {}
 }
diff --git a/tests/common/user_test.go b/tests/common/user_test.go
index 7f189b000..dc8e64871 100644
--- a/tests/common/user_test.go
+++ b/tests/common/user_test.go
@@ -66,7 +66,7 @@ func TestUserAdd_Simple(t *testing.T) {
 	for _, tc := range clusterTestCases() {
 		for _, nc := range tcs {
 			t.Run(tc.name+"/"+nc.name, func(t *testing.T) {
-				ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+				ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 				defer cancel()
 				clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 				defer clus.Close()
@@ -90,7 +90,7 @@ func TestUserAdd_DuplicateUserNotAllowed(t *testing.T) {
 	testRunner.BeforeTest(t)
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -114,7 +114,7 @@ func TestUserList(t *testing.T) {
 	testRunner.BeforeTest(t)
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -145,7 +145,7 @@ func TestUserDelete(t *testing.T) {
 	testRunner.BeforeTest(t)
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -182,7 +182,7 @@ func TestUserChangePassword(t *testing.T) {
 	testRunner.BeforeTest(t)
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
diff --git a/tests/common/wait_leader_test.go b/tests/common/wait_leader_test.go
index 5f9a8ba05..fd15a8ae5 100644
--- a/tests/common/wait_leader_test.go
+++ b/tests/common/wait_leader_test.go
@@ -29,7 +29,7 @@ func TestWaitLeader(t *testing.T) {
 
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
@@ -56,7 +56,7 @@ func TestWaitLeader_MemberStop(t *testing.T) {
 
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 			defer clus.Close()
diff --git a/tests/common/watch_test.go b/tests/common/watch_test.go
index fe6d6b0c8..4e853df92 100644
--- a/tests/common/watch_test.go
+++ b/tests/common/watch_test.go
@@ -31,7 +31,7 @@ func TestWatch(t *testing.T) {
 	watchTimeout := 1 * time.Second
 	for _, tc := range clusterTestCases() {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterConfig(tc.config))
 
diff --git a/tests/e2e/cluster_downgrade_test.go b/tests/e2e/cluster_downgrade_test.go
index 0d80f0c4b..511a9fe1f 100644
--- a/tests/e2e/cluster_downgrade_test.go
+++ b/tests/e2e/cluster_downgrade_test.go
@@ -137,7 +137,7 @@ func testDowngradeUpgrade(t *testing.T, numberOfMembersToDowngrade int, clusterS
 	e2e.ValidateDowngradeInfo(t, epc, &pb.DowngradeInfo{Enabled: false})
 
 	t.Log("Adding member to test membership, but a learner avoid breaking quorum")
-	resp, err := cc.MemberAddAsLearner(context.Background(), "fake1", []string{"http://127.0.0.1:1001"})
+	resp, err := cc.MemberAddAsLearner(t.Context(), "fake1", []string{"http://127.0.0.1:1001"})
 	require.NoError(t, err)
 	if triggerSnapshot {
 		t.Logf("Generating snapshot")
@@ -145,7 +145,7 @@ func testDowngradeUpgrade(t *testing.T, numberOfMembersToDowngrade int, clusterS
 		verifySnapshot(t, epc)
 	}
 	t.Log("Removing learner to test membership")
-	_, err = cc.MemberRemove(context.Background(), resp.Member.ID)
+	_, err = cc.MemberRemove(t.Context(), resp.Member.ID)
 	require.NoError(t, err)
 	beforeMembers, beforeKV := getMembersAndKeys(t, cc)
 
@@ -197,7 +197,7 @@ func testDowngradeUpgrade(t *testing.T, numberOfMembersToDowngrade int, clusterS
 	}
 
 	t.Log("Adding learner to test membership, but avoid breaking quorum")
-	resp, err = cc.MemberAddAsLearner(context.Background(), "fake2", []string{"http://127.0.0.1:1002"})
+	resp, err = cc.MemberAddAsLearner(t.Context(), "fake2", []string{"http://127.0.0.1:1002"})
 	require.NoError(t, err)
 	if triggerSnapshot {
 		t.Logf("Generating snapshot")
@@ -205,7 +205,7 @@ func testDowngradeUpgrade(t *testing.T, numberOfMembersToDowngrade int, clusterS
 		verifySnapshot(t, epc)
 	}
 	t.Log("Removing learner to test membership")
-	_, err = cc.MemberRemove(context.Background(), resp.Member.ID)
+	_, err = cc.MemberRemove(t.Context(), resp.Member.ID)
 	require.NoError(t, err)
 	beforeMembers, beforeKV = getMembersAndKeys(t, cc)
 
@@ -229,7 +229,7 @@ func testDowngradeUpgrade(t *testing.T, numberOfMembersToDowngrade int, clusterS
 }
 
 func newCluster(t *testing.T, clusterSize int, snapshotCount uint64) *e2e.EtcdProcessCluster {
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithClusterSize(clusterSize),
 		e2e.WithSnapshotCount(snapshotCount),
 		e2e.WithKeepDataDir(true),
@@ -246,7 +246,7 @@ func newCluster(t *testing.T, clusterSize int, snapshotCount uint64) *e2e.EtcdPr
 }
 
 func generateSnapshot(t *testing.T, snapshotCount uint64, cc *e2e.EtcdctlV3) {
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	var i uint64
@@ -286,7 +286,7 @@ func verifySnapshotMembers(t *testing.T, epc *e2e.EtcdProcessCluster, expectedMe
 }
 
 func getMembersAndKeys(t *testing.T, cc *e2e.EtcdctlV3) (*clientv3.MemberListResponse, *clientv3.GetResponse) {
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	kvs, err := cc.Get(ctx, "", config.GetOptions{Prefix: true})
diff --git a/tests/e2e/cmux_test.go b/tests/e2e/cmux_test.go
index 9281705ee..7d9596dbe 100644
--- a/tests/e2e/cmux_test.go
+++ b/tests/e2e/cmux_test.go
@@ -69,7 +69,7 @@ func TestConnectionMultiplexing(t *testing.T) {
 		},
 	} {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx := context.Background()
+			ctx := t.Context()
 			cfg := e2e.NewConfig(e2e.WithClusterSize(1))
 			cfg.Client.ConnectionType = tc.serverTLS
 			cfg.ClientHTTPSeparate = tc.separateHTTPPort
diff --git a/tests/e2e/corrupt_test.go b/tests/e2e/corrupt_test.go
index 86535345e..d9e7b0770 100644
--- a/tests/e2e/corrupt_test.go
+++ b/tests/e2e/corrupt_test.go
@@ -99,7 +99,7 @@ func corruptTest(cx ctlCtx) {
 func TestInPlaceRecovery(t *testing.T) {
 	basePort := 20000
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	// Initialize the cluster.
@@ -198,7 +198,7 @@ func TestPeriodicCheckDetectsCorruptionWithExperimentalFlag(t *testing.T) {
 func testPeriodicCheckDetectsCorruption(t *testing.T, useExperimentalFlag bool) {
 	checkTime := time.Second
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 	var corruptCheckTime e2e.EPClusterOption
 	if useExperimentalFlag {
@@ -233,7 +233,7 @@ func testPeriodicCheckDetectsCorruption(t *testing.T, useExperimentalFlag bool)
 	err = testutil.CorruptBBolt(datadir.ToBackendFileName(epc.Procs[0].Config().DataDirPath))
 	require.NoError(t, err)
 
-	err = epc.Procs[0].Restart(context.TODO())
+	err = epc.Procs[0].Restart(t.Context())
 	require.NoError(t, err)
 	time.Sleep(checkTime * 11 / 10)
 	alarmResponse, err := cc.AlarmList(ctx)
@@ -252,7 +252,7 @@ func TestCompactHashCheckDetectCorruptionWithFeatureGate(t *testing.T) {
 func testCompactHashCheckDetectCorruption(t *testing.T, useFeatureGate bool) {
 	checkTime := time.Second
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 	opts := []e2e.EPClusterOption{e2e.WithKeepDataDir(true), e2e.WithCompactHashCheckTime(checkTime)}
 	if useFeatureGate {
@@ -308,7 +308,7 @@ func TestCompactHashCheckDetectCorruptionInterruptWithExperimentalFlag(t *testin
 func testCompactHashCheckDetectCorruptionInterrupt(t *testing.T, useFeatureGate bool, useExperimentalFlag bool) {
 	checkTime := time.Second
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 60*time.Second)
 	defer cancel()
 
 	slowCompactionNodeIndex := 1
@@ -407,7 +407,7 @@ func TestCtlV3LinearizableRead(t *testing.T) {
 func testCtlV3ReadAfterWrite(t *testing.T, ops ...clientv3.OpOption) {
 	e2e.BeforeTest(t)
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t,
 		e2e.WithClusterSize(1),
diff --git a/tests/e2e/ctl_v3_auth_cluster_test.go b/tests/e2e/ctl_v3_auth_cluster_test.go
index 35b7cd289..e758d6e9b 100644
--- a/tests/e2e/ctl_v3_auth_cluster_test.go
+++ b/tests/e2e/ctl_v3_auth_cluster_test.go
@@ -30,7 +30,7 @@ import (
 
 func TestAuthCluster(t *testing.T) {
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t,
diff --git a/tests/e2e/ctl_v3_auth_no_proxy_test.go b/tests/e2e/ctl_v3_auth_no_proxy_test.go
index 8529ff38d..8b79cc0dd 100644
--- a/tests/e2e/ctl_v3_auth_no_proxy_test.go
+++ b/tests/e2e/ctl_v3_auth_no_proxy_test.go
@@ -43,7 +43,7 @@ func TestCtlV3AuthCertCNAndUsernameNoPassword(t *testing.T) {
 
 func TestCtlV3AuthCertCNWithWithConcurrentOperation(t *testing.T) {
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	// apply the certificate which has `root` CommonName,
diff --git a/tests/e2e/ctl_v3_grpc_test.go b/tests/e2e/ctl_v3_grpc_test.go
index 78881f3ed..58f2791e2 100644
--- a/tests/e2e/ctl_v3_grpc_test.go
+++ b/tests/e2e/ctl_v3_grpc_test.go
@@ -110,7 +110,7 @@ func TestAuthority(t *testing.T) {
 		for _, clusterSize := range []int{1, 3} {
 			t.Run(fmt.Sprintf("Size: %d, Scenario: %q", clusterSize, tc.name), func(t *testing.T) {
 				e2e.BeforeTest(t)
-				ctx, cancel := context.WithCancel(context.Background())
+				ctx, cancel := context.WithCancel(t.Context())
 				defer cancel()
 
 				cfg := e2e.NewConfigNoTLS()
@@ -125,7 +125,7 @@ func TestAuthority(t *testing.T) {
 					cfg.BaseClientScheme = "unix"
 				}
 
-				epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(cfg))
+				epc, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 				if err != nil {
 					t.Fatalf("could not start etcd process cluster (%v)", err)
 				}
@@ -159,7 +159,7 @@ func templateEndpoints(t *testing.T, pattern string, clus *e2e.EtcdProcessCluste
 
 func assertAuthority(t *testing.T, expectAuthorityPattern string, clus *e2e.EtcdProcessCluster) {
 	for i := range clus.Procs {
-		line, _ := clus.Procs[i].Logs().ExpectWithContext(context.TODO(), expect.ExpectedResponse{Value: `http2: decoded hpack field header field ":authority"`})
+		line, _ := clus.Procs[i].Logs().ExpectWithContext(t.Context(), expect.ExpectedResponse{Value: `http2: decoded hpack field header field ":authority"`})
 		line = strings.TrimSuffix(line, "\n")
 		line = strings.TrimSuffix(line, "\r")
 
diff --git a/tests/e2e/ctl_v3_member_no_proxy_test.go b/tests/e2e/ctl_v3_member_no_proxy_test.go
index be492f6a4..6f1aa28c4 100644
--- a/tests/e2e/ctl_v3_member_no_proxy_test.go
+++ b/tests/e2e/ctl_v3_member_no_proxy_test.go
@@ -33,7 +33,7 @@ import (
 
 func TestMemberReplace(t *testing.T) {
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
 	defer cancel()
 
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t)
@@ -100,7 +100,7 @@ func TestMemberReplace(t *testing.T) {
 
 func TestMemberReplaceWithLearner(t *testing.T) {
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
 	defer cancel()
 
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t)
diff --git a/tests/e2e/ctl_v3_member_test.go b/tests/e2e/ctl_v3_member_test.go
index 7eb2a046b..e9d302716 100644
--- a/tests/e2e/ctl_v3_member_test.go
+++ b/tests/e2e/ctl_v3_member_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -69,7 +68,7 @@ func TestCtlV3MemberUpdatePeerTLS(t *testing.T) {
 func TestCtlV3ConsistentMemberList(t *testing.T) {
 	e2e.BeforeTest(t)
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t,
 		e2e.WithClusterSize(1),
@@ -290,7 +289,7 @@ func ctlV3MemberUpdate(cx ctlCtx, memberID, peerURL string) error {
 
 func TestRemoveNonExistingMember(t *testing.T) {
 	e2e.BeforeTest(t)
-	ctx := context.Background()
+	ctx := t.Context()
 
 	cfg := e2e.ConfigStandalone(*e2e.NewConfig())
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithConfig(cfg))
diff --git a/tests/e2e/ctl_v3_move_leader_test.go b/tests/e2e/ctl_v3_move_leader_test.go
index cc3e80774..c77beb2b8 100644
--- a/tests/e2e/ctl_v3_move_leader_test.go
+++ b/tests/e2e/ctl_v3_move_leader_test.go
@@ -84,7 +84,7 @@ func testCtlV3MoveLeader(t *testing.T, cfg e2e.EtcdProcessClusterConfig, envVars
 			TLS:         tcfg,
 		})
 		require.NoError(t, err)
-		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 		resp, err := cli.Status(ctx, ep)
 		if err != nil {
 			t.Fatalf("failed to get status from endpoint %s: %v", ep, err)
@@ -145,7 +145,7 @@ func setupEtcdctlTest(t *testing.T, cfg *e2e.EtcdProcessClusterConfig, quorum bo
 	if !quorum {
 		cfg = e2e.ConfigStandalone(*cfg)
 	}
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(cfg))
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 	if err != nil {
 		t.Fatalf("could not start etcd process cluster (%v)", err)
 	}
diff --git a/tests/e2e/ctl_v3_snapshot_test.go b/tests/e2e/ctl_v3_snapshot_test.go
index d66249a54..f793c320e 100644
--- a/tests/e2e/ctl_v3_snapshot_test.go
+++ b/tests/e2e/ctl_v3_snapshot_test.go
@@ -169,7 +169,7 @@ func testIssue6361(t *testing.T) {
 
 	e2e.BeforeTest(t)
 
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithClusterSize(1),
 		e2e.WithKeepDataDir(true),
 	)
@@ -222,7 +222,7 @@ func testIssue6361(t *testing.T) {
 			epc.Procs[0].Config().Args[i+1] = newDataDir
 		}
 	}
-	require.NoError(t, epc.Procs[0].Restart(context.TODO()))
+	require.NoError(t, epc.Procs[0].Restart(t.Context()))
 
 	t.Log("Ensuring the restored member has the correct data...")
 	for i := range kvs {
@@ -294,7 +294,7 @@ func snapshotVersionTest(cx ctlCtx) {
 func TestRestoreCompactionRevBump(t *testing.T) {
 	e2e.BeforeTest(t)
 
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithClusterSize(1),
 		e2e.WithKeepDataDir(true),
 	)
@@ -309,13 +309,13 @@ func TestRestoreCompactionRevBump(t *testing.T) {
 
 	ctl := epc.Etcdctl()
 
-	watchCh := ctl.Watch(context.Background(), "foo", config.WatchOptions{Prefix: true})
+	watchCh := ctl.Watch(t.Context(), "foo", config.WatchOptions{Prefix: true})
 	// flake-fix: the watch can sometimes miss the first put below causing test failure
 	time.Sleep(100 * time.Millisecond)
 
 	kvs := []testutils.KV{{Key: "foo1", Val: "val1"}, {Key: "foo2", Val: "val2"}, {Key: "foo3", Val: "val3"}}
 	for i := range kvs {
-		require.NoError(t, ctl.Put(context.Background(), kvs[i].Key, kvs[i].Val, config.PutOptions{}))
+		require.NoError(t, ctl.Put(t.Context(), kvs[i].Key, kvs[i].Val, config.PutOptions{}))
 	}
 
 	watchTimeout := 1 * time.Second
@@ -337,10 +337,10 @@ func TestRestoreCompactionRevBump(t *testing.T) {
 	// add some more kvs that are not in the snapshot that will be lost after restore
 	unsnappedKVs := []testutils.KV{{Key: "unsnapped1", Val: "one"}, {Key: "unsnapped2", Val: "two"}, {Key: "unsnapped3", Val: "three"}}
 	for i := range unsnappedKVs {
-		require.NoError(t, ctl.Put(context.Background(), unsnappedKVs[i].Key, unsnappedKVs[i].Val, config.PutOptions{}))
+		require.NoError(t, ctl.Put(t.Context(), unsnappedKVs[i].Key, unsnappedKVs[i].Val, config.PutOptions{}))
 	}
 
-	membersBefore, err := ctl.MemberList(context.Background(), false)
+	membersBefore, err := ctl.MemberList(t.Context(), false)
 	require.NoError(t, err)
 
 	t.Log("Stopping the original server...")
@@ -374,12 +374,12 @@ func TestRestoreCompactionRevBump(t *testing.T) {
 	// Verify that initial snapshot is created by the restore operation
 	verifySnapshotMembers(t, epc, membersBefore)
 
-	require.NoError(t, epc.Restart(context.Background()))
+	require.NoError(t, epc.Restart(t.Context()))
 
 	t.Log("Ensuring the restored member has the correct data...")
 	hasKVs(t, ctl, kvs, currentRev, baseRev)
 	for i := range unsnappedKVs {
-		v, gerr := ctl.Get(context.Background(), unsnappedKVs[i].Key, config.GetOptions{})
+		v, gerr := ctl.Get(t.Context(), unsnappedKVs[i].Key, config.GetOptions{})
 		require.NoError(t, gerr)
 		require.Equal(t, int64(0), v.Count)
 	}
@@ -395,7 +395,7 @@ func TestRestoreCompactionRevBump(t *testing.T) {
 	// clients might restart the watch at the old base revision, that should not yield any new data
 	// everything up until bumpAmount+currentRev should return "already compacted"
 	for i := bumpAmount - 2; i < bumpAmount+currentRev; i++ {
-		watchCh = ctl.Watch(context.Background(), "foo", config.WatchOptions{Prefix: true, Revision: int64(i)})
+		watchCh = ctl.Watch(t.Context(), "foo", config.WatchOptions{Prefix: true, Revision: int64(i)})
 		cancelResult := <-watchCh
 		require.Equal(t, v3rpc.ErrCompacted, cancelResult.Err())
 		require.Truef(t, cancelResult.Canceled, "expected ongoing watch to be cancelled after restoring with --mark-compacted")
@@ -403,10 +403,10 @@ func TestRestoreCompactionRevBump(t *testing.T) {
 	}
 
 	// a watch after that revision should yield successful results when a new put arrives
-	ctx, cancel := context.WithTimeout(context.Background(), watchTimeout*5)
+	ctx, cancel := context.WithTimeout(t.Context(), watchTimeout*5)
 	defer cancel()
 	watchCh = ctl.Watch(ctx, "foo", config.WatchOptions{Prefix: true, Revision: int64(bumpAmount + currentRev + 1)})
-	require.NoError(t, ctl.Put(context.Background(), "foo4", "val4", config.PutOptions{}))
+	require.NoError(t, ctl.Put(t.Context(), "foo4", "val4", config.PutOptions{}))
 	watchRes, err = testutils.KeyValuesFromWatchChan(watchCh, 1, watchTimeout)
 	require.NoErrorf(t, err, "failed to get key-values from watch channel %s", err)
 	require.Equal(t, []testutils.KV{{Key: "foo4", Val: "val4"}}, watchRes)
@@ -414,7 +414,7 @@ func TestRestoreCompactionRevBump(t *testing.T) {
 
 func hasKVs(t *testing.T, ctl *e2e.EtcdctlV3, kvs []testutils.KV, currentRev int, baseRev int) {
 	for i := range kvs {
-		v, err := ctl.Get(context.Background(), kvs[i].Key, config.GetOptions{})
+		v, err := ctl.Get(t.Context(), kvs[i].Key, config.GetOptions{})
 		require.NoError(t, err)
 		require.Equal(t, int64(1), v.Count)
 		require.Equal(t, kvs[i].Val, string(v.Kvs[0].Value))
@@ -427,7 +427,7 @@ func hasKVs(t *testing.T, ctl *e2e.EtcdctlV3, kvs []testutils.KV, currentRev int
 
 func TestBreakConsistentIndexNewerThanSnapshot(t *testing.T) {
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	var snapshotCount uint64 = 50
diff --git a/tests/e2e/ctl_v3_test.go b/tests/e2e/ctl_v3_test.go
index 2cd112dc6..9c5c2c857 100644
--- a/tests/e2e/ctl_v3_test.go
+++ b/tests/e2e/ctl_v3_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"fmt"
 	"os"
 	"strings"
@@ -60,7 +59,7 @@ func TestClusterVersion(t *testing.T) {
 				e2e.WithRollingStart(tt.rollingStart),
 			)
 
-			epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(cfg))
+			epc, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 			if err != nil {
 				t.Fatalf("could not start etcd process cluster (%v)", err)
 			}
@@ -232,7 +231,7 @@ func testCtlWithOffline(t *testing.T, testFunc func(ctlCtx), testOfflineFunc fun
 		ret.cfg.KeepDataDir = true
 	}
 
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(&ret.cfg))
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(&ret.cfg))
 	if err != nil {
 		t.Fatalf("could not start etcd process cluster (%v)", err)
 	}
diff --git a/tests/e2e/defrag_no_space_test.go b/tests/e2e/defrag_no_space_test.go
index f6ceabe66..f3c70c744 100644
--- a/tests/e2e/defrag_no_space_test.go
+++ b/tests/e2e/defrag_no_space_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"fmt"
 	"testing"
 	"time"
@@ -48,7 +47,7 @@ func TestDefragNoSpace(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			e2e.BeforeTest(t)
 
-			clus, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+			clus, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 				e2e.WithClusterSize(1),
 				e2e.WithGoFailEnabled(true),
 			)
@@ -57,12 +56,12 @@ func TestDefragNoSpace(t *testing.T) {
 
 			member := clus.Procs[0]
 
-			require.NoError(t, member.Failpoints().SetupHTTP(context.Background(), tc.failpoint, fmt.Sprintf(`return("%s")`, tc.err)))
-			require.ErrorContains(t, member.Etcdctl().Defragment(context.Background(), config.DefragOption{Timeout: time.Minute}), tc.err)
+			require.NoError(t, member.Failpoints().SetupHTTP(t.Context(), tc.failpoint, fmt.Sprintf(`return("%s")`, tc.err)))
+			require.ErrorContains(t, member.Etcdctl().Defragment(t.Context(), config.DefragOption{Timeout: time.Minute}), tc.err)
 
 			// Make sure etcd continues to run even after the failed defrag attempt
-			require.NoError(t, member.Etcdctl().Put(context.Background(), "foo", "bar", config.PutOptions{}))
-			value, err := member.Etcdctl().Get(context.Background(), "foo", config.GetOptions{})
+			require.NoError(t, member.Etcdctl().Put(t.Context(), "foo", "bar", config.PutOptions{}))
+			value, err := member.Etcdctl().Get(t.Context(), "foo", config.GetOptions{})
 			require.NoError(t, err)
 			require.Len(t, value.Kvs, 1)
 			require.Equal(t, "bar", string(value.Kvs[0].Value))
diff --git a/tests/e2e/discovery_test.go b/tests/e2e/discovery_test.go
index 15f51a957..5ecccc51b 100644
--- a/tests/e2e/discovery_test.go
+++ b/tests/e2e/discovery_test.go
@@ -47,7 +47,7 @@ func testClusterUsingDiscovery(t *testing.T, size int, peerTLS bool) {
 		t.Skipf("%q does not exist", e2e.BinPath.EtcdLastRelease)
 	}
 
-	dc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	dc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithBasePort(2000),
 		e2e.WithVersion(e2e.LastVersion),
 		e2e.WithClusterSize(1),
@@ -60,12 +60,12 @@ func testClusterUsingDiscovery(t *testing.T, size int, peerTLS bool) {
 
 	dcc := MustNewHTTPClient(t, dc.EndpointsHTTP(), nil)
 	dkapi := client.NewKeysAPI(dcc)
-	ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), integration.RequestTimeout)
 	_, err = dkapi.Create(ctx, "/_config/size", fmt.Sprintf("%d", size))
 	require.NoError(t, err)
 	cancel()
 
-	c, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	c, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithBasePort(3000),
 		e2e.WithClusterSize(size),
 		e2e.WithIsPeerTLS(peerTLS),
diff --git a/tests/e2e/discovery_v3_test.go b/tests/e2e/discovery_v3_test.go
index f3c47dd34..384f294cb 100644
--- a/tests/e2e/discovery_v3_test.go
+++ b/tests/e2e/discovery_v3_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"fmt"
 	"strconv"
 	"strings"
@@ -55,7 +54,7 @@ func testClusterUsingV3Discovery(t *testing.T, discoveryClusterSize, targetClust
 	e2e.BeforeTest(t)
 
 	// step 1: start the discovery service
-	ds, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	ds, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithBasePort(2000),
 		e2e.WithClusterSize(discoveryClusterSize),
 		e2e.WithClientConnType(clientTLSType),
@@ -122,5 +121,5 @@ func bootstrapEtcdClusterUsingV3Discovery(t *testing.T, discoveryEndpoints []str
 	}
 
 	// start the cluster
-	return e2e.StartEtcdProcessCluster(context.TODO(), t, epc, cfg)
+	return e2e.StartEtcdProcessCluster(t.Context(), t, epc, cfg)
 }
diff --git a/tests/e2e/etcd_config_test.go b/tests/e2e/etcd_config_test.go
index 1a8415d2d..51c5bb890 100644
--- a/tests/e2e/etcd_config_test.go
+++ b/tests/e2e/etcd_config_test.go
@@ -40,7 +40,7 @@ func TestEtcdExampleConfig(t *testing.T) {
 
 	proc, err := e2e.SpawnCmd([]string{e2e.BinPath.Etcd, "--config-file", exampleConfigFile}, nil)
 	require.NoError(t, err)
-	require.NoError(t, e2e.WaitReadyExpectProc(context.TODO(), proc, e2e.EtcdServerReadyLines))
+	require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), proc, e2e.EtcdServerReadyLines))
 	require.NoError(t, proc.Stop())
 }
 
@@ -80,7 +80,7 @@ func TestEtcdMultiPeer(t *testing.T) {
 	}
 
 	for _, p := range procs {
-		err := e2e.WaitReadyExpectProc(context.TODO(), p, e2e.EtcdServerReadyLines)
+		err := e2e.WaitReadyExpectProc(t.Context(), p, e2e.EtcdServerReadyLines)
 		require.NoError(t, err)
 	}
 }
@@ -102,7 +102,7 @@ func TestEtcdUnixPeers(t *testing.T) {
 	)
 	defer os.Remove("etcd.unix:1")
 	require.NoError(t, err)
-	require.NoError(t, e2e.WaitReadyExpectProc(context.TODO(), proc, e2e.EtcdServerReadyLines))
+	require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), proc, e2e.EtcdServerReadyLines))
 	require.NoError(t, proc.Stop())
 }
 
@@ -150,7 +150,7 @@ func TestEtcdListenMetricsURLsWithMissingClientTLSInfo(t *testing.T) {
 		_ = proc.Close()
 	}()
 
-	require.NoError(t, e2e.WaitReadyExpectProc(context.TODO(), proc, []string{embed.ErrMissingClientTLSInfoForMetricsURL.Error()}))
+	require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), proc, []string{embed.ErrMissingClientTLSInfoForMetricsURL.Error()}))
 }
 
 // TestEtcdPeerCNAuth checks that the inter peer auth based on CN of cert is working correctly.
@@ -224,7 +224,7 @@ func TestEtcdPeerCNAuth(t *testing.T) {
 		} else {
 			expect = []string{"remote error: tls: bad certificate"}
 		}
-		err := e2e.WaitReadyExpectProc(context.TODO(), p, expect)
+		err := e2e.WaitReadyExpectProc(t.Context(), p, expect)
 		require.NoError(t, err)
 	}
 }
@@ -311,7 +311,7 @@ func TestEtcdPeerMultiCNAuth(t *testing.T) {
 		} else {
 			expect = []string{"remote error: tls: bad certificate"}
 		}
-		err := e2e.WaitReadyExpectProc(context.TODO(), p, expect)
+		err := e2e.WaitReadyExpectProc(t.Context(), p, expect)
 		require.NoError(t, err)
 	}
 }
@@ -384,7 +384,7 @@ func TestEtcdPeerNameAuth(t *testing.T) {
 		} else {
 			expect = []string{"client certificate authentication failed"}
 		}
-		err := e2e.WaitReadyExpectProc(context.TODO(), p, expect)
+		err := e2e.WaitReadyExpectProc(t.Context(), p, expect)
 		require.NoError(t, err)
 	}
 }
@@ -490,7 +490,7 @@ func TestEtcdPeerLocalAddr(t *testing.T) {
 		} else {
 			expect = []string{"x509: certificate is valid for 127.0.0.1, not "}
 		}
-		err := e2e.WaitReadyExpectProc(context.TODO(), p, expect)
+		err := e2e.WaitReadyExpectProc(t.Context(), p, expect)
 		require.NoError(t, err)
 	}
 }
@@ -570,7 +570,7 @@ func TestBootstrapDefragFlag(t *testing.T) {
 
 	proc, err := e2e.SpawnCmd([]string{e2e.BinPath.Etcd, "--experimental-bootstrap-defrag-threshold-megabytes", "1000"}, nil)
 	require.NoError(t, err)
-	require.NoError(t, e2e.WaitReadyExpectProc(context.TODO(), proc, []string{"Skipping defragmentation"}))
+	require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), proc, []string{"Skipping defragmentation"}))
 	require.NoError(t, proc.Stop())
 
 	// wait for the process to exit, otherwise test will have leaked goroutine
@@ -582,7 +582,7 @@ func TestBootstrapDefragFlag(t *testing.T) {
 func TestSnapshotCatchupEntriesFlag(t *testing.T) {
 	e2e.SkipInShortMode(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 
 	proc, err := e2e.SpawnCmd([]string{e2e.BinPath.Etcd, "--experimental-snapshot-catchup-entries", "1000"}, nil)
@@ -600,7 +600,7 @@ func TestSnapshotCatchupEntriesFlag(t *testing.T) {
 // TestEtcdHealthyWithTinySnapshotCatchupEntries ensures multi-node etcd cluster remains healthy with 1 snapshot catch up entry
 func TestEtcdHealthyWithTinySnapshotCatchupEntries(t *testing.T) {
 	e2e.BeforeTest(t)
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithClusterSize(3),
 		e2e.WithSnapshotCount(1),
 		e2e.WithSnapshotCatchUpEntries(1),
@@ -613,7 +613,7 @@ func TestEtcdHealthyWithTinySnapshotCatchupEntries(t *testing.T) {
 	})
 
 	// simulate 10 clients keep writing to etcd in parallel with no error
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	g, ctx := errgroup.WithContext(ctx)
 	for i := 0; i < 10; i++ {
@@ -655,7 +655,7 @@ func TestEtcdTLSVersion(t *testing.T) {
 		}, nil,
 	)
 	assert.NoError(t, err)
-	assert.NoErrorf(t, e2e.WaitReadyExpectProc(context.TODO(), proc, e2e.EtcdServerReadyLines), "did not receive expected output from etcd process")
+	assert.NoErrorf(t, e2e.WaitReadyExpectProc(t.Context(), proc, e2e.EtcdServerReadyLines), "did not receive expected output from etcd process")
 	assert.NoError(t, proc.Stop())
 
 	proc.Wait() // ensure the port has been released
@@ -699,7 +699,7 @@ func TestEtcdDeprecatedFlags(t *testing.T) {
 				tc.args, nil,
 			)
 			require.NoError(t, err)
-			require.NoError(t, e2e.WaitReadyExpectProc(context.TODO(), proc, []string{tc.expectedMsg}))
+			require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), proc, []string{tc.expectedMsg}))
 			require.NoError(t, proc.Stop())
 
 			proc.Wait() // ensure the port has been released
@@ -727,7 +727,7 @@ func TestV2DeprecationEnforceDefaultValue(t *testing.T) {
 				append(commonArgs, "--v2-deprecation", optionLevel), nil,
 			)
 			require.NoError(t, err)
-			require.NoError(t, e2e.WaitReadyExpectProc(context.TODO(), proc, []string{expectedDeprecationLevelMsg}))
+			require.NoError(t, e2e.WaitReadyExpectProc(t.Context(), proc, []string{expectedDeprecationLevelMsg}))
 			require.NoError(t, proc.Stop())
 
 			proc.Wait() // ensure the port has been released
diff --git a/tests/e2e/etcd_grpcproxy_test.go b/tests/e2e/etcd_grpcproxy_test.go
index 02174e89f..4f7a54d5f 100644
--- a/tests/e2e/etcd_grpcproxy_test.go
+++ b/tests/e2e/etcd_grpcproxy_test.go
@@ -33,7 +33,7 @@ import (
 
 func TestGrpcProxyAutoSync(t *testing.T) {
 	e2e.SkipInShortMode(t)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithClusterSize(1))
@@ -93,7 +93,7 @@ func TestGrpcProxyAutoSync(t *testing.T) {
 
 func TestGrpcProxyTLSVersions(t *testing.T) {
 	e2e.SkipInShortMode(t)
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithClusterSize(1))
diff --git a/tests/e2e/etcd_mix_versions_test.go b/tests/e2e/etcd_mix_versions_test.go
index 46dab60db..205a76168 100644
--- a/tests/e2e/etcd_mix_versions_test.go
+++ b/tests/e2e/etcd_mix_versions_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"fmt"
 	"testing"
 	"time"
@@ -89,7 +88,7 @@ func mixVersionsSnapshotTestByAddingMember(t *testing.T, cfg *e2e.EtcdProcessClu
 	}
 
 	t.Logf("Create an etcd cluster with %d member", cfg.ClusterSize)
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithConfig(cfg),
 		e2e.WithSnapshotCount(10),
 	)
@@ -107,9 +106,9 @@ func mixVersionsSnapshotTestByAddingMember(t *testing.T, cfg *e2e.EtcdProcessClu
 	newCfg.Version = newInstanceVersion
 	newCfg.ServerConfig.SnapshotCatchUpEntries = 10
 	t.Log("Starting a new etcd instance")
-	_, err = epc.StartNewProc(context.TODO(), &newCfg, t, false /* addAsLearner */)
+	_, err = epc.StartNewProc(t.Context(), &newCfg, t, false /* addAsLearner */)
 	require.NoErrorf(t, err, "failed to start the new etcd instance")
-	defer epc.CloseProc(context.TODO(), nil)
+	defer epc.CloseProc(t.Context(), nil)
 
 	assertKVHash(t, epc)
 }
@@ -136,7 +135,7 @@ func mixVersionsSnapshotTestByMockPartition(t *testing.T, cfg *e2e.EtcdProcessCl
 		e2e.WithSnapshotCatchUpEntries(10),
 	}
 	t.Logf("Create an etcd cluster with %d member", cfg.ClusterSize)
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, clusterOptions...)
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t, clusterOptions...)
 	require.NoErrorf(t, err, "failed to start etcd cluster")
 	defer func() {
 		derr := epc.Close()
@@ -156,7 +155,7 @@ func mixVersionsSnapshotTestByMockPartition(t *testing.T, cfg *e2e.EtcdProcessCl
 	e2e.AssertProcessLogs(t, leaderEPC, "saved snapshot")
 
 	t.Log("Restart the partitioned member")
-	err = toPartitionedMember.Restart(context.TODO())
+	err = toPartitionedMember.Restart(t.Context())
 	require.NoError(t, err)
 
 	assertKVHash(t, epc)
@@ -170,7 +169,7 @@ func writeKVs(t *testing.T, etcdctl *e2e.EtcdctlV3, startIdx, endIdx int) {
 	for i := startIdx; i < endIdx; i++ {
 		key := fmt.Sprintf("key-%d", i)
 		value := fmt.Sprintf("value-%d", i)
-		err := etcdctl.Put(context.TODO(), key, value, config.PutOptions{})
+		err := etcdctl.Put(t.Context(), key, value, config.PutOptions{})
 		require.NoErrorf(t, err, "failed to put %q", key)
 	}
 }
@@ -182,7 +181,7 @@ func assertKVHash(t *testing.T, epc *e2e.EtcdProcessCluster) {
 	}
 	t.Log("Verify all nodes have exact same revision and hash")
 	assert.Eventually(t, func() bool {
-		hashKvs, err := epc.Etcdctl().HashKV(context.TODO(), 0)
+		hashKvs, err := epc.Etcdctl().HashKV(t.Context(), 0)
 		if err != nil {
 			t.Logf("failed to get HashKV: %v", err)
 			return false
diff --git a/tests/e2e/etcd_release_upgrade_test.go b/tests/e2e/etcd_release_upgrade_test.go
index 2722cacdb..18e08f7d3 100644
--- a/tests/e2e/etcd_release_upgrade_test.go
+++ b/tests/e2e/etcd_release_upgrade_test.go
@@ -17,6 +17,7 @@ package e2e
 import (
 	"context"
 	"fmt"
+	"strings"
 	"sync"
 	"testing"
 	"time"
@@ -24,10 +25,13 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	"go.etcd.io/etcd/api/v3/etcdserverpb"
 	"go.etcd.io/etcd/api/v3/version"
 	"go.etcd.io/etcd/client/pkg/v3/fileutil"
 	"go.etcd.io/etcd/pkg/v3/expect"
+	"go.etcd.io/etcd/tests/v3/framework/config"
 	"go.etcd.io/etcd/tests/v3/framework/e2e"
+	"go.etcd.io/etcd/tests/v3/framework/testutils"
 )
 
 // TestReleaseUpgrade ensures that changes to master branch does not affect
@@ -39,7 +43,7 @@ func TestReleaseUpgrade(t *testing.T) {
 
 	e2e.BeforeTest(t)
 
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithVersion(e2e.LastVersion),
 		e2e.WithSnapshotCount(3),
 		e2e.WithBasePeerScheme("unix"), // to avoid port conflict
@@ -82,7 +86,7 @@ func TestReleaseUpgrade(t *testing.T) {
 		epc.Procs[i].Config().KeepDataDir = true
 
 		t.Logf("Restarting node in the new version: %v", i)
-		if err = epc.Procs[i].Restart(context.TODO()); err != nil {
+		if err = epc.Procs[i].Restart(t.Context()); err != nil {
 			t.Fatalf("error restarting etcd process (%v)", err)
 		}
 
@@ -119,7 +123,7 @@ func TestReleaseUpgradeWithRestart(t *testing.T) {
 
 	e2e.BeforeTest(t)
 
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithVersion(e2e.LastVersion),
 		e2e.WithSnapshotCount(10),
 		e2e.WithBasePeerScheme("unix"),
@@ -158,7 +162,7 @@ func TestReleaseUpgradeWithRestart(t *testing.T) {
 		go func(i int) {
 			epc.Procs[i].Config().ExecPath = e2e.BinPath.Etcd
 			epc.Procs[i].Config().KeepDataDir = true
-			assert.NoErrorf(t, epc.Procs[i].Restart(context.TODO()), "error restarting etcd process")
+			assert.NoErrorf(t, epc.Procs[i].Restart(t.Context()), "error restarting etcd process")
 			wg.Done()
 		}(i)
 	}
@@ -166,3 +170,127 @@ func TestReleaseUpgradeWithRestart(t *testing.T) {
 
 	require.NoError(t, ctlV3Get(cx, []string{kvs[0].key}, []kv{kvs[0]}...))
 }
+
+func TestClusterUpgradeAfterPromotingMembers(t *testing.T) {
+	if !fileutil.Exist(e2e.BinPath.EtcdLastRelease) {
+		t.Skipf("%q does not exist", e2e.BinPath.EtcdLastRelease)
+	}
+
+	e2e.BeforeTest(t)
+
+	currentVersion, err := e2e.GetVersionFromBinary(e2e.BinPath.Etcd)
+	require.NoErrorf(t, err, "failed to get version from binary")
+
+	lastClusterVersion, err := e2e.GetVersionFromBinary(e2e.BinPath.EtcdLastRelease)
+	require.NoErrorf(t, err, "failed to get version from last release binary")
+
+	clusterSize := 3
+
+	for _, tc := range []struct {
+		name     string
+		snapshot int
+	}{
+		{
+			name:     "create snapshot after promoted",
+			snapshot: 10,
+		},
+		{
+			name: "no snapshot after promoted",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+
+			epc, _ := mustCreateNewClusterByPromotingMembers(t, e2e.LastVersion, clusterSize,
+				e2e.WithSnapshotCount(uint64(tc.snapshot)))
+			defer func() {
+				require.NoError(t, epc.Close())
+			}()
+
+			for i := 0; i < tc.snapshot; i++ {
+				err = epc.Etcdctl().Put(ctx, "foo", "bar", config.PutOptions{})
+				require.NoError(t, err)
+			}
+
+			err = e2e.DowngradeUpgradeMembers(t, nil, epc, clusterSize, false, lastClusterVersion, currentVersion)
+			require.NoError(t, err)
+
+			t.Logf("Checking all members' status after upgrading")
+			ensureAllMembersAreVotingMembers(t, epc)
+
+			t.Logf("Checking all members are ready to serve client requests")
+			for i := 0; i < clusterSize; i++ {
+				err = epc.Procs[i].Etcdctl().Put(context.Background(), "foo", "bar", config.PutOptions{})
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
+func mustCreateNewClusterByPromotingMembers(t *testing.T, clusterVersion e2e.ClusterVersion, clusterSize int, opts ...e2e.EPClusterOption) (*e2e.EtcdProcessCluster, []*etcdserverpb.Member) {
+	require.GreaterOrEqualf(t, clusterSize, 1, "clusterSize must be at least 1")
+
+	ctx := context.Background()
+
+	t.Logf("Creating new etcd cluster - version: %s, clusterSize: %v", clusterVersion, clusterSize)
+	opts = append(opts, e2e.WithVersion(clusterVersion), e2e.WithClusterSize(1))
+	epc, err := e2e.NewEtcdProcessCluster(ctx, t, opts...)
+	require.NoErrorf(t, err, "failed to start first etcd process")
+	defer func() {
+		if t.Failed() {
+			epc.Close()
+		}
+	}()
+
+	var promotedMembers []*etcdserverpb.Member
+	for i := 1; i < clusterSize; i++ {
+		var (
+			memberID uint64
+			aerr     error
+		)
+
+		// NOTE: New promoted member needs time to get connected.
+		t.Logf("[%d] Adding new member as learner", i)
+		testutils.ExecuteWithTimeout(t, 1*time.Minute, func() {
+			for {
+				memberID, aerr = epc.StartNewProc(ctx, nil, t, true)
+				if aerr != nil {
+					if strings.Contains(aerr.Error(), "etcdserver: unhealthy cluster") {
+						time.Sleep(1 * time.Second)
+						continue
+					}
+				}
+				break
+			}
+		})
+		require.NoError(t, aerr)
+
+		t.Logf("[%d] Promoting member (%x)", i, memberID)
+		etcdctl := epc.Procs[0].Etcdctl()
+		resp, merr := etcdctl.MemberPromote(ctx, memberID)
+		require.NoError(t, merr)
+
+		for _, m := range resp.Members {
+			if m.ID == memberID {
+				promotedMembers = append(promotedMembers, m)
+			}
+		}
+	}
+
+	t.Log("Ensure all members are voting members from user perspective")
+	ensureAllMembersAreVotingMembers(t, epc)
+
+	return epc, promotedMembers
+}
+
+func ensureAllMembersAreVotingMembers(t *testing.T, epc *e2e.EtcdProcessCluster) {
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	resp, err := epc.Etcdctl().MemberList(ctx, false)
+	require.NoError(t, err)
+	require.Len(t, resp.Members, len(epc.Procs))
+	for _, m := range resp.Members {
+		require.Falsef(t, m.IsLearner, "node(%x)", m.ID)
+	}
+}
diff --git a/tests/e2e/failover_test.go b/tests/e2e/failover_test.go
index 878603673..58ee806ca 100644
--- a/tests/e2e/failover_test.go
+++ b/tests/e2e/failover_test.go
@@ -136,7 +136,7 @@ func TestFailoverOnDefrag(t *testing.T) {
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
 			e2e.BeforeTest(t)
-			clus, cerr := e2e.NewEtcdProcessCluster(context.TODO(), t, tc.clusterOptions...)
+			clus, cerr := e2e.NewEtcdProcessCluster(t.Context(), t, tc.clusterOptions...)
 			require.NoError(t, cerr)
 			t.Cleanup(func() { clus.Stop() })
 
@@ -165,7 +165,7 @@ func TestFailoverOnDefrag(t *testing.T) {
 						return lastErr
 					default:
 					}
-					getContext, cancel := context.WithTimeout(context.Background(), requestTimeout)
+					getContext, cancel := context.WithTimeout(t.Context(), requestTimeout)
 					_, err := clusterClient.Get(getContext, "health")
 					cancel()
 					requestVolume++
@@ -199,6 +199,6 @@ func TestFailoverOnDefrag(t *testing.T) {
 }
 
 func triggerDefrag(t *testing.T, member e2e.EtcdProcess) {
-	require.NoError(t, member.Failpoints().SetupHTTP(context.Background(), "defragBeforeCopy", `sleep("10s")`))
-	require.NoError(t, member.Etcdctl().Defragment(context.Background(), config.DefragOption{Timeout: time.Minute}))
+	require.NoError(t, member.Failpoints().SetupHTTP(t.Context(), "defragBeforeCopy", `sleep("10s")`))
+	require.NoError(t, member.Etcdctl().Defragment(t.Context(), config.DefragOption{Timeout: time.Minute}))
 }
diff --git a/tests/e2e/gateway_test.go b/tests/e2e/gateway_test.go
index 600207610..8d4f44221 100644
--- a/tests/e2e/gateway_test.go
+++ b/tests/e2e/gateway_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"strings"
 	"testing"
 
@@ -28,7 +27,7 @@ import (
 var defaultGatewayEndpoint = "127.0.0.1:23790"
 
 func TestGateway(t *testing.T) {
-	ec, err := e2e.NewEtcdProcessCluster(context.TODO(), t)
+	ec, err := e2e.NewEtcdProcessCluster(t.Context(), t)
 	require.NoError(t, err)
 	defer ec.Stop()
 
diff --git a/tests/e2e/graceful_shutdown_test.go b/tests/e2e/graceful_shutdown_test.go
index b612a5c1b..d889bfa4c 100644
--- a/tests/e2e/graceful_shutdown_test.go
+++ b/tests/e2e/graceful_shutdown_test.go
@@ -47,7 +47,7 @@ func TestGracefulShutdown(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			testRunner := e2e.NewE2eRunner()
 			testRunner.BeforeTest(t)
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 			clus := testRunner.NewCluster(ctx, t, config.WithClusterSize(tc.clusterSize))
 			// clean up orphaned resources like closing member client.
diff --git a/tests/e2e/hashkv_test.go b/tests/e2e/hashkv_test.go
index 6c81e1cc7..e17ec3d6a 100644
--- a/tests/e2e/hashkv_test.go
+++ b/tests/e2e/hashkv_test.go
@@ -17,7 +17,6 @@
 package e2e
 
 import (
-	"context"
 	"fmt"
 	"testing"
 
@@ -63,7 +62,7 @@ func TestVerifyHashKVAfterCompact(t *testing.T) {
 					}
 				}
 
-				ctx := context.Background()
+				ctx := t.Context()
 
 				cfg := e2e.NewConfigClientTLS()
 				clus, err := e2e.NewEtcdProcessCluster(ctx, t,
@@ -107,7 +106,7 @@ func TestVerifyHashKVAfterTwoCompactionsOnTombstone_MixVersions(t *testing.T) {
 		t.Skipf("%q does not exist", e2e.BinPath.EtcdLastRelease)
 	}
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	cfg := e2e.NewConfigClientTLS()
 	clus, err := e2e.NewEtcdProcessCluster(ctx, t,
@@ -149,7 +148,7 @@ func TestVerifyHashKVAfterCompactionOnLastTombstone_MixVersions(t *testing.T) {
 		{"key0", "key1"},
 	} {
 		t.Run(fmt.Sprintf("#%v", keys), func(t *testing.T) {
-			ctx := context.Background()
+			ctx := t.Context()
 
 			cfg := e2e.NewConfigClientTLS()
 			clus, err := e2e.NewEtcdProcessCluster(ctx, t,
@@ -182,7 +181,7 @@ func populateDataForHashKV(t *testing.T, clus *e2e.EtcdProcessCluster, clientCfg
 	c := newClient(t, clus.EndpointsGRPC(), clientCfg)
 	defer c.Close()
 
-	ctx := context.Background()
+	ctx := t.Context()
 	totalOperations := 40
 
 	var (
@@ -218,7 +217,7 @@ func populateDataForHashKV(t *testing.T, clus *e2e.EtcdProcessCluster, clientCfg
 }
 
 func verifyConsistentHashKVAcrossAllMembers(t *testing.T, cli *e2e.EtcdctlV3, hashKVOnRev int64) {
-	ctx := context.Background()
+	ctx := t.Context()
 
 	t.Logf("HashKV on rev=%d", hashKVOnRev)
 	resp, err := cli.HashKV(ctx, hashKVOnRev)
diff --git a/tests/e2e/http_health_check_test.go b/tests/e2e/http_health_check_test.go
index 86b41bfd6..883a115af 100644
--- a/tests/e2e/http_health_check_test.go
+++ b/tests/e2e/http_health_check_test.go
@@ -150,7 +150,7 @@ func TestHTTPHealthHandler(t *testing.T) {
 
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
 			defer cancel()
 			clus, err := e2e.NewEtcdProcessCluster(ctx, t, tc.clusterOptions...)
 			require.NoError(t, err)
@@ -313,7 +313,7 @@ func TestHTTPLivezReadyzHandler(t *testing.T) {
 
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second)
 			defer cancel()
 			clus, err := e2e.NewEtcdProcessCluster(ctx, t, tc.clusterOptions...)
 			require.NoError(t, err)
@@ -336,7 +336,7 @@ func TestHTTPLivezReadyzHandler(t *testing.T) {
 }
 
 func doHealthCheckAndVerify(t *testing.T, client *http.Client, url string, expectTimeoutError bool, expectStatusCode int, expectRespSubStrings []string) {
-	ctx, cancel := context.WithTimeout(context.Background(), healthCheckTimeout)
+	ctx, cancel := context.WithTimeout(t.Context(), healthCheckTimeout)
 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
 	require.NoErrorf(t, err, "failed to creat request %+v", err)
 	resp, herr := client.Do(req)
diff --git a/tests/e2e/leader_snapshot_no_proxy_test.go b/tests/e2e/leader_snapshot_no_proxy_test.go
index 7b3c39270..b58d294cf 100644
--- a/tests/e2e/leader_snapshot_no_proxy_test.go
+++ b/tests/e2e/leader_snapshot_no_proxy_test.go
@@ -35,7 +35,7 @@ import (
 
 func TestRecoverSnapshotBackend(t *testing.T) {
 	e2e.BeforeTest(t)
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t,
diff --git a/tests/e2e/logging_test.go b/tests/e2e/logging_test.go
index d391e2f1b..8e6b064ad 100644
--- a/tests/e2e/logging_test.go
+++ b/tests/e2e/logging_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"encoding/json"
 	"testing"
 	"time"
@@ -109,7 +108,7 @@ func TestNoErrorLogsDuringNormalOperations(t *testing.T) {
 	for _, tc := range tests {
 		t.Run(tc.name, func(t *testing.T) {
 			e2e.BeforeTest(t)
-			ctx := context.TODO()
+			ctx := t.Context()
 
 			epc, err := e2e.NewEtcdProcessCluster(ctx, t, tc.options...)
 			require.NoError(t, err)
diff --git a/tests/e2e/metrics_test.go b/tests/e2e/metrics_test.go
index 184a430d6..9d730ed3a 100644
--- a/tests/e2e/metrics_test.go
+++ b/tests/e2e/metrics_test.go
@@ -15,17 +15,12 @@
 package e2e
 
 import (
-	"bytes"
 	"context"
 	"fmt"
-	"io"
-	"net/http"
 	"net/url"
 	"testing"
 	"time"
 
-	dto "github.com/prometheus/client_model/go"
-	"github.com/prometheus/common/expfmt"
 	"github.com/stretchr/testify/require"
 
 	"go.etcd.io/etcd/api/v3/version"
@@ -167,7 +162,6 @@ func TestNoMetricsMissing(t *testing.T) {
 			"etcd_debugging_snap_save_marshalling_duration_seconds",
 			"etcd_debugging_snap_save_total_duration_seconds",
 			"etcd_debugging_store_expires_total",
-			"etcd_debugging_store_reads_total",
 			"etcd_debugging_store_watch_requests_total",
 			"etcd_debugging_store_watchers",
 			"etcd_debugging_store_writes_total",
@@ -307,7 +301,7 @@ func TestNoMetricsMissing(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
 			e2e.BeforeTest(t)
-			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 			defer cancel()
 
 			epc, err := e2e.NewEtcdProcessCluster(ctx, t, tc.options...)
@@ -325,7 +319,7 @@ func TestNoMetricsMissing(t *testing.T) {
 			metricsURL, err := url.JoinPath(epc.Procs[0].Config().ClientURL, "metrics")
 			require.NoError(t, err)
 
-			mfs, err := getMetrics(metricsURL)
+			mfs, err := e2e.GetMetrics(metricsURL)
 			require.NoError(t, err)
 
 			var missingMetrics []string
@@ -342,23 +336,6 @@ func TestNoMetricsMissing(t *testing.T) {
 	}
 }
 
-func getMetrics(metricsURL string) (map[string]*dto.MetricFamily, error) {
-	httpClient := http.Client{Transport: &http.Transport{}}
-	resp, err := httpClient.Get(metricsURL)
-	if err != nil {
-		return nil, err
-	}
-	defer resp.Body.Close()
-
-	data, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, err
-	}
-
-	var parser expfmt.TextParser
-	return parser.TextToMetricFamilies(bytes.NewReader(data))
-}
-
 // formatMetrics is only for test purpose
 /*func formatMetrics(metrics []string) string {
 	quoted := make([]string, len(metrics))
diff --git a/tests/e2e/promote_experimental_flag_test.go b/tests/e2e/promote_experimental_flag_test.go
index 13a8fcba4..18fb0bd3b 100644
--- a/tests/e2e/promote_experimental_flag_test.go
+++ b/tests/e2e/promote_experimental_flag_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"testing"
 	"time"
 
@@ -28,7 +27,7 @@ import (
 func TestWarningApplyDuration(t *testing.T) {
 	e2e.BeforeTest(t)
 
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithClusterSize(1),
 		e2e.WithWarningUnaryRequestDuration(time.Microsecond),
 	)
@@ -42,7 +41,7 @@ func TestWarningApplyDuration(t *testing.T) {
 	})
 
 	cc := epc.Etcdctl()
-	err = cc.Put(context.TODO(), "foo", "bar", config.PutOptions{})
+	err = cc.Put(t.Context(), "foo", "bar", config.PutOptions{})
 	require.NoErrorf(t, err, "error on put")
 
 	// verify warning
@@ -55,7 +54,7 @@ func TestWarningApplyDuration(t *testing.T) {
 func TestExperimentalWarningApplyDuration(t *testing.T) {
 	e2e.BeforeTest(t)
 
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithClusterSize(1),
 		e2e.WithExperimentalWarningUnaryRequestDuration(time.Microsecond),
 	)
@@ -69,7 +68,7 @@ func TestExperimentalWarningApplyDuration(t *testing.T) {
 	})
 
 	cc := epc.Etcdctl()
-	err = cc.Put(context.TODO(), "foo", "bar", config.PutOptions{})
+	err = cc.Put(t.Context(), "foo", "bar", config.PutOptions{})
 	require.NoErrorf(t, err, "error on put")
 
 	// verify warning
@@ -79,7 +78,7 @@ func TestExperimentalWarningApplyDuration(t *testing.T) {
 func TestBothWarningApplyDurationFlagsFail(t *testing.T) {
 	e2e.BeforeTest(t)
 
-	_, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	_, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithClusterSize(1),
 		e2e.WithWarningUnaryRequestDuration(time.Second),
 		e2e.WithExperimentalWarningUnaryRequestDuration(time.Second),
diff --git a/tests/e2e/reproduce_17780_test.go b/tests/e2e/reproduce_17780_test.go
index f5ef97b91..fc54b8e8c 100644
--- a/tests/e2e/reproduce_17780_test.go
+++ b/tests/e2e/reproduce_17780_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"fmt"
 	"testing"
 	"time"
@@ -34,7 +33,7 @@ func TestReproduce17780(t *testing.T) {
 
 	compactionBatchLimit := 10
 
-	ctx := context.TODO()
+	ctx := t.Context()
 	clus, cerr := e2e.NewEtcdProcessCluster(ctx, t,
 		e2e.WithClusterSize(3),
 		e2e.WithGoFailEnabled(true),
diff --git a/tests/e2e/reproduce_19406_test.go b/tests/e2e/reproduce_19406_test.go
new file mode 100644
index 000000000..be942aaba
--- /dev/null
+++ b/tests/e2e/reproduce_19406_test.go
@@ -0,0 +1,113 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package e2e
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"testing"
+	"time"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/pkg/v3/stringutil"
+	"go.etcd.io/etcd/tests/v3/framework/e2e"
+
+	"github.com/stretchr/testify/require"
+)
+
+// TestReproduce19406 reproduces the issue: https://github.com/etcd-io/etcd/issues/19406
+func TestReproduce19406(t *testing.T) {
+	e2e.BeforeTest(t)
+
+	compactionSleepInterval := 100 * time.Millisecond
+	ctx := context.TODO()
+
+	clus, cerr := e2e.NewEtcdProcessCluster(ctx, t,
+		e2e.WithClusterSize(1),
+		e2e.WithGoFailEnabled(true),
+		e2e.WithCompactionBatchLimit(1),
+		e2e.WithCompactionSleepInterval(compactionSleepInterval),
+	)
+	require.NoError(t, cerr)
+	t.Cleanup(func() { require.NoError(t, clus.Stop()) })
+
+	// Produce some data
+	cli := newClient(t, clus.EndpointsGRPC(), e2e.ClientConfig{})
+	valueSize := 10
+	var latestRevision int64
+
+	produceKeyNum := 20
+	for i := 0; i <= produceKeyNum; i++ {
+		resp, err := cli.Put(ctx, fmt.Sprintf("%d", i), stringutil.RandString(uint(valueSize)))
+		require.NoError(t, err)
+		latestRevision = resp.Header.Revision
+	}
+
+	// Sleep for PerCompactionInterationInterval to simulate a single iteration of compaction lasting at least this duration.
+	PerCompactionInterationInterval := compactionSleepInterval
+	require.NoError(t, clus.Procs[0].Failpoints().SetupHTTP(ctx, "compactAfterAcquiredBatchTxLock",
+		fmt.Sprintf(`sleep("%s")`, PerCompactionInterationInterval)))
+
+	// start compaction
+	t.Log("start compaction...")
+	_, err := cli.Compact(ctx, latestRevision, clientv3.WithCompactPhysical())
+	require.NoError(t, err)
+	t.Log("finished compaction...")
+
+	// Validate that total compaction sleep interval
+	// Compaction runs in batches. During each batch, it acquires a lock, releases it at the end,
+	// and then waits for a compactionSleepInterval before starting the next batch. This pause
+	// allows PUT requests to be processed.
+	// Therefore, the total compaction sleep interval larger or equal to
+	// (compaction iteration number - 1) * compactionSleepInterval
+	httpEndpoint := clus.EndpointsHTTP()[0]
+	totalKeys := produceKeyNum + 1
+	pauseDuration, totalDuration := getEtcdCompactionMetrics(t, httpEndpoint)
+	require.NoError(t, err)
+	actualSleepInterval := time.Duration(totalDuration-pauseDuration) * time.Millisecond
+	expectSleepInterval := compactionSleepInterval * time.Duration(totalKeys)
+	t.Logf("db_compaction_pause_duration: %.2f db_compaction_total_duration: %.2f, totalKeys: %d",
+		pauseDuration, totalDuration, totalKeys)
+	require.GreaterOrEqualf(t, actualSleepInterval, expectSleepInterval,
+		"expect total compact sleep interval larger than (%v) but got (%v)",
+		expectSleepInterval, actualSleepInterval)
+}
+
+func getEtcdCompactionMetrics(t *testing.T, httpEndpoint string) (pauseDuration, totalDuration float64) {
+	metricsURL, err := url.JoinPath(httpEndpoint, "metrics")
+	require.NoError(t, err)
+
+	// Fetch metrics from the endpoint
+	metricFamilies, err := e2e.GetMetrics(metricsURL)
+	require.NoError(t, err)
+
+	// Extract sum from histogram metric
+	getHistogramSum := func(name string) float64 {
+		mf, ok := metricFamilies[name]
+		require.Truef(t, ok, "metric %q not found", name)
+		require.NotEmptyf(t, mf.Metric, "metric %q has no data", name)
+
+		hist := mf.Metric[0].GetHistogram()
+		require.NotEmptyf(t, hist, "metric %q is not a histogram", name)
+
+		return hist.GetSampleSum()
+	}
+
+	pauseDuration = getHistogramSum("etcd_debugging_mvcc_db_compaction_pause_duration_milliseconds")
+	totalDuration = getHistogramSum("etcd_debugging_mvcc_db_compaction_total_duration_milliseconds")
+
+	return pauseDuration, totalDuration
+}
diff --git a/tests/e2e/runtime_reconfiguration_test.go b/tests/e2e/runtime_reconfiguration_test.go
index 1736126e6..650e9358d 100644
--- a/tests/e2e/runtime_reconfiguration_test.go
+++ b/tests/e2e/runtime_reconfiguration_test.go
@@ -60,7 +60,7 @@ func TestRuntimeReconfigGrowClusterSize(t *testing.T) {
 	}
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 			defer cancel()
 
 			epc, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithClusterSize(tc.clusterSize))
@@ -102,7 +102,7 @@ func TestRuntimeReconfigDecreaseClusterSize(t *testing.T) {
 	}
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 			defer cancel()
 
 			epc, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithClusterSize(tc.clusterSize))
@@ -140,7 +140,7 @@ func TestRuntimeReconfigRollingUpgrade(t *testing.T) {
 
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 			defer cancel()
 
 			epc, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithClusterSize(3))
diff --git a/tests/e2e/utils.go b/tests/e2e/utils.go
index 8917bd807..3f4ca015b 100644
--- a/tests/e2e/utils.go
+++ b/tests/e2e/utils.go
@@ -69,13 +69,13 @@ func newClient(t *testing.T, entpoints []string, cfg e2e.ClientConfig) *clientv3
 }
 
 // tlsInfo follows the Client-to-server communication in https://etcd.io/docs/v3.6/op-guide/security/#basic-setup
-func tlsInfo(t testing.TB, cfg e2e.ClientConfig) (*transport.TLSInfo, error) {
+func tlsInfo(tb testing.TB, cfg e2e.ClientConfig) (*transport.TLSInfo, error) {
 	switch cfg.ConnectionType {
 	case e2e.ClientNonTLS, e2e.ClientTLSAndNonTLS:
 		return nil, nil
 	case e2e.ClientTLS:
 		if cfg.AutoTLS {
-			tls, err := transport.SelfCert(zap.NewNop(), t.TempDir(), []string{"localhost"}, 1)
+			tls, err := transport.SelfCert(zap.NewNop(), tb.TempDir(), []string{"localhost"}, 1)
 			if err != nil {
 				return nil, fmt.Errorf("failed to generate cert: %w", err)
 			}
diff --git a/tests/e2e/utl_migrate_test.go b/tests/e2e/utl_migrate_test.go
index 5ee933f0e..d002d4ef7 100644
--- a/tests/e2e/utl_migrate_test.go
+++ b/tests/e2e/utl_migrate_test.go
@@ -17,7 +17,6 @@
 package e2e
 
 import (
-	"context"
 	"fmt"
 	"path/filepath"
 	"strings"
@@ -138,7 +137,7 @@ func TestEtctlutlMigrate(t *testing.T) {
 			}
 			dataDirPath := t.TempDir()
 
-			epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+			epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 				e2e.WithVersion(tc.clusterVersion),
 				e2e.WithDataDirPath(dataDirPath),
 				e2e.WithClusterSize(1),
diff --git a/tests/e2e/v2store_deprecation_test.go b/tests/e2e/v2store_deprecation_test.go
index b267ae240..e65b75bf6 100644
--- a/tests/e2e/v2store_deprecation_test.go
+++ b/tests/e2e/v2store_deprecation_test.go
@@ -39,13 +39,13 @@ import (
 	"go.etcd.io/etcd/tests/v3/framework/e2e"
 )
 
-func writeCustomV2Data(t testing.TB, epc *e2e.EtcdProcessCluster, count int) {
+func writeCustomV2Data(tb testing.TB, epc *e2e.EtcdProcessCluster, count int) {
 	for i := 0; i < count; i++ {
 		if err := e2e.CURLPut(epc, e2e.CURLReq{
 			Endpoint: "/v2/keys/foo", Value: "bar" + fmt.Sprint(i),
 			Expected: expect.ExpectedResponse{Value: `{"action":"set","node":{"key":"/foo","value":"bar` + fmt.Sprint(i)},
 		}); err != nil {
-			t.Fatalf("failed put with curl (%v)", err)
+			tb.Fatalf("failed put with curl (%v)", err)
 		}
 	}
 }
@@ -72,7 +72,7 @@ func TestV2DeprecationWriteOnlyWAL(t *testing.T) {
 		e2e.WithEnableV2(true),
 		e2e.WithDataDirPath(dataDirPath),
 	))
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(cfg))
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 	require.NoError(t, err)
 	memberDataDir := epc.Procs[0].Config().DataDirPath
 
@@ -101,7 +101,7 @@ func TestV2DeprecationWriteOnlySnapshot(t *testing.T) {
 		e2e.WithDataDirPath(dataDirPath),
 		e2e.WithSnapshotCount(10),
 	))
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(cfg))
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 	require.NoError(t, err)
 	memberDataDir := epc.Procs[0].Config().DataDirPath
 
@@ -123,7 +123,7 @@ func TestV2DeprecationSnapshotMatches(t *testing.T) {
 	e2e.BeforeTest(t)
 	lastReleaseData := t.TempDir()
 	currentReleaseData := t.TempDir()
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	if !fileutil.Exist(e2e.BinPath.EtcdLastRelease) {
@@ -156,7 +156,7 @@ func TestV2DeprecationSnapshotMatches(t *testing.T) {
 func TestV2DeprecationSnapshotRecover(t *testing.T) {
 	e2e.BeforeTest(t)
 	dataDir := t.TempDir()
-	ctx, cancel := context.WithCancel(context.Background())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	if !fileutil.Exist(e2e.BinPath.EtcdLastRelease) {
@@ -176,7 +176,7 @@ func TestV2DeprecationSnapshotRecover(t *testing.T) {
 		e2e.WithVersion(e2e.CurrentVersion),
 		e2e.WithDataDirPath(dataDir),
 	))
-	epc, err = e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(cfg))
+	epc, err = e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 	require.NoError(t, err)
 
 	cc = epc.Etcdctl()
@@ -191,47 +191,47 @@ func TestV2DeprecationSnapshotRecover(t *testing.T) {
 	assert.NoError(t, epc.Close())
 }
 
-func runEtcdAndCreateSnapshot(t testing.TB, serverVersion e2e.ClusterVersion, dataDir string, snapshotCount uint64) *e2e.EtcdProcessCluster {
+func runEtcdAndCreateSnapshot(tb testing.TB, serverVersion e2e.ClusterVersion, dataDir string, snapshotCount uint64) *e2e.EtcdProcessCluster {
 	cfg := e2e.ConfigStandalone(*e2e.NewConfig(
 		e2e.WithVersion(serverVersion),
 		e2e.WithDataDirPath(dataDir),
 		e2e.WithSnapshotCount(snapshotCount),
 		e2e.WithKeepDataDir(true),
 	))
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(cfg))
-	assert.NoError(t, err)
+	epc, err := e2e.NewEtcdProcessCluster(tb.Context(), tb, e2e.WithConfig(cfg))
+	assert.NoError(tb, err)
 	return epc
 }
 
-func addAndRemoveKeysAndMembers(ctx context.Context, t testing.TB, cc *e2e.EtcdctlV3, snapshotCount uint64) (members []uint64) {
+func addAndRemoveKeysAndMembers(ctx context.Context, tb testing.TB, cc *e2e.EtcdctlV3, snapshotCount uint64) (members []uint64) {
 	// Execute some non-trivial key&member operation
 	var i uint64
 	for i = 0; i < snapshotCount*3; i++ {
 		err := cc.Put(ctx, fmt.Sprintf("%d", i), "1", config.PutOptions{})
-		require.NoError(t, err)
+		require.NoError(tb, err)
 	}
 	member1, err := cc.MemberAddAsLearner(ctx, "member1", []string{"http://127.0.0.1:2000"})
-	require.NoError(t, err)
+	require.NoError(tb, err)
 	members = append(members, member1.Member.ID)
 
 	for i = 0; i < snapshotCount*2; i++ {
 		_, err = cc.Delete(ctx, fmt.Sprintf("%d", i), config.DeleteOptions{})
-		require.NoError(t, err)
+		require.NoError(tb, err)
 	}
 	_, err = cc.MemberRemove(ctx, member1.Member.ID)
-	require.NoError(t, err)
+	require.NoError(tb, err)
 
 	for i = 0; i < snapshotCount; i++ {
 		err = cc.Put(ctx, fmt.Sprintf("%d", i), "2", config.PutOptions{})
-		require.NoError(t, err)
+		require.NoError(tb, err)
 	}
 	member2, err := cc.MemberAddAsLearner(ctx, "member2", []string{"http://127.0.0.1:2001"})
-	require.NoError(t, err)
+	require.NoError(tb, err)
 	members = append(members, member2.Member.ID)
 
 	for i = 0; i < snapshotCount/2; i++ {
 		err = cc.Put(ctx, fmt.Sprintf("%d", i), "3", config.PutOptions{})
-		assert.NoError(t, err)
+		assert.NoError(tb, err)
 	}
 	return members
 }
@@ -240,39 +240,39 @@ func filterSnapshotFiles(path string) bool {
 	return strings.HasSuffix(path, ".snap")
 }
 
-func assertSnapshotsMatch(t testing.TB, firstDataDir, secondDataDir string, patch func([]byte) []byte) {
-	lg := zaptest.NewLogger(t)
+func assertSnapshotsMatch(tb testing.TB, firstDataDir, secondDataDir string, patch func([]byte) []byte) {
+	lg := zaptest.NewLogger(tb)
 	firstFiles, err := fileutil.ListFiles(firstDataDir, filterSnapshotFiles)
-	require.NoError(t, err)
+	require.NoError(tb, err)
 	secondFiles, err := fileutil.ListFiles(secondDataDir, filterSnapshotFiles)
-	require.NoError(t, err)
-	assert.NotEmpty(t, firstFiles)
-	assert.NotEmpty(t, secondFiles)
-	assert.Equal(t, len(firstFiles), len(secondFiles))
+	require.NoError(tb, err)
+	assert.NotEmpty(tb, firstFiles)
+	assert.NotEmpty(tb, secondFiles)
+	assert.Len(tb, secondFiles, len(firstFiles))
 	sort.Strings(firstFiles)
 	sort.Strings(secondFiles)
 	for i := 0; i < len(firstFiles); i++ {
 		firstSnapshot, err := snap.Read(lg, firstFiles[i])
-		require.NoError(t, err)
+		require.NoError(tb, err)
 		secondSnapshot, err := snap.Read(lg, secondFiles[i])
-		require.NoError(t, err)
-		assertMembershipEqual(t, openSnap(patch(firstSnapshot.Data)), openSnap(patch(secondSnapshot.Data)))
+		require.NoError(tb, err)
+		assertMembershipEqual(tb, openSnap(patch(firstSnapshot.Data)), openSnap(patch(secondSnapshot.Data)))
 	}
 }
 
-func assertMembershipEqual(t testing.TB, firstStore v2store.Store, secondStore v2store.Store) {
-	rc1 := membership.NewCluster(zaptest.NewLogger(t))
+func assertMembershipEqual(tb testing.TB, firstStore v2store.Store, secondStore v2store.Store) {
+	rc1 := membership.NewCluster(zaptest.NewLogger(tb))
 	rc1.SetStore(firstStore)
 	rc1.Recover(func(lg *zap.Logger, v *semver.Version) {})
 
-	rc2 := membership.NewCluster(zaptest.NewLogger(t))
+	rc2 := membership.NewCluster(zaptest.NewLogger(tb))
 	rc2.SetStore(secondStore)
 	rc2.Recover(func(lg *zap.Logger, v *semver.Version) {})
 
 	// membership should match
 	if !reflect.DeepEqual(rc1.Members(), rc2.Members()) {
-		t.Logf("memberids_from_last_version = %+v, member_ids_from_current_version = %+v", rc1.MemberIDs(), rc2.MemberIDs())
-		t.Errorf("members_from_last_version_snapshot = %+v, members_from_current_version_snapshot %+v", rc1.Members(), rc2.Members())
+		tb.Logf("memberids_from_last_version = %+v, member_ids_from_current_version = %+v", rc1.MemberIDs(), rc2.MemberIDs())
+		tb.Errorf("members_from_last_version_snapshot = %+v, members_from_current_version_snapshot %+v", rc1.Members(), rc2.Members())
 	}
 }
 
diff --git a/tests/e2e/v3_curl_maxstream_test.go b/tests/e2e/v3_curl_maxstream_test.go
index 027a3b538..0833fdbd3 100644
--- a/tests/e2e/v3_curl_maxstream_test.go
+++ b/tests/e2e/v3_curl_maxstream_test.go
@@ -88,7 +88,7 @@ func testCurlV3MaxStream(t *testing.T, reachLimit bool, opts ...ctlOption) {
 
 	// Step 2: create the cluster
 	t.Log("Creating an etcd cluster")
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t, e2e.WithConfig(&cx.cfg))
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(&cx.cfg))
 	require.NoErrorf(t, err, "Failed to start etcd cluster")
 	cx.epc = epc
 	cx.dataDir = epc.Procs[0].Config().DataDirPath
diff --git a/tests/e2e/v3_curl_watch_test.go b/tests/e2e/v3_curl_watch_test.go
index f6ced632c..f0c20b857 100644
--- a/tests/e2e/v3_curl_watch_test.go
+++ b/tests/e2e/v3_curl_watch_test.go
@@ -54,7 +54,7 @@ func testCurlV3Watch(cx ctlCtx) {
 func TestCurlWatchIssue19509(t *testing.T) {
 	e2e.BeforeTest(t)
 
-	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
 	defer cancel()
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithConfig(e2e.NewConfigClientTLS()), e2e.WithClusterSize(1))
 	require.NoError(t, err)
diff --git a/tests/e2e/v3_lease_no_proxy_test.go b/tests/e2e/v3_lease_no_proxy_test.go
index c6418c869..989700587 100644
--- a/tests/e2e/v3_lease_no_proxy_test.go
+++ b/tests/e2e/v3_lease_no_proxy_test.go
@@ -56,7 +56,7 @@ func TestLeaseRevoke_ClientSwitchToOtherMember(t *testing.T) {
 func testLeaseRevokeIssue(t *testing.T, clusterSize int, connectToOneFollower bool) {
 	e2e.BeforeTest(t)
 
-	ctx := context.Background()
+	ctx := t.Context()
 
 	t.Log("Starting a new etcd cluster")
 	epc, err := e2e.NewEtcdProcessCluster(ctx, t,
@@ -126,7 +126,7 @@ func testLeaseRevokeIssue(t *testing.T, clusterSize int, connectToOneFollower bo
 	err = epc.Procs[leaderIdx].Failpoints().SetupHTTP(ctx, "raftBeforeSave", `sleep("30s")`)
 	require.NoError(t, err)
 
-	cctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	cctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
 	t.Logf("Waiting for a new leader to be elected, old leader index: %d, old leader ID: %d", leaderIdx, oldLeaderID)
 	testutils.ExecuteUntil(cctx, t, func() {
 		for {
diff --git a/tests/e2e/watch_test.go b/tests/e2e/watch_test.go
index 758177c64..c628a0cc8 100644
--- a/tests/e2e/watch_test.go
+++ b/tests/e2e/watch_test.go
@@ -95,13 +95,13 @@ func TestWatchDelayForPeriodicProgressNotification(t *testing.T) {
 		cfg.Client = tc.client
 		cfg.ClientHTTPSeparate = tc.clientHTTPSeparate
 		t.Run(tc.name, func(t *testing.T) {
-			clus, err := e2e.NewEtcdProcessCluster(context.Background(), t, e2e.WithConfig(cfg))
+			clus, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 			require.NoError(t, err)
 			defer clus.Close()
 			c := newClient(t, clus.EndpointsGRPC(), tc.client)
-			require.NoError(t, fillEtcdWithData(context.Background(), c, tc.dbSizeBytes))
+			require.NoError(t, fillEtcdWithData(t.Context(), c, tc.dbSizeBytes))
 
-			ctx, cancel := context.WithTimeout(context.Background(), watchTestDuration)
+			ctx, cancel := context.WithTimeout(t.Context(), watchTestDuration)
 			defer cancel()
 			g := errgroup.Group{}
 			continuouslyExecuteGetAll(ctx, t, &g, c)
@@ -120,13 +120,13 @@ func TestWatchDelayForManualProgressNotification(t *testing.T) {
 		cfg.Client = tc.client
 		cfg.ClientHTTPSeparate = tc.clientHTTPSeparate
 		t.Run(tc.name, func(t *testing.T) {
-			clus, err := e2e.NewEtcdProcessCluster(context.Background(), t, e2e.WithConfig(cfg))
+			clus, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 			require.NoError(t, err)
 			defer clus.Close()
 			c := newClient(t, clus.EndpointsGRPC(), tc.client)
-			require.NoError(t, fillEtcdWithData(context.Background(), c, tc.dbSizeBytes))
+			require.NoError(t, fillEtcdWithData(t.Context(), c, tc.dbSizeBytes))
 
-			ctx, cancel := context.WithTimeout(context.Background(), watchTestDuration)
+			ctx, cancel := context.WithTimeout(t.Context(), watchTestDuration)
 			defer cancel()
 			g := errgroup.Group{}
 			continuouslyExecuteGetAll(ctx, t, &g, c)
@@ -157,13 +157,13 @@ func TestWatchDelayForEvent(t *testing.T) {
 		cfg.Client = tc.client
 		cfg.ClientHTTPSeparate = tc.clientHTTPSeparate
 		t.Run(tc.name, func(t *testing.T) {
-			clus, err := e2e.NewEtcdProcessCluster(context.Background(), t, e2e.WithConfig(cfg))
+			clus, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 			require.NoError(t, err)
 			defer clus.Close()
 			c := newClient(t, clus.EndpointsGRPC(), tc.client)
-			require.NoError(t, fillEtcdWithData(context.Background(), c, tc.dbSizeBytes))
+			require.NoError(t, fillEtcdWithData(t.Context(), c, tc.dbSizeBytes))
 
-			ctx, cancel := context.WithTimeout(context.Background(), watchTestDuration)
+			ctx, cancel := context.WithTimeout(t.Context(), watchTestDuration)
 			defer cancel()
 			g := errgroup.Group{}
 			g.Go(func() error {
@@ -270,14 +270,14 @@ func TestDeleteEventDrop_Issue18089(t *testing.T) {
 	cfg := e2e.DefaultConfig()
 	cfg.ClusterSize = 1
 	cfg.Client = e2e.ClientConfig{ConnectionType: e2e.ClientTLS}
-	clus, err := e2e.NewEtcdProcessCluster(context.Background(), t, e2e.WithConfig(cfg))
+	clus, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg))
 	require.NoError(t, err)
 	defer clus.Close()
 
 	c := newClient(t, clus.EndpointsGRPC(), cfg.Client)
 	defer c.Close()
 
-	ctx := context.Background()
+	ctx := t.Context()
 	const (
 		key = "k"
 		v2  = "v2"
@@ -345,14 +345,14 @@ func testStartWatcherFromCompactedRevision(t *testing.T, performCompactOnTombsto
 	e2e.BeforeTest(t)
 	cfg := e2e.DefaultConfig()
 	cfg.Client = e2e.ClientConfig{ConnectionType: e2e.ClientTLS}
-	clus, err := e2e.NewEtcdProcessCluster(context.Background(), t, e2e.WithConfig(cfg), e2e.WithClusterSize(1))
+	clus, err := e2e.NewEtcdProcessCluster(t.Context(), t, e2e.WithConfig(cfg), e2e.WithClusterSize(1))
 	require.NoError(t, err)
 	defer clus.Close()
 
 	c := newClient(t, clus.EndpointsGRPC(), cfg.Client)
 	defer c.Close()
 
-	ctx := context.Background()
+	ctx := t.Context()
 	key := "foo"
 	totalRev := 100
 
@@ -494,11 +494,11 @@ func testStartWatcherFromCompactedRevision(t *testing.T, performCompactOnTombsto
 func TestResumeCompactionOnTombstone(t *testing.T) {
 	e2e.BeforeTest(t)
 
-	ctx := context.Background()
+	ctx := t.Context()
 	compactBatchLimit := 5
 
 	cfg := e2e.DefaultConfig()
-	clus, err := e2e.NewEtcdProcessCluster(context.Background(),
+	clus, err := e2e.NewEtcdProcessCluster(t.Context(),
 		t,
 		e2e.WithConfig(cfg),
 		e2e.WithClusterSize(1),
diff --git a/tests/e2e/zap_logging_test.go b/tests/e2e/zap_logging_test.go
index 6752a23cc..0d7b7ffed 100644
--- a/tests/e2e/zap_logging_test.go
+++ b/tests/e2e/zap_logging_test.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"encoding/json"
 	"testing"
 	"time"
@@ -29,7 +28,7 @@ import (
 func TestServerJsonLogging(t *testing.T) {
 	e2e.BeforeTest(t)
 
-	epc, err := e2e.NewEtcdProcessCluster(context.TODO(), t,
+	epc, err := e2e.NewEtcdProcessCluster(t.Context(), t,
 		e2e.WithClusterSize(1),
 		e2e.WithLogLevel("debug"),
 	)
@@ -115,7 +114,7 @@ func TestConnectionRejectMessage(t *testing.T) {
 			t.Log("Starting an etcd process and wait for it to get ready.")
 			p, err := e2e.SpawnCmd(commonArgs, nil)
 			require.NoError(t, err)
-			err = e2e.WaitReadyExpectProc(context.TODO(), p, e2e.EtcdServerReadyLines)
+			err = e2e.WaitReadyExpectProc(t.Context(), p, e2e.EtcdServerReadyLines)
 			require.NoError(t, err)
 			defer func() {
 				p.Stop()
@@ -127,7 +126,7 @@ func TestConnectionRejectMessage(t *testing.T) {
 			doneCh := make(chan struct{}, 1)
 			go func() {
 				startedCh <- struct{}{}
-				verr := e2e.WaitReadyExpectProc(context.TODO(), p, []string{tc.expectedErrMsg})
+				verr := e2e.WaitReadyExpectProc(t.Context(), p, []string{tc.expectedErrMsg})
 				assert.NoError(t, verr)
 				doneCh <- struct{}{}
 			}()
diff --git a/tests/framework/e2e/cluster.go b/tests/framework/e2e/cluster.go
index 5af564072..6e667f641 100644
--- a/tests/framework/e2e/cluster.go
+++ b/tests/framework/e2e/cluster.go
@@ -425,23 +425,23 @@ func WithExtensiveMetrics() EPClusterOption {
 
 // NewEtcdProcessCluster launches a new cluster from etcd processes, returning
 // a new EtcdProcessCluster once all nodes are ready to accept client requests.
-func NewEtcdProcessCluster(ctx context.Context, t testing.TB, opts ...EPClusterOption) (*EtcdProcessCluster, error) {
+func NewEtcdProcessCluster(ctx context.Context, tb testing.TB, opts ...EPClusterOption) (*EtcdProcessCluster, error) {
 	cfg := NewConfig(opts...)
-	epc, err := InitEtcdProcessCluster(t, cfg)
+	epc, err := InitEtcdProcessCluster(tb, cfg)
 	if err != nil {
 		return nil, err
 	}
 
-	return StartEtcdProcessCluster(ctx, t, epc, cfg)
+	return StartEtcdProcessCluster(ctx, tb, epc, cfg)
 }
 
 // InitEtcdProcessCluster initializes a new cluster based on the given config.
 // It doesn't start the cluster.
-func InitEtcdProcessCluster(t testing.TB, cfg *EtcdProcessClusterConfig) (*EtcdProcessCluster, error) {
-	SkipInShortMode(t)
+func InitEtcdProcessCluster(tb testing.TB, cfg *EtcdProcessClusterConfig) (*EtcdProcessCluster, error) {
+	SkipInShortMode(tb)
 
 	if cfg.Logger == nil {
-		cfg.Logger = zaptest.NewLogger(t)
+		cfg.Logger = zaptest.NewLogger(tb)
 	}
 	if cfg.BasePort == 0 {
 		cfg.BasePort = EtcdProcessBasePort
@@ -460,17 +460,17 @@ func InitEtcdProcessCluster(t testing.TB, cfg *EtcdProcessClusterConfig) (*EtcdP
 		}
 	}
 
-	etcdCfgs := cfg.EtcdAllServerProcessConfigs(t)
+	etcdCfgs := cfg.EtcdAllServerProcessConfigs(tb)
 	epc := &EtcdProcessCluster{
 		Cfg:     cfg,
-		lg:      zaptest.NewLogger(t),
+		lg:      zaptest.NewLogger(tb),
 		Procs:   make([]EtcdProcess, cfg.ClusterSize),
 		nextSeq: cfg.ClusterSize,
 	}
 
 	// launch etcd processes
 	for i := range etcdCfgs {
-		proc, err := NewEtcdProcess(t, etcdCfgs[i])
+		proc, err := NewEtcdProcess(tb, etcdCfgs[i])
 		if err != nil {
 			epc.Close()
 			return nil, fmt.Errorf("cannot configure: %w", err)
@@ -482,7 +482,7 @@ func InitEtcdProcessCluster(t testing.TB, cfg *EtcdProcessClusterConfig) (*EtcdP
 }
 
 // StartEtcdProcessCluster launches a new cluster from etcd processes.
-func StartEtcdProcessCluster(ctx context.Context, t testing.TB, epc *EtcdProcessCluster, cfg *EtcdProcessClusterConfig) (*EtcdProcessCluster, error) {
+func StartEtcdProcessCluster(ctx context.Context, tb testing.TB, epc *EtcdProcessCluster, cfg *EtcdProcessClusterConfig) (*EtcdProcessCluster, error) {
 	if cfg.RollingStart {
 		if err := epc.RollingStart(ctx); err != nil {
 			return nil, fmt.Errorf("cannot rolling-start: %w", err)
@@ -496,11 +496,11 @@ func StartEtcdProcessCluster(ctx context.Context, t testing.TB, epc *EtcdProcess
 	for _, proc := range epc.Procs {
 		if cfg.GoFailEnabled && !proc.Failpoints().Enabled() {
 			epc.Close()
-			t.Skip("please run 'make gofail-enable && make build' before running the test")
+			tb.Skip("please run 'make gofail-enable && make build' before running the test")
 		}
 	}
 	if cfg.InitialLeaderIndex >= 0 {
-		if err := epc.MoveLeader(ctx, t, cfg.InitialLeaderIndex); err != nil {
+		if err := epc.MoveLeader(ctx, tb, cfg.InitialLeaderIndex); err != nil {
 			return nil, fmt.Errorf("failed to move leader: %w", err)
 		}
 	}
@@ -1078,29 +1078,29 @@ func findMemberIDByEndpoint(members []*etcdserverpb.Member, endpoint string) (ui
 
 // WaitLeader returns index of the member in c.Members() that is leader
 // or fails the test (if not established in 30s).
-func (epc *EtcdProcessCluster) WaitLeader(t testing.TB) int {
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+func (epc *EtcdProcessCluster) WaitLeader(tb testing.TB) int {
+	ctx, cancel := context.WithTimeout(tb.Context(), 30*time.Second)
 	defer cancel()
-	return epc.WaitMembersForLeader(ctx, t, epc.Procs)
+	return epc.WaitMembersForLeader(ctx, tb, epc.Procs)
 }
 
 // WaitMembersForLeader waits until given members agree on the same leader,
 // and returns its 'index' in the 'membs' list
-func (epc *EtcdProcessCluster) WaitMembersForLeader(ctx context.Context, t testing.TB, membs []EtcdProcess) int {
+func (epc *EtcdProcessCluster) WaitMembersForLeader(ctx context.Context, tb testing.TB, membs []EtcdProcess) int {
 	cc := epc.Etcdctl()
 
 	// ensure leader is up via linearizable get
 	for {
 		select {
 		case <-ctx.Done():
-			t.Fatal("WaitMembersForLeader timeout")
+			tb.Fatal("WaitMembersForLeader timeout")
 		default:
 		}
 		_, err := cc.Get(ctx, "0", config.GetOptions{Timeout: 10*config.TickDuration + time.Second})
 		if err == nil || strings.Contains(err.Error(), "Key not found") {
 			break
 		}
-		t.Logf("WaitMembersForLeader Get err: %v", err)
+		tb.Logf("WaitMembersForLeader Get err: %v", err)
 	}
 
 	leaders := make(map[uint64]struct{})
@@ -1108,7 +1108,7 @@ func (epc *EtcdProcessCluster) WaitMembersForLeader(ctx context.Context, t testi
 	for {
 		select {
 		case <-ctx.Done():
-			t.Fatal("WaitMembersForLeader timeout")
+			tb.Fatal("WaitMembersForLeader timeout")
 		default:
 		}
 		for i := range membs {
@@ -1118,7 +1118,7 @@ func (epc *EtcdProcessCluster) WaitMembersForLeader(ctx context.Context, t testi
 					// if member[i] has stopped
 					continue
 				}
-				t.Fatal(err)
+				tb.Fatal(err)
 			}
 			members[resp[0].Header.MemberId] = i
 			leaders[resp[0].Leader] = struct{}{}
@@ -1133,24 +1133,24 @@ func (epc *EtcdProcessCluster) WaitMembersForLeader(ctx context.Context, t testi
 	}
 	for l := range leaders {
 		if index, ok := members[l]; ok {
-			t.Logf("members agree on a leader, members:%v , leader:%v", members, l)
+			tb.Logf("members agree on a leader, members:%v , leader:%v", members, l)
 			return index
 		}
-		t.Fatalf("members agree on a leader which is not one of members, members:%v , leader:%v", members, l)
+		tb.Fatalf("members agree on a leader which is not one of members, members:%v , leader:%v", members, l)
 	}
-	t.Fatal("impossible path of execution")
+	tb.Fatal("impossible path of execution")
 	return -1
 }
 
 // MoveLeader moves the leader to the ith process.
-func (epc *EtcdProcessCluster) MoveLeader(ctx context.Context, t testing.TB, i int) error {
+func (epc *EtcdProcessCluster) MoveLeader(ctx context.Context, tb testing.TB, i int) error {
 	if i < 0 || i >= len(epc.Procs) {
 		return fmt.Errorf("invalid index: %d, must between 0 and %d", i, len(epc.Procs)-1)
 	}
-	t.Logf("moving leader to Procs[%d]", i)
-	oldLeader := epc.WaitMembersForLeader(ctx, t, epc.Procs)
+	tb.Logf("moving leader to Procs[%d]", i)
+	oldLeader := epc.WaitMembersForLeader(ctx, tb, epc.Procs)
 	if oldLeader == i {
-		t.Logf("Procs[%d] is already the leader", i)
+		tb.Logf("Procs[%d] is already the leader", i)
 		return nil
 	}
 	resp, err := epc.Procs[i].Etcdctl().Status(ctx)
@@ -1162,10 +1162,10 @@ func (epc *EtcdProcessCluster) MoveLeader(ctx context.Context, t testing.TB, i i
 	if err != nil {
 		return err
 	}
-	newLeader := epc.WaitMembersForLeader(ctx, t, epc.Procs)
+	newLeader := epc.WaitMembersForLeader(ctx, tb, epc.Procs)
 	if newLeader != i {
-		t.Fatalf("expect new leader to be Procs[%d] but got Procs[%d]", i, newLeader)
+		tb.Fatalf("expect new leader to be Procs[%d] but got Procs[%d]", i, newLeader)
 	}
-	t.Logf("moved leader from Procs[%d] to Procs[%d]", oldLeader, i)
+	tb.Logf("moved leader from Procs[%d] to Procs[%d]", oldLeader, i)
 	return nil
 }
diff --git a/tests/framework/e2e/cluster_direct.go b/tests/framework/e2e/cluster_direct.go
index 70c60dbf4..ea46ea31d 100644
--- a/tests/framework/e2e/cluster_direct.go
+++ b/tests/framework/e2e/cluster_direct.go
@@ -18,6 +18,6 @@ package e2e
 
 import "testing"
 
-func NewEtcdProcess(t testing.TB, cfg *EtcdServerProcessConfig) (EtcdProcess, error) {
-	return NewEtcdServerProcess(t, cfg)
+func NewEtcdProcess(tb testing.TB, cfg *EtcdServerProcessConfig) (EtcdProcess, error) {
+	return NewEtcdServerProcess(tb, cfg)
 }
diff --git a/tests/framework/e2e/config.go b/tests/framework/e2e/config.go
index acc1d82e0..646a868e9 100644
--- a/tests/framework/e2e/config.go
+++ b/tests/framework/e2e/config.go
@@ -23,6 +23,13 @@ const (
 	LastVersion         ClusterVersion = "last-version"
 )
 
+func (cv ClusterVersion) String() string {
+	if cv == CurrentVersion {
+		return "current-version"
+	}
+	return string(cv)
+}
+
 type ClusterContext struct {
 	Version ClusterVersion
 }
diff --git a/tests/framework/e2e/downgrade.go b/tests/framework/e2e/downgrade.go
index 1f9e71ec2..432a422f0 100644
--- a/tests/framework/e2e/downgrade.go
+++ b/tests/framework/e2e/downgrade.go
@@ -15,7 +15,6 @@
 package e2e
 
 import (
-	"context"
 	"encoding/json"
 	"fmt"
 	"math/rand"
@@ -26,6 +25,7 @@ import (
 	"github.com/coreos/go-semver/semver"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/zap"
+	"golang.org/x/sync/errgroup"
 
 	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
 	"go.etcd.io/etcd/api/v3/version"
@@ -37,7 +37,7 @@ func DowngradeEnable(t *testing.T, epc *EtcdProcessCluster, ver *semver.Version)
 	t.Logf("etcdctl downgrade enable %s", ver.String())
 	c := epc.Etcdctl()
 	testutils.ExecuteWithTimeout(t, 20*time.Second, func() {
-		err := c.DowngradeEnable(context.TODO(), ver.String())
+		err := c.DowngradeEnable(t.Context(), ver.String())
 		require.NoError(t, err)
 	})
 
@@ -60,7 +60,7 @@ func DowngradeCancel(t *testing.T, epc *EtcdProcessCluster) {
 	testutils.ExecuteWithTimeout(t, 1*time.Minute, func() {
 		for {
 			t.Logf("etcdctl downgrade cancel")
-			err = c.DowngradeCancel(context.TODO())
+			err = c.DowngradeCancel(t.Context())
 			if err != nil {
 				if strings.Contains(err.Error(), "no inflight downgrade job") {
 					// cancellation has been performed successfully
@@ -93,7 +93,7 @@ func ValidateDowngradeInfo(t *testing.T, clus *EtcdProcessCluster, expected *pb.
 
 		testutils.ExecuteWithTimeout(t, 1*time.Minute, func() {
 			for {
-				statuses, err := mc.Status(context.Background())
+				statuses, err := mc.Status(t.Context())
 				if err != nil {
 					cfg.Logger.Warn("failed to get member status and retrying",
 						zap.Error(err),
@@ -146,6 +146,8 @@ func DowngradeUpgradeMembersByID(t *testing.T, lg *zap.Logger, clus *EtcdProcess
 		opString = "downgrading"
 		newExecPath = BinPath.EtcdLastRelease
 	}
+
+	g := new(errgroup.Group)
 	for _, memberID := range membersToChange {
 		member := clus.Procs[memberID]
 		if member.Config().ExecPath == newExecPath {
@@ -157,10 +159,15 @@ func DowngradeUpgradeMembersByID(t *testing.T, lg *zap.Logger, clus *EtcdProcess
 		}
 		member.Config().ExecPath = newExecPath
 		lg.Info("Restarting member", zap.String("member", member.Config().Name))
-		err := member.Start(context.TODO())
-		if err != nil {
-			return err
-		}
+		// We shouldn't block on waiting for the member to be ready,
+		// otherwise it will be blocked forever if other members are
+		// not started yet.
+		g.Go(func() error {
+			return member.Start(t.Context())
+		})
+	}
+	if err := g.Wait(); err != nil {
+		return err
 	}
 
 	t.Log("Waiting health interval to make sure the leader propagates version to new processes")
diff --git a/tests/framework/e2e/e2e.go b/tests/framework/e2e/e2e.go
index f78df5792..6f3954d5d 100644
--- a/tests/framework/e2e/e2e.go
+++ b/tests/framework/e2e/e2e.go
@@ -39,11 +39,11 @@ func (e e2eRunner) TestMain(m *testing.M) {
 	os.Exit(v)
 }
 
-func (e e2eRunner) BeforeTest(t testing.TB) {
-	BeforeTest(t)
+func (e e2eRunner) BeforeTest(tb testing.TB) {
+	BeforeTest(tb)
 }
 
-func (e e2eRunner) NewCluster(ctx context.Context, t testing.TB, opts ...config.ClusterOption) intf.Cluster {
+func (e e2eRunner) NewCluster(ctx context.Context, tb testing.TB, opts ...config.ClusterOption) intf.Cluster {
 	cfg := config.NewClusterConfig(opts...)
 	e2eConfig := NewConfig(
 		WithClusterSize(cfg.ClusterSize),
@@ -68,7 +68,7 @@ func (e e2eRunner) NewCluster(ctx context.Context, t testing.TB, opts ...config.
 		e2eConfig.Client.AutoTLS = false
 		e2eConfig.Client.ConnectionType = ClientTLS
 	default:
-		t.Fatalf("ClientTLS config %q not supported", cfg.ClientTLS)
+		tb.Fatalf("ClientTLS config %q not supported", cfg.ClientTLS)
 	}
 	switch cfg.PeerTLS {
 	case config.NoTLS:
@@ -81,13 +81,13 @@ func (e e2eRunner) NewCluster(ctx context.Context, t testing.TB, opts ...config.
 		e2eConfig.IsPeerTLS = true
 		e2eConfig.IsPeerAutoTLS = false
 	default:
-		t.Fatalf("PeerTLS config %q not supported", cfg.PeerTLS)
+		tb.Fatalf("PeerTLS config %q not supported", cfg.PeerTLS)
 	}
-	epc, err := NewEtcdProcessCluster(ctx, t, WithConfig(e2eConfig))
+	epc, err := NewEtcdProcessCluster(ctx, tb, WithConfig(e2eConfig))
 	if err != nil {
-		t.Fatalf("could not start etcd integrationCluster: %s", err)
+		tb.Fatalf("could not start etcd integrationCluster: %s", err)
 	}
-	return &e2eCluster{t, *epc}
+	return &e2eCluster{tb, *epc}
 }
 
 type e2eCluster struct {
diff --git a/tests/framework/e2e/etcd_process.go b/tests/framework/e2e/etcd_process.go
index 0da62846f..6d8dd3036 100644
--- a/tests/framework/e2e/etcd_process.go
+++ b/tests/framework/e2e/etcd_process.go
@@ -103,7 +103,7 @@ type EtcdServerProcessConfig struct {
 	Proxy         *proxy.ServerConfig
 }
 
-func NewEtcdServerProcess(t testing.TB, cfg *EtcdServerProcessConfig) (*EtcdServerProcess, error) {
+func NewEtcdServerProcess(tb testing.TB, cfg *EtcdServerProcessConfig) (*EtcdServerProcess, error) {
 	if !fileutil.Exist(cfg.ExecPath) {
 		return nil, fmt.Errorf("could not find etcd binary: %s", cfg.ExecPath)
 	}
@@ -123,7 +123,7 @@ func NewEtcdServerProcess(t testing.TB, cfg *EtcdServerProcessConfig) (*EtcdServ
 		}
 	}
 	if cfg.LazyFSEnabled {
-		ep.lazyfs = newLazyFS(cfg.lg, cfg.DataDirPath, t)
+		ep.lazyfs = newLazyFS(cfg.lg, cfg.DataDirPath, tb)
 	}
 	return ep, nil
 }
@@ -321,7 +321,7 @@ func (ep *EtcdServerProcess) IsRunning() bool {
 func AssertProcessLogs(t *testing.T, ep EtcdProcess, expectLog string) {
 	t.Helper()
 	var err error
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
 	defer cancel()
 	_, err = ep.Logs().ExpectWithContext(ctx, expect.ExpectedResponse{Value: expectLog})
 	if err != nil {
diff --git a/tests/framework/e2e/etcdctl.go b/tests/framework/e2e/etcdctl.go
index 9235ab28d..e212e447c 100644
--- a/tests/framework/e2e/etcdctl.go
+++ b/tests/framework/e2e/etcdctl.go
@@ -56,6 +56,7 @@ func NewEtcdctl(cfg ClientConfig, endpoints []string, opts ...config.ClientOptio
 			DialOptions: []grpc.DialOption{grpc.WithBlock()},
 			Username:    ctl.authConfig.Username,
 			Password:    ctl.authConfig.Password,
+			Token:       ctl.authConfig.Token,
 		})
 		if err != nil {
 			return nil, err
@@ -74,6 +75,13 @@ func WithAuth(userName, password string) config.ClientOption {
 	}
 }
 
+func WithAuthToken(token string) config.ClientOption {
+	return func(c any) {
+		ctl := c.(*EtcdctlV3)
+		ctl.authConfig.Token = token
+	}
+}
+
 func WithEndpoints(endpoints []string) config.ClientOption {
 	return func(c any) {
 		ctl := c.(*EtcdctlV3)
@@ -350,7 +358,9 @@ func (ctl *EtcdctlV3) flags() map[string]string {
 		}
 	}
 	fmap["endpoints"] = strings.Join(ctl.endpoints, ",")
-	if !ctl.authConfig.Empty() {
+	if ctl.authConfig.Token != "" {
+		fmap["auth-jwt-token"] = ctl.authConfig.Token
+	} else if !ctl.authConfig.Empty() {
 		fmap["user"] = ctl.authConfig.Username + ":" + ctl.authConfig.Password
 	}
 	return fmap
diff --git a/tests/framework/e2e/metrics.go b/tests/framework/e2e/metrics.go
new file mode 100644
index 000000000..e28661206
--- /dev/null
+++ b/tests/framework/e2e/metrics.go
@@ -0,0 +1,41 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package e2e
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+
+	dto "github.com/prometheus/client_model/go"
+	"github.com/prometheus/common/expfmt"
+)
+
+func GetMetrics(metricsURL string) (map[string]*dto.MetricFamily, error) {
+	httpClient := http.Client{Transport: &http.Transport{}}
+	resp, err := httpClient.Get(metricsURL)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	data, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	var parser expfmt.TextParser
+	return parser.TextToMetricFamilies(bytes.NewReader(data))
+}
diff --git a/tests/framework/e2e/testing.go b/tests/framework/e2e/testing.go
index 7d7de27fd..99aa8dac6 100644
--- a/tests/framework/e2e/testing.go
+++ b/tests/framework/e2e/testing.go
@@ -20,7 +20,7 @@ import (
 	"go.etcd.io/etcd/client/pkg/v3/testutil"
 )
 
-func BeforeTest(t testing.TB) {
-	SkipInShortMode(t)
-	testutil.BeforeTest(t)
+func BeforeTest(tb testing.TB) {
+	SkipInShortMode(tb)
+	testutil.BeforeTest(tb)
 }
diff --git a/tests/framework/e2e/util.go b/tests/framework/e2e/util.go
index d72f2d493..94e1cd6a8 100644
--- a/tests/framework/e2e/util.go
+++ b/tests/framework/e2e/util.go
@@ -150,8 +150,8 @@ func ToTLS(s string) string {
 	return s
 }
 
-func SkipInShortMode(t testing.TB) {
-	testutil.SkipTestIfShortMode(t, "e2e tests are not running in --short mode")
+func SkipInShortMode(tb testing.TB) {
+	testutil.SkipTestIfShortMode(tb, "e2e tests are not running in --short mode")
 }
 
 func mergeEnvVariables(envVars map[string]string) []string {
diff --git a/tests/framework/integration/cluster.go b/tests/framework/integration/cluster.go
index 8b0f2b549..4ff6d83ed 100644
--- a/tests/framework/integration/cluster.go
+++ b/tests/framework/integration/cluster.go
@@ -412,23 +412,23 @@ func (c *Cluster) WaitMembersMatch(t testutil.TB, membs []*pb.Member) {
 
 // WaitLeader returns index of the member in c.Members that is leader
 // or fails the test (if not established in 30s).
-func (c *Cluster) WaitLeader(t testing.TB) int {
-	return c.WaitMembersForLeader(t, c.Members)
+func (c *Cluster) WaitLeader(tb testing.TB) int {
+	return c.WaitMembersForLeader(tb, c.Members)
 }
 
 // WaitMembersForLeader waits until given members agree on the same leader,
 // and returns its 'index' in the 'membs' list
-func (c *Cluster) WaitMembersForLeader(t testing.TB, membs []*Member) int {
-	t.Logf("WaitMembersForLeader")
-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+func (c *Cluster) WaitMembersForLeader(tb testing.TB, membs []*Member) int {
+	tb.Logf("WaitMembersForLeader")
+	ctx, cancel := context.WithTimeout(tb.Context(), 30*time.Second)
 	defer cancel()
 	l := 0
-	for l = c.waitMembersForLeader(ctx, t, membs); l < 0; {
+	for l = c.waitMembersForLeader(ctx, tb, membs); l < 0; {
 		if ctx.Err() != nil {
-			t.Fatalf("WaitLeader FAILED: %v", ctx.Err())
+			tb.Fatalf("WaitLeader FAILED: %v", ctx.Err())
 		}
 	}
-	t.Logf("WaitMembersForLeader succeeded. Cluster leader index: %v", l)
+	tb.Logf("WaitMembersForLeader succeeded. Cluster leader index: %v", l)
 
 	// TODO: Consider second pass check as sometimes leadership is lost
 	// soon after election:
@@ -444,15 +444,15 @@ func (c *Cluster) WaitMembersForLeader(t testing.TB, membs []*Member) int {
 
 // WaitMembersForLeader waits until given members agree on the same leader,
 // and returns its 'index' in the 'membs' list
-func (c *Cluster) waitMembersForLeader(ctx context.Context, t testing.TB, membs []*Member) int {
+func (c *Cluster) waitMembersForLeader(ctx context.Context, tb testing.TB, membs []*Member) int {
 	possibleLead := make(map[uint64]bool)
 	var lead uint64
 	for _, m := range membs {
 		possibleLead[uint64(m.Server.MemberID())] = true
 	}
-	cc, err := c.ClusterClient(t)
+	cc, err := c.ClusterClient(tb)
 	if err != nil {
-		t.Fatal(err)
+		tb.Fatal(err)
 	}
 	// ensure leader is up via linearizable get
 	for {
@@ -483,12 +483,12 @@ func (c *Cluster) waitMembersForLeader(ctx context.Context, t testing.TB, membs
 
 	for i, m := range membs {
 		if uint64(m.Server.MemberID()) == lead {
-			t.Logf("waitMembersForLeader found leader. Member: %v lead: %x", i, lead)
+			tb.Logf("waitMembersForLeader found leader. Member: %v lead: %x", i, lead)
 			return i
 		}
 	}
 
-	t.Logf("waitMembersForLeader failed (-1)")
+	tb.Logf("waitMembersForLeader failed (-1)")
 	return -1
 }
 
@@ -1457,7 +1457,7 @@ func (c *Cluster) Endpoints() []string {
 	return endpoints
 }
 
-func (c *Cluster) ClusterClient(t testing.TB, opts ...framecfg.ClientOption) (client *clientv3.Client, err error) {
+func (c *Cluster) ClusterClient(tb testing.TB, opts ...framecfg.ClientOption) (client *clientv3.Client, err error) {
 	cfg, err := c.newClientCfg()
 	if err != nil {
 		return nil, err
@@ -1469,7 +1469,7 @@ func (c *Cluster) ClusterClient(t testing.TB, opts ...framecfg.ClientOption) (cl
 	if err != nil {
 		return nil, err
 	}
-	t.Cleanup(func() {
+	tb.Cleanup(func() {
 		client.Close()
 	})
 	return client, nil
@@ -1483,6 +1483,13 @@ func WithAuth(userName, password string) framecfg.ClientOption {
 	}
 }
 
+func WithAuthToken(token string) framecfg.ClientOption {
+	return func(c any) {
+		cfg := c.(*clientv3.Config)
+		cfg.Token = token
+	}
+}
+
 func WithEndpoints(endpoints []string) framecfg.ClientOption {
 	return func(c any) {
 		cfg := c.(*clientv3.Config)
diff --git a/tests/framework/integration/integration.go b/tests/framework/integration/integration.go
index 6e5de0cd5..6152ca705 100644
--- a/tests/framework/integration/integration.go
+++ b/tests/framework/integration/integration.go
@@ -41,11 +41,11 @@ func (e integrationRunner) TestMain(m *testing.M) {
 	testutil.MustTestMainWithLeakDetection(m)
 }
 
-func (e integrationRunner) BeforeTest(t testing.TB) {
-	BeforeTest(t)
+func (e integrationRunner) BeforeTest(tb testing.TB) {
+	BeforeTest(tb)
 }
 
-func (e integrationRunner) NewCluster(ctx context.Context, t testing.TB, opts ...config.ClusterOption) intf.Cluster {
+func (e integrationRunner) NewCluster(ctx context.Context, tb testing.TB, opts ...config.ClusterOption) intf.Cluster {
 	var err error
 	cfg := config.NewClusterConfig(opts...)
 	integrationCfg := ClusterConfig{
@@ -55,27 +55,27 @@ func (e integrationRunner) NewCluster(ctx context.Context, t testing.TB, opts ..
 		AuthToken:                  cfg.AuthToken,
 		SnapshotCount:              cfg.SnapshotCount,
 	}
-	integrationCfg.ClientTLS, err = tlsInfo(t, cfg.ClientTLS)
+	integrationCfg.ClientTLS, err = tlsInfo(tb, cfg.ClientTLS)
 	if err != nil {
-		t.Fatalf("ClientTLS: %s", err)
+		tb.Fatalf("ClientTLS: %s", err)
 	}
-	integrationCfg.PeerTLS, err = tlsInfo(t, cfg.PeerTLS)
+	integrationCfg.PeerTLS, err = tlsInfo(tb, cfg.PeerTLS)
 	if err != nil {
-		t.Fatalf("PeerTLS: %s", err)
+		tb.Fatalf("PeerTLS: %s", err)
 	}
 	return &integrationCluster{
-		Cluster: NewCluster(t, &integrationCfg),
-		t:       t,
+		Cluster: NewCluster(tb, &integrationCfg),
+		t:       tb,
 		ctx:     ctx,
 	}
 }
 
-func tlsInfo(t testing.TB, cfg config.TLSConfig) (*transport.TLSInfo, error) {
+func tlsInfo(tb testing.TB, cfg config.TLSConfig) (*transport.TLSInfo, error) {
 	switch cfg {
 	case config.NoTLS:
 		return nil, nil
 	case config.AutoTLS:
-		tls, err := transport.SelfCert(zap.NewNop(), t.TempDir(), []string{"localhost"}, 1)
+		tls, err := transport.SelfCert(zap.NewNop(), tb.TempDir(), []string{"localhost"}, 1)
 		if err != nil {
 			return nil, fmt.Errorf("failed to generate cert: %w", err)
 		}
diff --git a/tests/framework/integration/testing.go b/tests/framework/integration/testing.go
index a4d03c532..7e0dd38d4 100644
--- a/tests/framework/integration/testing.go
+++ b/tests/framework/integration/testing.go
@@ -138,18 +138,18 @@ func assertInTestContext(t testutil.TB) {
 	}
 }
 
-func NewEmbedConfig(t testing.TB, name string) *embed.Config {
+func NewEmbedConfig(tb testing.TB, name string) *embed.Config {
 	cfg := embed.NewConfig()
 	cfg.Name = name
-	lg := zaptest.NewLogger(t, zaptest.Level(zapcore.InfoLevel)).Named(cfg.Name)
+	lg := zaptest.NewLogger(tb, zaptest.Level(zapcore.InfoLevel)).Named(cfg.Name)
 	cfg.ZapLoggerBuilder = embed.NewZapLoggerBuilder(lg)
-	cfg.Dir = t.TempDir()
+	cfg.Dir = tb.TempDir()
 	return cfg
 }
 
-func NewClient(t testing.TB, cfg clientv3.Config) (*clientv3.Client, error) {
+func NewClient(tb testing.TB, cfg clientv3.Config) (*clientv3.Client, error) {
 	if cfg.Logger == nil {
-		cfg.Logger = zaptest.NewLogger(t).Named("client")
+		cfg.Logger = zaptest.NewLogger(tb).Named("client")
 	}
 	return clientv3.New(cfg)
 }
diff --git a/tests/framework/testutils/execute.go b/tests/framework/testutils/execute.go
index d9c3d3358..9ea7e1222 100644
--- a/tests/framework/testutils/execute.go
+++ b/tests/framework/testutils/execute.go
@@ -24,7 +24,7 @@ import (
 )
 
 func ExecuteWithTimeout(t *testing.T, timeout time.Duration, f func()) {
-	ctx, cancel := context.WithTimeout(context.Background(), timeout)
+	ctx, cancel := context.WithTimeout(t.Context(), timeout)
 	defer cancel()
 	ExecuteUntil(ctx, t, f)
 }
diff --git a/tests/framework/testutils/log_observer_test.go b/tests/framework/testutils/log_observer_test.go
index 695caf972..a11ee175c 100644
--- a/tests/framework/testutils/log_observer_test.go
+++ b/tests/framework/testutils/log_observer_test.go
@@ -32,7 +32,7 @@ func TestLogObserver_Timeout(t *testing.T) {
 	logger := zap.New(logCore)
 	logger.Info(t.Name())
 
-	ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
+	ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
 	_, err := logOb.Expect(ctx, "unknown", 1)
 	cancel()
 	require.ErrorIs(t, err, context.DeadlineExceeded)
@@ -45,7 +45,7 @@ func TestLogObserver_Expect(t *testing.T) {
 
 	logger := zap.New(logCore)
 
-	ctx, cancel := context.WithCancel(context.TODO())
+	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
 	resCh := make(chan []string, 1)
diff --git a/tests/framework/unit/unit.go b/tests/framework/unit/unit.go
index f822b7dd1..d09ef0bdd 100644
--- a/tests/framework/unit/unit.go
+++ b/tests/framework/unit/unit.go
@@ -42,10 +42,10 @@ func (e unitRunner) TestMain(m *testing.M) {
 	}
 }
 
-func (e unitRunner) BeforeTest(t testing.TB) {
+func (e unitRunner) BeforeTest(tb testing.TB) {
 }
 
-func (e unitRunner) NewCluster(ctx context.Context, t testing.TB, opts ...config.ClusterOption) intf.Cluster {
-	testutil.SkipTestIfShortMode(t, "Cannot create clusters in --short tests")
+func (e unitRunner) NewCluster(ctx context.Context, tb testing.TB, opts ...config.ClusterOption) intf.Cluster {
+	testutil.SkipTestIfShortMode(tb, "Cannot create clusters in --short tests")
 	return nil
 }
diff --git a/tests/go.mod b/tests/go.mod
index 88dabf481..2cd8d042c 100644
--- a/tests/go.mod
+++ b/tests/go.mod
@@ -2,7 +2,7 @@ module go.etcd.io/etcd/tests/v3
 
 go 1.24
 
-toolchain go1.24.1
+toolchain go1.24.3
 
 replace (
 	go.etcd.io/etcd/api/v3 => ../api
@@ -16,16 +16,18 @@ replace (
 )
 
 require (
-	github.com/anishathalye/porcupine v1.0.0
+	github.com/anishathalye/porcupine v1.0.2
+	github.com/antithesishq/antithesis-sdk-go v0.4.3
 	github.com/coreos/go-semver v0.3.1
+	github.com/golang-jwt/jwt/v5 v5.2.2
 	github.com/golang/protobuf v1.5.4
 	github.com/google/go-cmp v0.7.0
 	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
 	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
-	github.com/prometheus/client_golang v1.21.0
-	github.com/prometheus/client_model v0.6.1
-	github.com/prometheus/common v0.62.0
+	github.com/prometheus/client_golang v1.22.0
+	github.com/prometheus/client_model v0.6.2
+	github.com/prometheus/common v0.63.0
 	github.com/soheilhy/cmux v0.1.5
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/api/v3 v3.6.0-alpha.0
@@ -38,17 +40,17 @@ require (
 	go.etcd.io/etcd/server/v3 v3.6.0-alpha.0
 	go.etcd.io/gofail v0.2.0
 	go.etcd.io/raft/v3 v3.6.0
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0
-	go.opentelemetry.io/otel v1.34.0
-	go.opentelemetry.io/otel/sdk v1.34.0
-	go.opentelemetry.io/otel/trace v1.34.0
-	go.opentelemetry.io/proto/otlp v1.5.0
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
+	go.opentelemetry.io/otel v1.35.0
+	go.opentelemetry.io/otel/sdk v1.35.0
+	go.opentelemetry.io/otel/trace v1.35.0
+	go.opentelemetry.io/proto/otlp v1.6.0
 	go.uber.org/zap v1.27.0
-	golang.org/x/crypto v0.35.0
-	golang.org/x/sync v0.11.0
-	golang.org/x/time v0.10.0
-	google.golang.org/grpc v1.70.0
-	google.golang.org/protobuf v1.36.5
+	golang.org/x/crypto v0.37.0
+	golang.org/x/sync v0.13.0
+	golang.org/x/time v0.11.0
+	google.golang.org/grpc v1.72.0
+	google.golang.org/protobuf v1.36.6
 )
 
 require (
@@ -60,27 +62,25 @@ require (
 	github.com/cheggaaa/pb/v3 v3.1.7 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/creack/pty v1.1.18 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/fatih/color v1.18.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/google/btree v1.1.3 // indirect
 	github.com/google/uuid v1.6.0 // indirect
-	github.com/gorilla/websocket v1.4.2 // indirect
+	github.com/gorilla/websocket v1.5.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jonboulle/clockwork v0.5.0 // indirect
-	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
@@ -90,17 +90,17 @@ require (
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.etcd.io/bbolt v1.4.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
-	go.opentelemetry.io/otel/metric v1.34.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
-	golang.org/x/net v0.36.0 // indirect
-	golang.org/x/sys v0.30.0 // indirect
-	golang.org/x/text v0.22.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
+	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/tests/go.sum b/tests/go.sum
index 0bb1c1773..02eced100 100644
--- a/tests/go.sum
+++ b/tests/go.sum
@@ -2,8 +2,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
 github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
-github.com/anishathalye/porcupine v1.0.0 h1:93eF6d26IMDky+G4h8FcLuYp1oO+no8a//I7asq/oKI=
-github.com/anishathalye/porcupine v1.0.0/go.mod h1:WM0SsFjWNl2Y4BqHr/E/ll2yY1GY1jqn+W7Z/84Zoog=
+github.com/anishathalye/porcupine v1.0.2 h1:cXMWjnN95KYsbZVTi9VmXj0ePs1w3ZJ82zWoXDy6WPE=
+github.com/anishathalye/porcupine v1.0.2/go.mod h1:WM0SsFjWNl2Y4BqHr/E/ll2yY1GY1jqn+W7Z/84Zoog=
+github.com/antithesishq/antithesis-sdk-go v0.4.3 h1:a2hGdDogClzHzFu20r1z0tzD6zwSWUipiaerAjZVP90=
+github.com/antithesishq/antithesis-sdk-go v0.4.3/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
 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/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
@@ -27,8 +29,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
 github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -48,11 +51,11 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 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/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -67,8 +70,8 @@ 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
@@ -106,15 +109,16 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
-github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
+github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
-github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
-github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+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/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
+github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -153,24 +157,24 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
 go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
-go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
-go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
+go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -183,8 +187,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -200,16 +204,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
-golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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=
@@ -217,14 +221,14 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -242,19 +246,19 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
-google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
+google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -266,7 +270,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=
-sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
+sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/tests/integration/clientv3/concurrency/election_test.go b/tests/integration/clientv3/concurrency/election_test.go
index 951c6a91f..b597e9130 100644
--- a/tests/integration/clientv3/concurrency/election_test.go
+++ b/tests/integration/clientv3/concurrency/election_test.go
@@ -21,6 +21,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/client/v3/concurrency"
 	integration2 "go.etcd.io/etcd/tests/v3/framework/integration"
@@ -49,16 +51,12 @@ func TestResumeElection(t *testing.T) {
 	defer cancel()
 
 	// become leader
-	if err = e.Campaign(ctx, "candidate1"); err != nil {
-		t.Fatalf("Campaign() returned non nil err: %s", err)
-	}
+	require.NoErrorf(t, e.Campaign(ctx, "candidate1"), "Campaign() returned non nil err")
 
 	// get the leadership details of the current election
 	var leader *clientv3.GetResponse
 	leader, err = e.Leader(ctx)
-	if err != nil {
-		t.Fatalf("Leader() returned non nil err: %s", err)
-	}
+	require.NoErrorf(t, err, "Leader() returned non nil err")
 
 	// Recreate the election
 	e = concurrency.ResumeElection(s, prefix,
@@ -86,19 +84,13 @@ func TestResumeElection(t *testing.T) {
 	// put some random data to generate a change event, this put should be
 	// ignored by Observe() because it is not under the election prefix.
 	_, err = cli.Put(ctx, "foo", "bar")
-	if err != nil {
-		t.Fatalf("Put('foo') returned non nil err: %s", err)
-	}
+	require.NoErrorf(t, err, "Put('foo') returned non nil err")
 
 	// resign as leader
-	if err := e.Resign(ctx); err != nil {
-		t.Fatalf("Resign() returned non nil err: %s", err)
-	}
+	require.NoErrorf(t, e.Resign(ctx), "Resign() returned non nil err")
 
 	// elect a different candidate
-	if err := e.Campaign(ctx, "candidate2"); err != nil {
-		t.Fatalf("Campaign() returned non nil err: %s", err)
-	}
+	require.NoErrorf(t, e.Campaign(ctx, "candidate2"), "Campaign() returned non nil err")
 
 	// wait for observed leader change
 	resp := <-respChan
diff --git a/tests/integration/clientv3/connectivity/black_hole_test.go b/tests/integration/clientv3/connectivity/black_hole_test.go
index 00b7849ea..a48444714 100644
--- a/tests/integration/clientv3/connectivity/black_hole_test.go
+++ b/tests/integration/clientv3/connectivity/black_hole_test.go
@@ -67,9 +67,8 @@ func TestBalancerUnderBlackholeKeepAliveWatch(t *testing.T) {
 	defer cli.Close()
 
 	wch := cli.Watch(context.Background(), "foo", clientv3.WithCreatedNotify())
-	if _, ok := <-wch; !ok {
-		t.Fatalf("watch failed on creation")
-	}
+	_, ok := <-wch
+	require.Truef(t, ok, "watch failed on creation")
 
 	// endpoint can switch to eps[1] when it detects the failure of eps[0]
 	cli.SetEndpoints(eps...)
diff --git a/tests/integration/clientv3/connectivity/dial_test.go b/tests/integration/clientv3/connectivity/dial_test.go
index 54556d0f8..80a3cb90f 100644
--- a/tests/integration/clientv3/connectivity/dial_test.go
+++ b/tests/integration/clientv3/connectivity/dial_test.go
@@ -63,9 +63,7 @@ func TestDialTLSExpired(t *testing.T) {
 		DialOptions: []grpc.DialOption{grpc.WithBlock()},
 		TLS:         tls,
 	})
-	if !clientv3test.IsClientTimeout(err) {
-		t.Fatalf("expected dial timeout error, got %v", err)
-	}
+	require.Truef(t, clientv3test.IsClientTimeout(err), "expected dial timeout error")
 }
 
 // TestDialTLSNoConfig ensures the client fails to dial / times out
@@ -85,9 +83,7 @@ func TestDialTLSNoConfig(t *testing.T) {
 			c.Close()
 		}
 	}()
-	if !clientv3test.IsClientTimeout(err) {
-		t.Fatalf("expected dial timeout error, got %v", err)
-	}
+	require.Truef(t, clientv3test.IsClientTimeout(err), "expected dial timeout error")
 }
 
 // TestDialSetEndpointsBeforeFail ensures SetEndpoints can replace unavailable
diff --git a/tests/integration/clientv3/connectivity/network_partition_test.go b/tests/integration/clientv3/connectivity/network_partition_test.go
index 557cdb0b3..eb14534e9 100644
--- a/tests/integration/clientv3/connectivity/network_partition_test.go
+++ b/tests/integration/clientv3/connectivity/network_partition_test.go
@@ -260,9 +260,7 @@ func testBalancerUnderNetworkPartitionWatch(t *testing.T, isolateLeader bool) {
 		if len(ev.Events) != 0 {
 			t.Fatal("expected no event")
 		}
-		if err = ev.Err(); !errors.Is(err, rpctypes.ErrNoLeader) {
-			t.Fatalf("expected %v, got %v", rpctypes.ErrNoLeader, err)
-		}
+		require.ErrorIs(t, ev.Err(), rpctypes.ErrNoLeader)
 	case <-time.After(integration2.RequestWaitTimeout): // enough time to detect leader lost
 		t.Fatal("took too long to detect leader lost")
 	}
@@ -302,9 +300,7 @@ func TestDropReadUnderNetworkPartition(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
 	_, err = kvc.Get(ctx, "a")
 	cancel()
-	if !errors.Is(err, rpctypes.ErrLeaderChanged) {
-		t.Fatalf("expected %v, got %v", rpctypes.ErrLeaderChanged, err)
-	}
+	require.ErrorIsf(t, err, rpctypes.ErrLeaderChanged, "expected %v, got %v", rpctypes.ErrLeaderChanged, err)
 
 	for i := 0; i < 5; i++ {
 		ctx, cancel = context.WithTimeout(context.TODO(), 10*time.Second)
diff --git a/tests/integration/clientv3/experimental/recipes/v3_barrier_test.go b/tests/integration/clientv3/experimental/recipes/v3_barrier_test.go
index b6dfef385..1fefd906d 100644
--- a/tests/integration/clientv3/experimental/recipes/v3_barrier_test.go
+++ b/tests/integration/clientv3/experimental/recipes/v3_barrier_test.go
@@ -19,6 +19,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	clientv3 "go.etcd.io/etcd/client/v3"
 	recipe "go.etcd.io/etcd/client/v3/experimental/recipes"
 	integration2 "go.etcd.io/etcd/tests/v3/framework/integration"
@@ -40,12 +42,8 @@ func TestBarrierMultiNode(t *testing.T) {
 
 func testBarrier(t *testing.T, waiters int, chooseClient func() *clientv3.Client) {
 	b := recipe.NewBarrier(chooseClient(), "test-barrier")
-	if err := b.Hold(); err != nil {
-		t.Fatalf("could not hold barrier (%v)", err)
-	}
-	if err := b.Hold(); err == nil {
-		t.Fatalf("able to double-hold barrier")
-	}
+	require.NoErrorf(t, b.Hold(), "could not hold barrier")
+	require.Errorf(t, b.Hold(), "able to double-hold barrier")
 
 	// put a random key to move the revision forward
 	if _, err := chooseClient().Put(context.Background(), "x", ""); err != nil {
@@ -75,9 +73,7 @@ func testBarrier(t *testing.T, waiters int, chooseClient func() *clientv3.Client
 	default:
 	}
 
-	if err := b.Release(); err != nil {
-		t.Fatalf("could not release barrier (%v)", err)
-	}
+	require.NoErrorf(t, b.Release(), "could not release barrier")
 
 	timerC := time.After(time.Duration(waiters*100) * time.Millisecond)
 	for i := 0; i < waiters; i++ {
diff --git a/tests/integration/clientv3/experimental/recipes/v3_double_barrier_test.go b/tests/integration/clientv3/experimental/recipes/v3_double_barrier_test.go
index d8f610e66..9dcc5530c 100644
--- a/tests/integration/clientv3/experimental/recipes/v3_double_barrier_test.go
+++ b/tests/integration/clientv3/experimental/recipes/v3_double_barrier_test.go
@@ -73,9 +73,7 @@ func TestDoubleBarrier(t *testing.T) {
 	default:
 	}
 
-	if err := b.Enter(); err != nil {
-		t.Fatalf("could not enter last barrier (%v)", err)
-	}
+	require.NoErrorf(t, b.Enter(), "could not enter last barrier")
 
 	timerC := time.After(time.Duration(waiters*100) * time.Millisecond)
 	for i := 0; i < waiters-1; i++ {
diff --git a/tests/integration/clientv3/experimental/recipes/v3_lock_test.go b/tests/integration/clientv3/experimental/recipes/v3_lock_test.go
index 4a802f72c..50b571b15 100644
--- a/tests/integration/clientv3/experimental/recipes/v3_lock_test.go
+++ b/tests/integration/clientv3/experimental/recipes/v3_lock_test.go
@@ -93,9 +93,7 @@ func testMutexLock(t *testing.T, waiters int, chooseClient func() *clientv3.Clie
 				t.Fatalf("lock %d followers did not wait", i)
 			default:
 			}
-			if err := m.Unlock(context.TODO()); err != nil {
-				t.Fatalf("could not release lock (%v)", err)
-			}
+			require.NoErrorf(t, m.Unlock(context.TODO()), "could not release lock")
 		}
 	}
 	wg.Wait()
@@ -233,9 +231,7 @@ func TestMutexWaitsOnCurrentHolder(t *testing.T) {
 			t.Fatal("failed to receive watch response")
 		}
 	}
-	if putCounts != 2 {
-		t.Fatalf("expect 2 put events, but got %v", putCounts)
-	}
+	require.Equalf(t, 2, putCounts, "expect 2 put events, but got %v", putCounts)
 
 	newOwnerSession, err := concurrency.NewSession(cli)
 	if err != nil {
@@ -250,12 +246,9 @@ func TestMutexWaitsOnCurrentHolder(t *testing.T) {
 
 	select {
 	case wrp := <-wch:
-		if len(wrp.Events) != 1 {
-			t.Fatalf("expect a event, but got %v events", len(wrp.Events))
-		}
-		if e := wrp.Events[0]; e.Type != mvccpb.PUT {
-			t.Fatalf("expect a put event on prefix test-mutex, but got event type %v", e.Type)
-		}
+		require.Lenf(t, wrp.Events, 1, "expect a event, but got %v events", len(wrp.Events))
+		e := wrp.Events[0]
+		require.Equalf(t, mvccpb.PUT, e.Type, "expect a put event on prefix test-mutex, but got event type %v", e.Type)
 	case <-time.After(time.Second):
 		t.Fatalf("failed to receive a watch response")
 	}
@@ -266,12 +259,9 @@ func TestMutexWaitsOnCurrentHolder(t *testing.T) {
 	// ensures the deletion of victim waiter from server side.
 	select {
 	case wrp := <-wch:
-		if len(wrp.Events) != 1 {
-			t.Fatalf("expect a event, but got %v events", len(wrp.Events))
-		}
-		if e := wrp.Events[0]; e.Type != mvccpb.DELETE {
-			t.Fatalf("expect a delete event on prefix test-mutex, but got event type %v", e.Type)
-		}
+		require.Lenf(t, wrp.Events, 1, "expect a event, but got %v events", len(wrp.Events))
+		e := wrp.Events[0]
+		require.Equalf(t, mvccpb.DELETE, e.Type, "expect a delete event on prefix test-mutex, but got event type %v", e.Type)
 	case <-time.After(time.Second):
 		t.Fatal("failed to receive a watch response")
 	}
@@ -357,18 +347,14 @@ func testRWMutex(t *testing.T, waiters int, chooseClient func() *clientv3.Client
 				t.Fatalf("rlock %d readers did not wait", i)
 			default:
 			}
-			if err := wl.Unlock(); err != nil {
-				t.Fatalf("could not release lock (%v)", err)
-			}
+			require.NoErrorf(t, wl.Unlock(), "could not release lock")
 		case rl := <-rlockedC:
 			select {
 			case <-wlockedC:
 				t.Fatalf("rlock %d writers did not wait", i)
 			default:
 			}
-			if err := rl.RUnlock(); err != nil {
-				t.Fatalf("could not release rlock (%v)", err)
-			}
+			require.NoErrorf(t, rl.RUnlock(), "could not release rlock")
 		}
 	}
 }
diff --git a/tests/integration/clientv3/experimental/recipes/v3_queue_test.go b/tests/integration/clientv3/experimental/recipes/v3_queue_test.go
index 73ed5552f..89172dfec 100644
--- a/tests/integration/clientv3/experimental/recipes/v3_queue_test.go
+++ b/tests/integration/clientv3/experimental/recipes/v3_queue_test.go
@@ -20,6 +20,8 @@ import (
 	"sync/atomic"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	recipe "go.etcd.io/etcd/client/v3/experimental/recipes"
 	integration2 "go.etcd.io/etcd/tests/v3/framework/integration"
 )
@@ -57,12 +59,8 @@ func TestQueueOneReaderOneWriter(t *testing.T) {
 	q := recipe.NewQueue(etcdc, "testq")
 	for i := 0; i < 5; i++ {
 		s, err := q.Dequeue()
-		if err != nil {
-			t.Fatalf("error dequeueing (%v)", err)
-		}
-		if s != fmt.Sprintf("%d", i) {
-			t.Fatalf("expected dequeue value %v, got %v", s, i)
-		}
+		require.NoErrorf(t, err, "error dequeueing (%v)", err)
+		require.Equalf(t, s, fmt.Sprintf("%d", i), "expected dequeue value %v, got %v", s, i)
 	}
 }
 
@@ -103,25 +101,18 @@ func TestPrQueueOneReaderOneWriter(t *testing.T) {
 	for i := 0; i < 5; i++ {
 		// [0, 2] priority for priority collision to test seq keys
 		pr := uint16(rand.Intn(3))
-		if err := q.Enqueue(fmt.Sprintf("%d", pr), pr); err != nil {
-			t.Fatalf("error enqueuing (%v)", err)
-		}
+		require.NoErrorf(t, q.Enqueue(fmt.Sprintf("%d", pr), pr), "error enqueuing")
 	}
 
 	// read back items; confirm priority order is respected
 	lastPr := -1
 	for i := 0; i < 5; i++ {
 		s, err := q.Dequeue()
-		if err != nil {
-			t.Fatalf("error dequeueing (%v)", err)
-		}
+		require.NoErrorf(t, err, "error dequeueing (%v)", err)
 		curPr := 0
-		if _, err := fmt.Sscanf(s, "%d", &curPr); err != nil {
-			t.Fatalf(`error parsing item "%s" (%v)`, s, err)
-		}
-		if lastPr > curPr {
-			t.Fatalf("expected priority %v > %v", curPr, lastPr)
-		}
+		_, err = fmt.Sscanf(s, "%d", &curPr)
+		require.NoErrorf(t, err, `error parsing item "%s" (%v)`, s, err)
+		require.LessOrEqualf(t, lastPr, curPr, "expected priority %v > %v", curPr, lastPr)
 	}
 }
 
diff --git a/tests/integration/clientv3/lease/lease_test.go b/tests/integration/clientv3/lease/lease_test.go
index eaf4a75ff..083d45a3c 100644
--- a/tests/integration/clientv3/lease/lease_test.go
+++ b/tests/integration/clientv3/lease/lease_test.go
@@ -41,9 +41,7 @@ func TestLeaseNotFoundError(t *testing.T) {
 	kv := clus.RandClient()
 
 	_, err := kv.Put(context.TODO(), "foo", "bar", clientv3.WithLease(clientv3.LeaseID(500)))
-	if !errors.Is(err, rpctypes.ErrLeaseNotFound) {
-		t.Fatalf("expected %v, got %v", rpctypes.ErrLeaseNotFound, err)
-	}
+	require.ErrorIsf(t, err, rpctypes.ErrLeaseNotFound, "expected %v, got %v", rpctypes.ErrLeaseNotFound, err)
 }
 
 func TestLeaseGrant(t *testing.T) {
@@ -57,9 +55,7 @@ func TestLeaseGrant(t *testing.T) {
 	kv := clus.RandClient()
 
 	_, merr := lapi.Grant(context.Background(), clientv3.MaxLeaseTTL+1)
-	if !errors.Is(merr, rpctypes.ErrLeaseTTLTooLarge) {
-		t.Fatalf("err = %v, want %v", merr, rpctypes.ErrLeaseTTLTooLarge)
-	}
+	require.ErrorIsf(t, merr, rpctypes.ErrLeaseTTLTooLarge, "err = %v, want %v", merr, rpctypes.ErrLeaseTTLTooLarge)
 
 	resp, err := lapi.Grant(context.Background(), 10)
 	if err != nil {
@@ -67,9 +63,7 @@ func TestLeaseGrant(t *testing.T) {
 	}
 
 	_, err = kv.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
-	if err != nil {
-		t.Fatalf("failed to create key with lease %v", err)
-	}
+	require.NoErrorf(t, err, "failed to create key with lease %v", err)
 }
 
 func TestLeaseRevoke(t *testing.T) {
@@ -93,9 +87,7 @@ func TestLeaseRevoke(t *testing.T) {
 	}
 
 	_, err = kv.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
-	if !errors.Is(err, rpctypes.ErrLeaseNotFound) {
-		t.Fatalf("err = %v, want %v", err, rpctypes.ErrLeaseNotFound)
-	}
+	require.ErrorIsf(t, err, rpctypes.ErrLeaseNotFound, "err = %v, want %v", err, rpctypes.ErrLeaseNotFound)
 }
 
 func TestLeaseKeepAliveOnce(t *testing.T) {
@@ -153,9 +145,7 @@ func TestLeaseKeepAlive(t *testing.T) {
 		t.Errorf("chan is closed, want not closed")
 	}
 
-	if kresp == nil {
-		t.Fatalf("unexpected null response")
-	}
+	require.NotNilf(t, kresp, "unexpected null response")
 
 	if kresp.ID != resp.ID {
 		t.Errorf("ID = %x, want %x", kresp.ID, resp.ID)
@@ -341,9 +331,7 @@ func TestLeaseKeepAliveFullResponseQueue(t *testing.T) {
 
 	// expect lease keepalive every 10-second
 	lresp, err := lapi.Grant(context.Background(), 30)
-	if err != nil {
-		t.Fatalf("failed to create lease %v", err)
-	}
+	require.NoErrorf(t, err, "failed to create lease %v", err)
 	id := lresp.ID
 
 	old := clientv3.LeaseResponseChSize
@@ -354,18 +342,14 @@ func TestLeaseKeepAliveFullResponseQueue(t *testing.T) {
 
 	// never fetch from response queue, and let it become full
 	_, err = lapi.KeepAlive(context.Background(), id)
-	if err != nil {
-		t.Fatalf("failed to keepalive lease %v", err)
-	}
+	require.NoErrorf(t, err, "failed to keepalive lease %v", err)
 
 	// TTL should not be refreshed after 3 seconds
 	// expect keepalive to be triggered after TTL/3
 	time.Sleep(3 * time.Second)
 
 	tr, terr := lapi.TimeToLive(context.Background(), id)
-	if terr != nil {
-		t.Fatalf("failed to get lease information %v", terr)
-	}
+	require.NoErrorf(t, terr, "failed to get lease information %v", terr)
 	if tr.TTL >= 29 {
 		t.Errorf("unexpected kept-alive lease TTL %d", tr.TTL)
 	}
@@ -423,9 +407,7 @@ func TestLeaseRevokeNewAfterClose(t *testing.T) {
 	case <-time.After(integration2.RequestWaitTimeout):
 		t.Fatal("le.Revoke took too long")
 	case errMsg := <-errMsgCh:
-		if errMsg != "" {
-			t.Fatalf("%v", errMsg)
-		}
+		require.Empty(t, errMsg)
 	}
 }
 
@@ -445,9 +427,7 @@ func TestLeaseKeepAliveCloseAfterDisconnectRevoke(t *testing.T) {
 	rc, kerr := cli.KeepAlive(context.Background(), resp.ID)
 	require.NoError(t, kerr)
 	kresp := <-rc
-	if kresp.ID != resp.ID {
-		t.Fatalf("ID = %x, want %x", kresp.ID, resp.ID)
-	}
+	require.Equalf(t, kresp.ID, resp.ID, "ID = %x, want %x", kresp.ID, resp.ID)
 
 	// keep client disconnected
 	clus.Members[0].Stop(t)
@@ -489,9 +469,7 @@ func TestLeaseKeepAliveInitTimeout(t *testing.T) {
 	require.NoError(t, kerr)
 	select {
 	case ka, ok := <-rc:
-		if ok {
-			t.Fatalf("unexpected keepalive %v, expected closed channel", ka)
-		}
+		require.Falsef(t, ok, "unexpected keepalive %v, expected closed channel", ka)
 	case <-time.After(10 * time.Second):
 		t.Fatalf("keepalive channel did not close")
 	}
@@ -514,17 +492,14 @@ func TestLeaseKeepAliveTTLTimeout(t *testing.T) {
 	require.NoError(t, err)
 	rc, kerr := cli.KeepAlive(context.Background(), resp.ID)
 	require.NoError(t, kerr)
-	if kresp := <-rc; kresp.ID != resp.ID {
-		t.Fatalf("ID = %x, want %x", kresp.ID, resp.ID)
-	}
+	kresp := <-rc
+	require.Equalf(t, kresp.ID, resp.ID, "ID = %x, want %x", kresp.ID, resp.ID)
 
 	// keep client disconnected
 	clus.Members[0].Stop(t)
 	select {
 	case ka, ok := <-rc:
-		if ok {
-			t.Fatalf("unexpected keepalive %v, expected closed channel", ka)
-		}
+		require.Falsef(t, ok, "unexpected keepalive %v, expected closed channel", ka)
 	case <-time.After(10 * time.Second):
 		t.Fatalf("keepalive channel did not close")
 	}
@@ -559,12 +534,8 @@ func TestLeaseTimeToLive(t *testing.T) {
 
 	lresp, lerr := lapi.TimeToLive(context.Background(), resp.ID, clientv3.WithAttachedKeys())
 	require.NoError(t, lerr)
-	if lresp.ID != resp.ID {
-		t.Fatalf("leaseID expected %d, got %d", resp.ID, lresp.ID)
-	}
-	if lresp.GrantedTTL != int64(10) {
-		t.Fatalf("GrantedTTL expected %d, got %d", 10, lresp.GrantedTTL)
-	}
+	require.Equalf(t, lresp.ID, resp.ID, "leaseID expected %d, got %d", resp.ID, lresp.ID)
+	require.Equalf(t, int64(10), lresp.GrantedTTL, "GrantedTTL expected %d, got %d", 10, lresp.GrantedTTL)
 	if lresp.TTL == 0 || lresp.TTL > lresp.GrantedTTL {
 		t.Fatalf("unexpected TTL %d (granted %d)", lresp.TTL, lresp.GrantedTTL)
 	}
@@ -573,15 +544,11 @@ func TestLeaseTimeToLive(t *testing.T) {
 		ks[i] = string(lresp.Keys[i])
 	}
 	sort.Strings(ks)
-	if !reflect.DeepEqual(ks, keys) {
-		t.Fatalf("keys expected %v, got %v", keys, ks)
-	}
+	require.Truef(t, reflect.DeepEqual(ks, keys), "keys expected %v, got %v", keys, ks)
 
 	lresp, lerr = lapi.TimeToLive(context.Background(), resp.ID)
 	require.NoError(t, lerr)
-	if len(lresp.Keys) != 0 {
-		t.Fatalf("unexpected keys %+v", lresp.Keys)
-	}
+	require.Emptyf(t, lresp.Keys, "unexpected keys %+v", lresp.Keys)
 }
 
 func TestLeaseTimeToLiveLeaseNotFound(t *testing.T) {
@@ -602,21 +569,11 @@ func TestLeaseTimeToLiveLeaseNotFound(t *testing.T) {
 
 	lresp, err := cli.TimeToLive(context.Background(), resp.ID)
 	// TimeToLive() should return a response with TTL=-1.
-	if err != nil {
-		t.Fatalf("expected err to be nil")
-	}
-	if lresp == nil {
-		t.Fatalf("expected lresp not to be nil")
-	}
-	if lresp.ResponseHeader == nil {
-		t.Fatalf("expected ResponseHeader not to be nil")
-	}
-	if lresp.ID != resp.ID {
-		t.Fatalf("expected Lease ID %v, but got %v", resp.ID, lresp.ID)
-	}
-	if lresp.TTL != -1 {
-		t.Fatalf("expected TTL %v, but got %v", lresp.TTL, lresp.TTL)
-	}
+	require.NoErrorf(t, err, "expected err to be nil")
+	require.NotNilf(t, lresp, "expected lresp not to be nil")
+	require.NotNilf(t, lresp.ResponseHeader, "expected ResponseHeader not to be nil")
+	require.Equalf(t, lresp.ID, resp.ID, "expected Lease ID %v, but got %v", resp.ID, lresp.ID)
+	require.Equalf(t, lresp.TTL, int64(-1), "expected TTL %v, but got %v", lresp.TTL, lresp.TTL)
 }
 
 func TestLeaseLeases(t *testing.T) {
@@ -638,13 +595,9 @@ func TestLeaseLeases(t *testing.T) {
 
 	resp, err := cli.Leases(context.Background())
 	require.NoError(t, err)
-	if len(resp.Leases) != 5 {
-		t.Fatalf("len(resp.Leases) expected 5, got %d", len(resp.Leases))
-	}
+	require.Lenf(t, resp.Leases, 5, "len(resp.Leases) expected 5, got %d", len(resp.Leases))
 	for i := range resp.Leases {
-		if ids[i] != resp.Leases[i].ID {
-			t.Fatalf("#%d: lease ID expected %d, got %d", i, ids[i], resp.Leases[i].ID)
-		}
+		require.Equalf(t, ids[i], resp.Leases[i].ID, "#%d: lease ID expected %d, got %d", i, ids[i], resp.Leases[i].ID)
 	}
 }
 
@@ -686,9 +639,7 @@ func TestLeaseRenewLostQuorum(t *testing.T) {
 
 	select {
 	case _, ok := <-ka:
-		if !ok {
-			t.Fatalf("keepalive closed")
-		}
+		require.Truef(t, ok, "keepalive closed")
 	case <-time.After(time.Duration(r.TTL) * time.Second):
 		t.Fatalf("timed out waiting for keepalive")
 	}
@@ -710,9 +661,7 @@ func TestLeaseKeepAliveLoopExit(t *testing.T) {
 
 	_, err = cli.KeepAlive(ctx, resp.ID)
 	var keepAliveHaltedErr clientv3.ErrKeepAliveHalted
-	if !errors.As(err, &keepAliveHaltedErr) {
-		t.Fatalf("expected %T, got %v(%T)", clientv3.ErrKeepAliveHalted{}, err, err)
-	}
+	require.ErrorAsf(t, err, &keepAliveHaltedErr, "expected %T, got %v(%T)", clientv3.ErrKeepAliveHalted{}, err, err)
 }
 
 // TestV3LeaseFailureOverlap issues Grant and KeepAlive requests to a cluster
@@ -813,17 +762,13 @@ func TestLeaseWithRequireLeader(t *testing.T) {
 
 	select {
 	case resp, ok := <-kaReqLeader:
-		if ok {
-			t.Fatalf("expected closed require leader, got response %+v", resp)
-		}
+		require.Falsef(t, ok, "expected closed require leader, got response %+v", resp)
 	case <-time.After(5 * time.Second):
 		t.Fatal("keepalive with require leader took too long to close")
 	}
 	select {
 	case _, ok := <-kaWait:
-		if !ok {
-			t.Fatalf("got closed channel with no require leader, expected non-closed")
-		}
+		require.Truef(t, ok, "got closed channel with no require leader, expected non-closed")
 	case <-time.After(10 * time.Millisecond):
 		// wait some to detect any closes happening soon after kaReqLeader closing
 	}
diff --git a/tests/integration/clientv3/lease/leasing_test.go b/tests/integration/clientv3/lease/leasing_test.go
index 9d6d77b35..1751fdeb7 100644
--- a/tests/integration/clientv3/lease/leasing_test.go
+++ b/tests/integration/clientv3/lease/leasing_test.go
@@ -102,9 +102,7 @@ func TestLeasingInterval(t *testing.T) {
 
 	resp, err := lkv.Get(context.TODO(), "abc/", clientv3.WithPrefix())
 	require.NoError(t, err)
-	if len(resp.Kvs) != 3 {
-		t.Fatalf("expected keys %+v, got response keys %+v", keys, resp.Kvs)
-	}
+	require.Lenf(t, resp.Kvs, 3, "expected keys %+v, got response keys %+v", keys, resp.Kvs)
 
 	// load into cache
 	_, err = lkv.Get(context.TODO(), "abc/a")
@@ -113,9 +111,7 @@ func TestLeasingInterval(t *testing.T) {
 	// get when prefix is also a cached key
 	resp, err = lkv.Get(context.TODO(), "abc/a", clientv3.WithPrefix())
 	require.NoError(t, err)
-	if len(resp.Kvs) != 2 {
-		t.Fatalf("expected keys %+v, got response keys %+v", keys, resp.Kvs)
-	}
+	require.Lenf(t, resp.Kvs, 2, "expected keys %+v, got response keys %+v", keys, resp.Kvs)
 }
 
 // TestLeasingPutInvalidateNew checks the leasing KV updates its cache on a Put to a new key.
@@ -137,9 +133,7 @@ func TestLeasingPutInvalidateNew(t *testing.T) {
 	require.NoError(t, err)
 	cResp, cerr := clus.Client(0).Get(context.TODO(), "k")
 	require.NoError(t, cerr)
-	if !reflect.DeepEqual(lkvResp, cResp) {
-		t.Fatalf(`expected %+v, got response %+v`, cResp, lkvResp)
-	}
+	require.Truef(t, reflect.DeepEqual(lkvResp, cResp), `expected %+v, got response %+v`, cResp, lkvResp)
 }
 
 // TestLeasingPutInvalidateExisting checks the leasing KV updates its cache on a Put to an existing key.
@@ -164,9 +158,7 @@ func TestLeasingPutInvalidateExisting(t *testing.T) {
 	require.NoError(t, err)
 	cResp, cerr := clus.Client(0).Get(context.TODO(), "k")
 	require.NoError(t, cerr)
-	if !reflect.DeepEqual(lkvResp, cResp) {
-		t.Fatalf(`expected %+v, got response %+v`, cResp, lkvResp)
-	}
+	require.Truef(t, reflect.DeepEqual(lkvResp, cResp), `expected %+v, got response %+v`, cResp, lkvResp)
 }
 
 // TestLeasingGetNoLeaseTTL checks a key with a TTL is not leased.
@@ -218,9 +210,7 @@ func TestLeasingGetSerializable(t *testing.T) {
 	// don't necessarily try to acquire leasing key ownership for new key
 	resp, err := lkv.Get(context.TODO(), "uncached", clientv3.WithSerializable())
 	require.NoError(t, err)
-	if len(resp.Kvs) != 0 {
-		t.Fatalf(`expected no keys, got response %+v`, resp)
-	}
+	require.Emptyf(t, resp.Kvs, `expected no keys, got response %+v`, resp)
 
 	clus.Members[0].Stop(t)
 
@@ -414,9 +404,7 @@ func TestLeasingDeleteOwner(t *testing.T) {
 	resp, err := lkv.Get(context.TODO(), "k")
 	require.NoError(t, err)
 
-	if len(resp.Kvs) != 0 {
-		t.Fatalf(`expected "k" to be deleted, got response %+v`, resp)
-	}
+	require.Emptyf(t, resp.Kvs, `expected "k" to be deleted, got response %+v`, resp)
 	// try to double delete
 	_, err = lkv.Delete(context.TODO(), "k")
 	require.NoError(t, err)
@@ -447,9 +435,7 @@ func TestLeasingDeleteNonOwner(t *testing.T) {
 	// key should be removed from lkv1
 	resp, err := lkv1.Get(context.TODO(), "k")
 	require.NoError(t, err)
-	if len(resp.Kvs) != 0 {
-		t.Fatalf(`expected "k" to be deleted, got response %+v`, resp)
-	}
+	require.Emptyf(t, resp.Kvs, `expected "k" to be deleted, got response %+v`, resp)
 }
 
 func TestLeasingOverwriteResponse(t *testing.T) {
@@ -532,9 +518,8 @@ func TestLeasingTxnOwnerGetRange(t *testing.T) {
 
 	tresp, terr := lkv.Txn(context.TODO()).Then(clientv3.OpGet("k-", clientv3.WithPrefix())).Commit()
 	require.NoError(t, terr)
-	if resp := tresp.Responses[0].GetResponseRange(); len(resp.Kvs) != keyCount {
-		t.Fatalf("expected %d keys, got response %+v", keyCount, resp.Kvs)
-	}
+	resp := tresp.Responses[0].GetResponseRange()
+	require.Equalf(t, len(resp.Kvs), keyCount, "expected %d keys, got response %+v", keyCount, resp.Kvs)
 }
 
 func TestLeasingTxnOwnerGet(t *testing.T) {
@@ -596,22 +581,14 @@ func TestLeasingTxnOwnerGet(t *testing.T) {
 		Else(elseOps...).Commit()
 
 	require.NoError(t, terr)
-	if tresp.Succeeded != useThen {
-		t.Fatalf("expected succeeded=%v, got tresp=%+v", useThen, tresp)
-	}
-	if len(tresp.Responses) != len(ops) {
-		t.Fatalf("expected %d responses, got %d", len(ops), len(tresp.Responses))
-	}
+	require.Equalf(t, tresp.Succeeded, useThen, "expected succeeded=%v, got tresp=%+v", useThen, tresp)
+	require.Lenf(t, ops, len(tresp.Responses), "expected %d responses, got %d", len(ops), len(tresp.Responses))
 	wrev := presps[len(presps)-1].Header.Revision
-	if tresp.Header.Revision < wrev {
-		t.Fatalf("expected header revision >= %d, got %d", wrev, tresp.Header.Revision)
-	}
+	require.GreaterOrEqualf(t, tresp.Header.Revision, wrev, "expected header revision >= %d, got %d", wrev, tresp.Header.Revision)
 	for i := range ops {
 		k := fmt.Sprintf("k-%d", i)
 		rr := tresp.Responses[i].GetResponseRange()
-		if rr == nil {
-			t.Fatalf("expected get response, got %+v", tresp.Responses[i])
-		}
+		require.NotNilf(t, rr, "expected get response, got %+v", tresp.Responses[i])
 		if string(rr.Kvs[0].Key) != k || string(rr.Kvs[0].Value) != k+k {
 			t.Errorf(`expected key for %q, got %+v`, k, rr.Kvs)
 		}
@@ -637,18 +614,14 @@ func TestLeasingTxnOwnerDeleteRange(t *testing.T) {
 	// cache in lkv
 	resp, err := lkv.Get(context.TODO(), "k-", clientv3.WithPrefix())
 	require.NoError(t, err)
-	if len(resp.Kvs) != keyCount {
-		t.Fatalf("expected %d keys, got %d", keyCount, len(resp.Kvs))
-	}
+	require.Equalf(t, len(resp.Kvs), keyCount, "expected %d keys, got %d", keyCount, len(resp.Kvs))
 
 	_, terr := lkv.Txn(context.TODO()).Then(clientv3.OpDelete("k-", clientv3.WithPrefix())).Commit()
 	require.NoError(t, terr)
 
 	resp, err = lkv.Get(context.TODO(), "k-", clientv3.WithPrefix())
 	require.NoError(t, err)
-	if len(resp.Kvs) != 0 {
-		t.Fatalf("expected no keys, got %d", len(resp.Kvs))
-	}
+	require.Emptyf(t, resp.Kvs, "expected no keys, got %d", len(resp.Kvs))
 }
 
 func TestLeasingTxnOwnerDelete(t *testing.T) {
@@ -672,9 +645,7 @@ func TestLeasingTxnOwnerDelete(t *testing.T) {
 
 	resp, err := lkv.Get(context.TODO(), "k")
 	require.NoError(t, err)
-	if len(resp.Kvs) != 0 {
-		t.Fatalf("expected no keys, got %d", len(resp.Kvs))
-	}
+	require.Emptyf(t, resp.Kvs, "expected no keys, got %d", len(resp.Kvs))
 }
 
 func TestLeasingTxnOwnerIf(t *testing.T) {
@@ -794,9 +765,8 @@ func TestLeasingTxnCancel(t *testing.T) {
 		time.Sleep(100 * time.Millisecond)
 		cancel()
 	}()
-	if _, err := lkv2.Txn(ctx).Then(clientv3.OpPut("k", "v")).Commit(); !errors.Is(err, context.Canceled) {
-		t.Fatalf("expected %v, got %v", context.Canceled, err)
-	}
+	_, err = lkv2.Txn(ctx).Then(clientv3.OpPut("k", "v")).Commit()
+	require.ErrorIsf(t, err, context.Canceled, "expected %v, got %v", context.Canceled, err)
 }
 
 func TestLeasingTxnNonOwnerPut(t *testing.T) {
@@ -860,9 +830,7 @@ func TestLeasingTxnNonOwnerPut(t *testing.T) {
 			c++
 		}
 	}
-	if c != 3 {
-		t.Fatalf("expected 3 put events, got %+v", evs)
-	}
+	require.Equalf(t, 3, c, "expected 3 put events, got %+v", evs)
 }
 
 // TestLeasingTxnRandIfThenOrElse randomly leases keys two separate clients, then
@@ -946,9 +914,7 @@ func TestLeasingTxnRandIfThenOrElse(t *testing.T) {
 	tresp, terr := lkv1.Txn(context.TODO()).If(cmps...).Then(thenOps...).Else(elseOps...).Commit()
 	require.NoError(t, terr)
 	// cmps always succeed
-	if tresp.Succeeded != useThen {
-		t.Fatalf("expected succeeded=%v, got tresp=%+v", useThen, tresp)
-	}
+	require.Equalf(t, tresp.Succeeded, useThen, "expected succeeded=%v, got tresp=%+v", useThen, tresp)
 	// get should match what was put
 	checkPuts := func(s string, kv clientv3.KV) {
 		for _, op := range ops {
@@ -982,9 +948,8 @@ func TestLeasingOwnerPutError(t *testing.T) {
 	clus.Members[0].Stop(t)
 	ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
 	defer cancel()
-	if resp, err := lkv.Put(ctx, "k", "v"); err == nil {
-		t.Fatalf("expected error, got response %+v", resp)
-	}
+	resp, err := lkv.Put(ctx, "k", "v")
+	require.Errorf(t, err, "expected error, got response %+v", resp)
 }
 
 func TestLeasingOwnerDeleteError(t *testing.T) {
@@ -1002,9 +967,8 @@ func TestLeasingOwnerDeleteError(t *testing.T) {
 	clus.Members[0].Stop(t)
 	ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
 	defer cancel()
-	if resp, err := lkv.Delete(ctx, "k"); err == nil {
-		t.Fatalf("expected error, got response %+v", resp)
-	}
+	resp, err := lkv.Delete(ctx, "k")
+	require.Errorf(t, err, "expected error, got response %+v", resp)
 }
 
 func TestLeasingNonOwnerPutError(t *testing.T) {
@@ -1019,9 +983,8 @@ func TestLeasingNonOwnerPutError(t *testing.T) {
 	clus.Members[0].Stop(t)
 	ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond)
 	defer cancel()
-	if resp, err := lkv.Put(ctx, "k", "v"); err == nil {
-		t.Fatalf("expected error, got response %+v", resp)
-	}
+	resp, err := lkv.Put(ctx, "k", "v")
+	require.Errorf(t, err, "expected error, got response %+v", resp)
 }
 
 func TestLeasingOwnerDeletePrefix(t *testing.T) {
@@ -1057,9 +1020,7 @@ func testLeasingOwnerDelete(t *testing.T, del clientv3.Op) {
 	for i := 0; i < 8; i++ {
 		resp, err := lkv.Get(context.TODO(), fmt.Sprintf("key/%d", i))
 		require.NoError(t, err)
-		if len(resp.Kvs) != 0 {
-			t.Fatalf("expected no keys on key/%d, got %+v", i, resp)
-		}
+		require.Emptyf(t, resp.Kvs, "expected no keys on key/%d, got %+v", i, resp)
 	}
 
 	// confirm keys were deleted atomically
@@ -1070,9 +1031,8 @@ func testLeasingOwnerDelete(t *testing.T, del clientv3.Op) {
 		clientv3.WithRev(delResp.Header.Revision),
 		clientv3.WithPrefix())
 
-	if wresp := <-w; len(wresp.Events) != 8 {
-		t.Fatalf("expected %d delete events,got %d", 8, len(wresp.Events))
-	}
+	wresp := <-w
+	require.Lenf(t, wresp.Events, 8, "expected %d delete events,got %d", 8, len(wresp.Events))
 }
 
 func TestLeasingDeleteRangeBounds(t *testing.T) {
@@ -1102,9 +1062,7 @@ func TestLeasingDeleteRangeBounds(t *testing.T) {
 	for _, k := range []string{"j", "m"} {
 		resp, geterr := clus.Client(0).Get(context.TODO(), "0/"+k, clientv3.WithPrefix())
 		require.NoError(t, geterr)
-		if len(resp.Kvs) != 1 {
-			t.Fatalf("expected leasing key, got %+v", resp)
-		}
+		require.Lenf(t, resp.Kvs, 1, "expected leasing key, got %+v", resp)
 	}
 
 	// j and m should still have leases registered since not under k*
@@ -1220,15 +1178,11 @@ func TestLeasingPutGetDeleteConcurrent(t *testing.T) {
 	resp, err := lkvs[0].Get(context.TODO(), "k")
 	require.NoError(t, err)
 
-	if len(resp.Kvs) > 0 {
-		t.Fatalf("expected no kvs, got %+v", resp.Kvs)
-	}
+	require.Emptyf(t, resp.Kvs, "expected no kvs, got %+v", resp.Kvs)
 
 	resp, err = clus.Client(0).Get(context.TODO(), "k")
 	require.NoError(t, err)
-	if len(resp.Kvs) > 0 {
-		t.Fatalf("expected no kvs, got %+v", resp.Kvs)
-	}
+	require.Emptyf(t, resp.Kvs, "expected no kvs, got %+v", resp.Kvs)
 }
 
 // TestLeasingReconnectOwnerRevoke checks that revocation works if
@@ -1330,9 +1284,7 @@ func TestLeasingReconnectOwnerRevokeCompact(t *testing.T) {
 	require.NoError(t, err)
 	resp, err := lkv1.Get(cctx, "k")
 	require.NoError(t, err)
-	if string(resp.Kvs[0].Value) != "v" {
-		t.Fatalf(`expected "v" value, got %+v`, resp)
-	}
+	require.Equalf(t, "v", string(resp.Kvs[0].Value), `expected "v" value, got %+v`, resp)
 }
 
 // TestLeasingReconnectOwnerConsistency checks a write error on an owner will
@@ -1399,9 +1351,7 @@ func TestLeasingReconnectOwnerConsistency(t *testing.T) {
 	require.NoError(t, lerr)
 	cresp, cerr := clus.Client(0).Get(context.TODO(), "k")
 	require.NoError(t, cerr)
-	if !reflect.DeepEqual(lresp.Kvs, cresp.Kvs) {
-		t.Fatalf("expected %+v, got %+v", cresp, lresp)
-	}
+	require.Truef(t, reflect.DeepEqual(lresp.Kvs, cresp.Kvs), "expected %+v, got %+v", cresp, lresp)
 }
 
 func TestLeasingTxnAtomicCache(t *testing.T) {
@@ -1561,9 +1511,7 @@ func TestLeasingReconnectNonOwnerGet(t *testing.T) {
 		require.NoError(t, lerr)
 		cresp, cerr := clus.Client(0).Get(context.TODO(), k)
 		require.NoError(t, cerr)
-		if !reflect.DeepEqual(lresp.Kvs, cresp.Kvs) {
-			t.Fatalf("expected %+v, got %+v", cresp, lresp)
-		}
+		require.Truef(t, reflect.DeepEqual(lresp.Kvs, cresp.Kvs), "expected %+v, got %+v", cresp, lresp)
 	}
 }
 
@@ -1591,9 +1539,7 @@ func TestLeasingTxnRangeCmp(t *testing.T) {
 	cmp := clientv3.Compare(clientv3.Version("k").WithPrefix(), "=", 1)
 	tresp, terr := lkv.Txn(context.TODO()).If(cmp).Commit()
 	require.NoError(t, terr)
-	if tresp.Succeeded {
-		t.Fatalf("expected Succeeded=false, got %+v", tresp)
-	}
+	require.Falsef(t, tresp.Succeeded, "expected Succeeded=false, got %+v", tresp)
 }
 
 func TestLeasingDo(t *testing.T) {
@@ -1631,9 +1577,7 @@ func TestLeasingDo(t *testing.T) {
 
 	gresp, err := clus.Client(0).Get(context.TODO(), "a", clientv3.WithPrefix())
 	require.NoError(t, err)
-	if len(gresp.Kvs) != 0 {
-		t.Fatalf("expected no keys, got %+v", gresp.Kvs)
-	}
+	require.Emptyf(t, gresp.Kvs, "expected no keys, got %+v", gresp.Kvs)
 }
 
 func TestLeasingTxnOwnerPutBranch(t *testing.T) {
@@ -1667,9 +1611,7 @@ func TestLeasingTxnOwnerPutBranch(t *testing.T) {
 		require.NoError(t, err)
 		clusResp, err := clus.Client(1).Get(context.TODO(), k)
 		require.NoError(t, err)
-		if !reflect.DeepEqual(clusResp.Kvs, lkvResp.Kvs) {
-			t.Fatalf("expected %+v, got %+v", clusResp.Kvs, lkvResp.Kvs)
-		}
+		require.Truef(t, reflect.DeepEqual(clusResp.Kvs, lkvResp.Kvs), "expected %+v, got %+v", clusResp.Kvs, lkvResp.Kvs)
 	}
 }
 
@@ -1751,9 +1693,7 @@ func TestLeasingSessionExpire(t *testing.T) {
 
 	resp, err := lkv.Get(context.TODO(), "abc")
 	require.NoError(t, err)
-	if v := string(resp.Kvs[0].Value); v != "def" {
-		t.Fatalf("expected %q, got %q", "v", v)
-	}
+	require.Equal(t, "def", string(resp.Kvs[0].Value))
 }
 
 func TestLeasingSessionExpireCancel(t *testing.T) {
diff --git a/tests/integration/clientv3/maintenance_test.go b/tests/integration/clientv3/maintenance_test.go
index 7283006d2..6c75b63b8 100644
--- a/tests/integration/clientv3/maintenance_test.go
+++ b/tests/integration/clientv3/maintenance_test.go
@@ -31,6 +31,8 @@ import (
 	"github.com/stretchr/testify/require"
 	"go.uber.org/zap/zaptest"
 	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 
 	"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
 	"go.etcd.io/etcd/api/v3/version"
@@ -230,9 +232,22 @@ func testMaintenanceSnapshotTimeout(t *testing.T, snapshot func(context.Context,
 	time.Sleep(2 * time.Second)
 
 	_, err = io.Copy(io.Discard, rc2)
-	if err != nil && !IsClientTimeout(err) {
-		t.Errorf("expected client timeout, got %v", err)
+	if IsClientTimeout(err) {
+		return
 	}
+	// Assumes the client receives a single message header and then
+	// waits for the payload body. If the context is canceled before
+	// the payload arrives, the client will read io.EOF. However, the
+	// grpc-go client converts this into io.ErrUnexpectedEOF with an
+	// internal error code. Ideally, grpc-go might return context.Canceled
+	// instead, but it's unclear if that's feasible. Let's explicitly
+	// check for this error in the test code.
+	//
+	// REF: https://github.com/grpc/grpc-go/blob/6821606f351799b026fda1e6ba143315e6c1e620/rpc_util.go#L644
+	//
+	// Once https://github.com/grpc/grpc-go/issues/8281 is fixed, we should
+	// revert this change. See more discussion in https://github.com/etcd-io/etcd/pull/19833.
+	assert.ErrorIs(t, status.Error(codes.Internal, io.ErrUnexpectedEOF.Error()), err)
 }
 
 // TestMaintenanceSnapshotWithVersionErrorInflight ensures that ReaderCloser returned by SnapshotWithVersion function
diff --git a/tests/integration/clientv3/naming/endpoints_test.go b/tests/integration/clientv3/naming/endpoints_test.go
index 3c93c6c7d..649c05813 100644
--- a/tests/integration/clientv3/naming/endpoints_test.go
+++ b/tests/integration/clientv3/naming/endpoints_test.go
@@ -61,14 +61,10 @@ func TestEndpointManager(t *testing.T) {
 		Endpoint: e1,
 	}
 
-	if !reflect.DeepEqual(us[0], wu) {
-		t.Fatalf("up = %#v, want %#v", us[0], wu)
-	}
+	require.Truef(t, reflect.DeepEqual(us[0], wu), "up = %#v, want %#v", us[0], wu)
 
 	err = em.DeleteEndpoint(context.TODO(), "foo/a1")
-	if err != nil {
-		t.Fatalf("failed to udpate %v", err)
-	}
+	require.NoErrorf(t, err, "failed to udpate %v", err)
 
 	us = <-w
 	if us == nil {
@@ -80,9 +76,7 @@ func TestEndpointManager(t *testing.T) {
 		Key: "foo/a1",
 	}
 
-	if !reflect.DeepEqual(us[0], wu) {
-		t.Fatalf("up = %#v, want %#v", us[1], wu)
-	}
+	require.Truef(t, reflect.DeepEqual(us[0], wu), "up = %#v, want %#v", us[0], wu)
 }
 
 // TestEndpointManagerAtomicity ensures the resolver will initialize
@@ -112,9 +106,7 @@ func TestEndpointManagerAtomicity(t *testing.T) {
 	require.NoError(t, err)
 
 	updates := <-w
-	if len(updates) != 2 {
-		t.Fatalf("expected two updates, got %+v", updates)
-	}
+	require.Lenf(t, updates, 2, "expected two updates, got %+v", updates)
 
 	_, err = c.Txn(context.TODO()).Then(etcd.OpDelete("foo/host"), etcd.OpDelete("foo/host2")).Commit()
 	require.NoError(t, err)
@@ -155,15 +147,9 @@ func TestEndpointManagerCRUD(t *testing.T) {
 	if err != nil {
 		t.Fatal("failed to list foo")
 	}
-	if len(eps) != 2 {
-		t.Fatalf("unexpected the number of endpoints: %d", len(eps))
-	}
-	if !reflect.DeepEqual(eps[k1], e1) {
-		t.Fatalf("unexpected endpoints: %s", k1)
-	}
-	if !reflect.DeepEqual(eps[k2], e2) {
-		t.Fatalf("unexpected endpoints: %s", k2)
-	}
+	require.Lenf(t, eps, 2, "unexpected the number of endpoints: %d", len(eps))
+	require.Truef(t, reflect.DeepEqual(eps[k1], e1), "unexpected endpoints: %s", k1)
+	require.Truef(t, reflect.DeepEqual(eps[k2], e2), "unexpected endpoints: %s", k2)
 
 	// Delete
 	err = em.DeleteEndpoint(context.TODO(), k1)
@@ -175,12 +161,8 @@ func TestEndpointManagerCRUD(t *testing.T) {
 	if err != nil {
 		t.Fatal("failed to list foo")
 	}
-	if len(eps) != 1 {
-		t.Fatalf("unexpected the number of endpoints: %d", len(eps))
-	}
-	if !reflect.DeepEqual(eps[k2], e2) {
-		t.Fatalf("unexpected endpoints: %s", k2)
-	}
+	require.Lenf(t, eps, 1, "unexpected the number of endpoints: %d", len(eps))
+	require.Truef(t, reflect.DeepEqual(eps[k2], e2), "unexpected endpoints: %s", k2)
 
 	// Update
 	k3 := "foo/a3"
@@ -198,10 +180,6 @@ func TestEndpointManagerCRUD(t *testing.T) {
 	if err != nil {
 		t.Fatal("failed to list foo")
 	}
-	if len(eps) != 1 {
-		t.Fatalf("unexpected the number of endpoints: %d", len(eps))
-	}
-	if !reflect.DeepEqual(eps[k3], e3) {
-		t.Fatalf("unexpected endpoints: %s", k3)
-	}
+	require.Lenf(t, eps, 1, "unexpected the number of endpoints: %d", len(eps))
+	require.Truef(t, reflect.DeepEqual(eps[k3], e3), "unexpected endpoints: %s", k3)
 }
diff --git a/tests/integration/clientv3/naming/resolver_test.go b/tests/integration/clientv3/naming/resolver_test.go
index f39dbb2c2..40dc17721 100644
--- a/tests/integration/clientv3/naming/resolver_test.go
+++ b/tests/integration/clientv3/naming/resolver_test.go
@@ -104,26 +104,20 @@ func testEtcdGRPCResolver(t *testing.T, lbPolicy string) {
 
 		t.Logf("Response: %v", string(resp.GetPayload().GetBody()))
 
-		if resp.GetPayload() == nil {
-			t.Fatalf("unexpected response from foo: %s", resp.GetPayload().GetBody())
-		}
+		require.NotNilf(t, resp.GetPayload(), "unexpected response from foo: %s", resp.GetPayload().GetBody())
 		lastResponse = resp.GetPayload().GetBody()
 	}
 
 	// If the load balancing policy is pick first then return payload should equal number of requests
 	t.Logf("Last response: %v", string(lastResponse))
 	if lbPolicy == "pick_first" {
-		if string(lastResponse) != "3500" {
-			t.Fatalf("unexpected total responses from foo: %s", lastResponse)
-		}
+		require.Equalf(t, "3500", string(lastResponse), "unexpected total responses from foo: %s", lastResponse)
 	}
 
 	// If the load balancing policy is round robin we should see roughly half total requests served by each server
 	if lbPolicy == "round_robin" {
 		responses, err := strconv.Atoi(string(lastResponse))
-		if err != nil {
-			t.Fatalf("couldn't convert to int: %s", lastResponse)
-		}
+		require.NoErrorf(t, err, "couldn't convert to int: %s", lastResponse)
 
 		// Allow 25% tolerance as round robin is not perfect and we don't want the test to flake
 		expected := float64(totalRequests) * 0.5
diff --git a/tests/integration/clientv3/ordering_util_test.go b/tests/integration/clientv3/ordering_util_test.go
index bb46989e4..f7513190d 100644
--- a/tests/integration/clientv3/ordering_util_test.go
+++ b/tests/integration/clientv3/ordering_util_test.go
@@ -138,7 +138,5 @@ func TestUnresolvableOrderViolation(t *testing.T) {
 	time.Sleep(1 * time.Second) // give enough time for operation
 
 	_, err = OrderingKv.Get(ctx, "foo", clientv3.WithSerializable())
-	if !errors.Is(err, ordering.ErrNoGreaterRev) {
-		t.Fatalf("expected %v, got %v", ordering.ErrNoGreaterRev, err)
-	}
+	require.ErrorIsf(t, err, ordering.ErrNoGreaterRev, "expected %v, got %v", ordering.ErrNoGreaterRev, err)
 }
diff --git a/tests/integration/clientv3/snapshot/v3_snapshot_test.go b/tests/integration/clientv3/snapshot/v3_snapshot_test.go
index c39b092e1..1da423f5c 100644
--- a/tests/integration/clientv3/snapshot/v3_snapshot_test.go
+++ b/tests/integration/clientv3/snapshot/v3_snapshot_test.go
@@ -44,14 +44,10 @@ func TestSaveSnapshotFilePermissions(t *testing.T) {
 	defer os.RemoveAll(dbPath)
 
 	dbInfo, err := os.Stat(dbPath)
-	if err != nil {
-		t.Fatalf("failed to get test snapshot file status: %v", err)
-	}
+	require.NoErrorf(t, err, "failed to get test snapshot file status: %v", err)
 	actualFileMode := dbInfo.Mode()
 
-	if expectedFileMode != actualFileMode {
-		t.Fatalf("expected test snapshot file mode %s, got %s:", expectedFileMode, actualFileMode)
-	}
+	require.Equalf(t, expectedFileMode, actualFileMode, "expected test snapshot file mode %s, got %s:", expectedFileMode, actualFileMode)
 }
 
 // TestSaveSnapshotVersion ensures that the snapshot returns proper storage version.
@@ -67,9 +63,7 @@ func TestSaveSnapshotVersion(t *testing.T) {
 	ver, dbPath := createSnapshotFile(t, cfg, kvs)
 	defer os.RemoveAll(dbPath)
 
-	if ver != "3.6.0" {
-		t.Fatalf("expected snapshot version %s, got %s:", "3.6.0", ver)
-	}
+	require.Equalf(t, "3.6.0", ver, "expected snapshot version %s, got %s:", "3.6.0", ver)
 }
 
 type kv struct {
diff --git a/tests/integration/clientv3/user_test.go b/tests/integration/clientv3/user_test.go
index 201740fef..8f3ef8380 100644
--- a/tests/integration/clientv3/user_test.go
+++ b/tests/integration/clientv3/user_test.go
@@ -40,19 +40,13 @@ func TestUserError(t *testing.T) {
 	require.NoError(t, err)
 
 	_, err = authapi.UserAdd(context.TODO(), "foo", "bar")
-	if !errors.Is(err, rpctypes.ErrUserAlreadyExist) {
-		t.Fatalf("expected %v, got %v", rpctypes.ErrUserAlreadyExist, err)
-	}
+	require.ErrorIsf(t, err, rpctypes.ErrUserAlreadyExist, "expected %v, got %v", rpctypes.ErrUserAlreadyExist, err)
 
 	_, err = authapi.UserDelete(context.TODO(), "not-exist-user")
-	if !errors.Is(err, rpctypes.ErrUserNotFound) {
-		t.Fatalf("expected %v, got %v", rpctypes.ErrUserNotFound, err)
-	}
+	require.ErrorIsf(t, err, rpctypes.ErrUserNotFound, "expected %v, got %v", rpctypes.ErrUserNotFound, err)
 
 	_, err = authapi.UserGrantRole(context.TODO(), "foo", "test-role-does-not-exist")
-	if !errors.Is(err, rpctypes.ErrRoleNotFound) {
-		t.Fatalf("expected %v, got %v", rpctypes.ErrRoleNotFound, err)
-	}
+	require.ErrorIsf(t, err, rpctypes.ErrRoleNotFound, "expected %v, got %v", rpctypes.ErrRoleNotFound, err)
 }
 
 func TestAddUserAfterDelete(t *testing.T) {
@@ -115,9 +109,8 @@ func TestUserErrorAuth(t *testing.T) {
 	authSetupRoot(t, authapi.Auth)
 
 	// unauthenticated client
-	if _, err := authapi.UserAdd(context.TODO(), "foo", "bar"); !errors.Is(err, rpctypes.ErrUserEmpty) {
-		t.Fatalf("expected %v, got %v", rpctypes.ErrUserEmpty, err)
-	}
+	_, err := authapi.UserAdd(context.TODO(), "foo", "bar")
+	require.ErrorIsf(t, err, rpctypes.ErrUserEmpty, "expected %v, got %v", rpctypes.ErrUserEmpty, err)
 
 	// wrong id or password
 	cfg := clientv3.Config{
@@ -126,13 +119,11 @@ func TestUserErrorAuth(t *testing.T) {
 		DialOptions: []grpc.DialOption{grpc.WithBlock()},
 	}
 	cfg.Username, cfg.Password = "wrong-id", "123"
-	if _, err := integration2.NewClient(t, cfg); !errors.Is(err, rpctypes.ErrAuthFailed) {
-		t.Fatalf("expected %v, got %v", rpctypes.ErrAuthFailed, err)
-	}
+	_, err = integration2.NewClient(t, cfg)
+	require.ErrorIsf(t, err, rpctypes.ErrAuthFailed, "expected %v, got %v", rpctypes.ErrAuthFailed, err)
 	cfg.Username, cfg.Password = "root", "wrong-pass"
-	if _, err := integration2.NewClient(t, cfg); !errors.Is(err, rpctypes.ErrAuthFailed) {
-		t.Fatalf("expected %v, got %v", rpctypes.ErrAuthFailed, err)
-	}
+	_, err = integration2.NewClient(t, cfg)
+	require.ErrorIsf(t, err, rpctypes.ErrAuthFailed, "expected %v, got %v", rpctypes.ErrAuthFailed, err)
 
 	cfg.Username, cfg.Password = "root", "123"
 	authed, err := integration2.NewClient(t, cfg)
diff --git a/tests/integration/clientv3/watch_fragment_test.go b/tests/integration/clientv3/watch_fragment_test.go
index 81450f5f9..fd58fed81 100644
--- a/tests/integration/clientv3/watch_fragment_test.go
+++ b/tests/integration/clientv3/watch_fragment_test.go
@@ -23,6 +23,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	"go.etcd.io/etcd/client/pkg/v3/testutil"
 	clientv3 "go.etcd.io/etcd/client/v3"
 	integration2 "go.etcd.io/etcd/tests/v3/framework/integration"
@@ -87,9 +89,8 @@ func testWatchFragment(t *testing.T, fragment, exceedRecvLimit bool) {
 		}(i)
 	}
 	for i := 0; i < 10; i++ {
-		if err := <-errc; err != nil {
-			t.Fatalf("failed to put: %v", err)
-		}
+		err := <-errc
+		require.NoErrorf(t, err, "failed to put")
 	}
 
 	opts := []clientv3.OpOption{clientv3.WithPrefix(), clientv3.WithRev(1)}
@@ -103,23 +104,15 @@ func testWatchFragment(t *testing.T, fragment, exceedRecvLimit bool) {
 	case ws := <-wch:
 		// without fragment, should exceed gRPC client receive limit
 		if !fragment && exceedRecvLimit {
-			if len(ws.Events) != 0 {
-				t.Fatalf("expected 0 events with watch fragmentation, got %d", len(ws.Events))
-			}
+			require.Emptyf(t, ws.Events, "expected 0 events with watch fragmentation")
 			exp := "code = ResourceExhausted desc = grpc: received message larger than max ("
-			if !strings.Contains(ws.Err().Error(), exp) {
-				t.Fatalf("expected 'ResourceExhausted' error, got %v", ws.Err())
-			}
+			require.Containsf(t, ws.Err().Error(), exp, "expected 'ResourceExhausted' error")
 			return
 		}
 
 		// still expect merged watch events
-		if len(ws.Events) != 10 {
-			t.Fatalf("expected 10 events with watch fragmentation, got %d", len(ws.Events))
-		}
-		if ws.Err() != nil {
-			t.Fatalf("unexpected error %v", ws.Err())
-		}
+		require.Lenf(t, ws.Events, 10, "expected 10 events with watch fragmentation")
+		require.NoErrorf(t, ws.Err(), "unexpected error")
 
 	case <-time.After(testutil.RequestTimeout):
 		t.Fatalf("took too long to receive events")
diff --git a/tests/integration/clientv3/watch_test.go b/tests/integration/clientv3/watch_test.go
index f3f5c548d..bdb913d32 100644
--- a/tests/integration/clientv3/watch_test.go
+++ b/tests/integration/clientv3/watch_test.go
@@ -172,9 +172,8 @@ func TestWatchRange(t *testing.T) {
 }
 
 func testWatchRange(t *testing.T, wctx *watchctx) {
-	if wctx.ch = wctx.w.Watch(context.TODO(), "a", clientv3.WithRange("c")); wctx.ch == nil {
-		t.Fatalf("expected non-nil channel")
-	}
+	wctx.ch = wctx.w.Watch(context.TODO(), "a", clientv3.WithRange("c"))
+	require.NotNilf(t, wctx.ch, "expected non-nil channel")
 	putAndWatch(t, wctx, "a", "a")
 	putAndWatch(t, wctx, "b", "b")
 	putAndWatch(t, wctx, "bar", "bar")
@@ -204,9 +203,8 @@ func testWatchReconnRequest(t *testing.T, wctx *watchctx) {
 		}
 	}()
 	// should reconnect when requesting watch
-	if wctx.ch = wctx.w.Watch(context.TODO(), "a"); wctx.ch == nil {
-		t.Fatalf("expected non-nil channel")
-	}
+	wctx.ch = wctx.w.Watch(context.TODO(), "a")
+	require.NotNilf(t, wctx.ch, "expected non-nil channel")
 
 	// wait for disconnections to stop
 	stopc <- struct{}{}
@@ -230,9 +228,8 @@ func TestWatchReconnInit(t *testing.T) {
 }
 
 func testWatchReconnInit(t *testing.T, wctx *watchctx) {
-	if wctx.ch = wctx.w.Watch(context.TODO(), "a"); wctx.ch == nil {
-		t.Fatalf("expected non-nil channel")
-	}
+	wctx.ch = wctx.w.Watch(context.TODO(), "a")
+	require.NotNilf(t, wctx.ch, "expected non-nil channel")
 	wctx.clus.Members[wctx.wclientMember].Bridge().DropConnections()
 	// watcher should recover
 	putAndWatch(t, wctx, "a", "a")
@@ -245,9 +242,8 @@ func TestWatchReconnRunning(t *testing.T) {
 }
 
 func testWatchReconnRunning(t *testing.T, wctx *watchctx) {
-	if wctx.ch = wctx.w.Watch(context.TODO(), "a"); wctx.ch == nil {
-		t.Fatalf("expected non-nil channel")
-	}
+	wctx.ch = wctx.w.Watch(context.TODO(), "a")
+	require.NotNilf(t, wctx.ch, "expected non-nil channel")
 	putAndWatch(t, wctx, "a", "a")
 	// take down watcher connection
 	wctx.clus.Members[wctx.wclientMember].Bridge().DropConnections()
@@ -267,9 +263,7 @@ func testWatchCancelImmediate(t *testing.T, wctx *watchctx) {
 	wch := wctx.w.Watch(ctx, "a")
 	select {
 	case wresp, ok := <-wch:
-		if ok {
-			t.Fatalf("read wch got %v; expected closed channel", wresp)
-		}
+		require.Falsef(t, ok, "read wch got %v; expected closed channel", wresp)
 	default:
 		t.Fatalf("closed watcher channel should not block")
 	}
@@ -282,17 +276,14 @@ func TestWatchCancelInit(t *testing.T) {
 
 func testWatchCancelInit(t *testing.T, wctx *watchctx) {
 	ctx, cancel := context.WithCancel(context.Background())
-	if wctx.ch = wctx.w.Watch(ctx, "a"); wctx.ch == nil {
-		t.Fatalf("expected non-nil watcher channel")
-	}
+	wctx.ch = wctx.w.Watch(ctx, "a")
+	require.NotNilf(t, wctx.ch, "expected non-nil watcher channel")
 	cancel()
 	select {
 	case <-time.After(time.Second):
 		t.Fatalf("took too long to cancel")
 	case _, ok := <-wctx.ch:
-		if ok {
-			t.Fatalf("expected watcher channel to close")
-		}
+		require.Falsef(t, ok, "expected watcher channel to close")
 	}
 }
 
@@ -303,9 +294,8 @@ func TestWatchCancelRunning(t *testing.T) {
 
 func testWatchCancelRunning(t *testing.T, wctx *watchctx) {
 	ctx, cancel := context.WithCancel(context.Background())
-	if wctx.ch = wctx.w.Watch(ctx, "a"); wctx.ch == nil {
-		t.Fatalf("expected non-nil watcher channel")
-	}
+	wctx.ch = wctx.w.Watch(ctx, "a")
+	require.NotNilf(t, wctx.ch, "expected non-nil watcher channel")
 	_, err := wctx.kv.Put(ctx, "a", "a")
 	require.NoError(t, err)
 	cancel()
@@ -322,9 +312,7 @@ func testWatchCancelRunning(t *testing.T, wctx *watchctx) {
 		case <-time.After(time.Second):
 			t.Fatalf("took too long to close")
 		case v, ok2 := <-wctx.ch:
-			if ok2 {
-				t.Fatalf("expected watcher channel to close, got %v", v)
-			}
+			require.Falsef(t, ok2, "expected watcher channel to close, got %v", v)
 		}
 	}
 }
@@ -336,15 +324,10 @@ func putAndWatch(t *testing.T, wctx *watchctx, key, val string) {
 	case <-time.After(5 * time.Second):
 		t.Fatalf("watch timed out")
 	case v, ok := <-wctx.ch:
-		if !ok {
-			t.Fatalf("unexpected watch close")
-		}
-		if err := v.Err(); err != nil {
-			t.Fatalf("unexpected watch response error: %v", err)
-		}
-		if string(v.Events[0].Kv.Value) != val {
-			t.Fatalf("bad value got %v, wanted %v", v.Events[0].Kv.Value, val)
-		}
+		require.Truef(t, ok, "unexpected watch close")
+		err := v.Err()
+		require.NoErrorf(t, err, "unexpected watch response error")
+		require.Equalf(t, string(v.Events[0].Kv.Value), val, "bad value got %v, wanted %v", v.Events[0].Kv.Value, val)
 	}
 }
 
@@ -393,12 +376,8 @@ func TestWatchResumeAfterDisconnect(t *testing.T) {
 		if len(resp.Events) != 2 {
 			t.Fatal("expected two events on watch")
 		}
-		if string(resp.Events[0].Kv.Value) != "3" {
-			t.Fatalf("expected value=3, got event %+v", resp.Events[0])
-		}
-		if string(resp.Events[1].Kv.Value) != "4" {
-			t.Fatalf("expected value=4, got event %+v", resp.Events[1])
-		}
+		require.Equalf(t, "3", string(resp.Events[0].Kv.Value), "expected value=3, got event %+v", resp.Events[0])
+		require.Equalf(t, "4", string(resp.Events[1].Kv.Value), "expected value=4, got event %+v", resp.Events[1])
 	case <-time.After(5 * time.Second):
 		t.Fatal("watch timed out")
 	}
@@ -449,16 +428,12 @@ func TestWatchResumeCompacted(t *testing.T) {
 		var ok bool
 		select {
 		case wresp, ok = <-wch:
-			if !ok {
-				t.Fatalf("expected wresp, but got closed channel")
-			}
+			require.Truef(t, ok, "expected wresp, but got closed channel")
 		case <-time.After(5 * time.Second):
 			t.Fatalf("compacted watch timed out")
 		}
 		for _, ev := range wresp.Events {
-			if ev.Kv.ModRevision != wRev {
-				t.Fatalf("expected modRev %v, got %+v", wRev, ev)
-			}
+			require.Equalf(t, ev.Kv.ModRevision, wRev, "expected modRev %v, got %+v", wRev, ev)
 			wRev++
 		}
 		if wresp.Err() == nil {
@@ -643,9 +618,7 @@ func TestWatchRequestProgress(t *testing.T) {
 			for _, rch := range watchChans {
 				select {
 				case resp := <-rch: // wait for notification
-					if len(resp.Events) != 1 {
-						t.Fatalf("resp.Events expected 1, got %d", len(resp.Events))
-					}
+					require.Lenf(t, resp.Events, 1, "resp.Events expected 1, got %d", len(resp.Events))
 				case <-time.After(watchTimeout):
 					t.Fatalf("watch response expected in %v, but timed out", watchTimeout)
 				}
@@ -661,12 +634,8 @@ func TestWatchRequestProgress(t *testing.T) {
 			for _, rch := range watchChans {
 				select {
 				case resp := <-rch:
-					if !resp.IsProgressNotify() {
-						t.Fatalf("expected resp.IsProgressNotify() == true")
-					}
-					if resp.Header.Revision != 3 {
-						t.Fatalf("resp.Header.Revision expected 3, got %d", resp.Header.Revision)
-					}
+					require.Truef(t, resp.IsProgressNotify(), "expected resp.IsProgressNotify() == true")
+					require.Equalf(t, int64(3), resp.Header.Revision, "resp.Header.Revision expected 3, got %d", resp.Header.Revision)
 				case <-time.After(watchTimeout):
 					t.Fatalf("progress response expected in %v, but timed out", watchTimeout)
 				}
@@ -685,22 +654,16 @@ func TestWatchEventType(t *testing.T) {
 	ctx := context.Background()
 	watchChan := client.Watch(ctx, "/", clientv3.WithPrefix())
 
-	if _, err := client.Put(ctx, "/toDelete", "foo"); err != nil {
-		t.Fatalf("Put failed: %v", err)
-	}
-	if _, err := client.Put(ctx, "/toDelete", "bar"); err != nil {
-		t.Fatalf("Put failed: %v", err)
-	}
-	if _, err := client.Delete(ctx, "/toDelete"); err != nil {
-		t.Fatalf("Delete failed: %v", err)
-	}
+	_, err := client.Put(ctx, "/toDelete", "foo")
+	require.NoErrorf(t, err, "Put failed: %v", err)
+	_, err = client.Put(ctx, "/toDelete", "bar")
+	require.NoErrorf(t, err, "Put failed: %v", err)
+	_, err = client.Delete(ctx, "/toDelete")
+	require.NoErrorf(t, err, "Delete failed: %v", err)
 	lcr, err := client.Lease.Grant(ctx, 1)
-	if err != nil {
-		t.Fatalf("lease create failed: %v", err)
-	}
-	if _, err := client.Put(ctx, "/toExpire", "foo", clientv3.WithLease(lcr.ID)); err != nil {
-		t.Fatalf("Put failed: %v", err)
-	}
+	require.NoErrorf(t, err, "lease create failed: %v", err)
+	_, err = client.Put(ctx, "/toExpire", "foo", clientv3.WithLease(lcr.ID))
+	require.NoErrorf(t, err, "Put failed: %v", err)
 
 	tests := []struct {
 		et       mvccpb.Event_EventType
@@ -835,28 +798,21 @@ func TestWatchWithRequireLeader(t *testing.T) {
 
 	select {
 	case resp, ok := <-chLeader:
-		if !ok {
-			t.Fatalf("expected %v watch channel, got closed channel", rpctypes.ErrNoLeader)
-		}
-		if !errors.Is(resp.Err(), rpctypes.ErrNoLeader) {
-			t.Fatalf("expected %v watch response error, got %+v", rpctypes.ErrNoLeader, resp)
-		}
+		require.Truef(t, ok, "expected %v watch channel, got closed channel", rpctypes.ErrNoLeader)
+		require.ErrorIsf(t, resp.Err(), rpctypes.ErrNoLeader, "expected %v watch response error, got %+v", rpctypes.ErrNoLeader, resp)
 	case <-time.After(integration2.RequestWaitTimeout):
 		t.Fatal("watch without leader took too long to close")
 	}
 
 	select {
 	case resp, ok := <-chLeader:
-		if ok {
-			t.Fatalf("expected closed channel, got response %v", resp)
-		}
+		require.Falsef(t, ok, "expected closed channel, got response %v", resp)
 	case <-time.After(integration2.RequestWaitTimeout):
 		t.Fatal("waited too long for channel to close")
 	}
 
-	if _, ok := <-chNoLeader; !ok {
-		t.Fatalf("expected response, got closed channel")
-	}
+	_, ok := <-chNoLeader
+	require.Truef(t, ok, "expected response, got closed channel")
 
 	cnt, err := clus.Members[0].Metric(
 		"etcd_server_client_requests_total",
@@ -866,9 +822,7 @@ func TestWatchWithRequireLeader(t *testing.T) {
 	require.NoError(t, err)
 	cv, err := strconv.ParseInt(cnt, 10, 32)
 	require.NoError(t, err)
-	if cv < 2 { // >2 when retried
-		t.Fatalf("expected at least 2, got %q", cnt)
-	}
+	require.GreaterOrEqualf(t, cv, int64(2), "expected at least 2, got %q", cnt)
 }
 
 // TestWatchWithFilter checks that watch filtering works.
@@ -923,9 +877,7 @@ func TestWatchWithCreatedNotification(t *testing.T) {
 
 	resp := <-createC
 
-	if !resp.Created {
-		t.Fatalf("expected created event, got %v", resp)
-	}
+	require.Truef(t, resp.Created, "expected created event, got %v", resp)
 }
 
 // TestWatchWithCreatedNotificationDropConn ensures that
@@ -943,9 +895,7 @@ func TestWatchWithCreatedNotificationDropConn(t *testing.T) {
 
 	resp := <-wch
 
-	if !resp.Created {
-		t.Fatalf("expected created event, got %v", resp)
-	}
+	require.Truef(t, resp.Created, "expected created event, got %v", resp)
 
 	cluster.Members[0].Bridge().DropConnections()
 
@@ -1012,9 +962,7 @@ func TestWatchCancelOnServer(t *testing.T) {
 		t.Fatalf("expected n=2 and err=nil, got n=%d and err=%v", n, serr)
 	}
 
-	if maxWatchV-minWatchV < numWatches {
-		t.Fatalf("expected %d canceled watchers, got %d", numWatches, maxWatchV-minWatchV)
-	}
+	require.GreaterOrEqualf(t, maxWatchV-minWatchV, numWatches, "expected %d canceled watchers, got %d", numWatches, maxWatchV-minWatchV)
 }
 
 // TestWatchOverlapContextCancel stresses the watcher stream teardown path by
@@ -1180,14 +1128,8 @@ func testWatchClose(t *testing.T, wctx *watchctx) {
 	ctx, cancel := context.WithCancel(context.Background())
 	wch := wctx.w.Watch(ctx, "a")
 	cancel()
-	if wch == nil {
-		t.Fatalf("expected watcher channel, got nil")
-	}
-	if wctx.w.Close() != nil {
-		t.Fatalf("watch did not close successfully")
-	}
+	require.NotNilf(t, wch, "expected watcher channel, got nil")
+	require.NoErrorf(t, wctx.w.Close(), "watch did not close successfully")
 	wresp, ok := <-wch
-	if ok {
-		t.Fatalf("read wch got %v; expected closed channel", wresp)
-	}
+	require.Falsef(t, ok, "read wch got %v; expected closed channel", wresp)
 }
diff --git a/tests/integration/cluster_test.go b/tests/integration/cluster_test.go
index 732f5e2e1..9ba372bb8 100644
--- a/tests/integration/cluster_test.go
+++ b/tests/integration/cluster_test.go
@@ -132,9 +132,7 @@ func TestForceNewCluster(t *testing.T) {
 
 	ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
 	resp, err := c.Members[0].Client.Put(ctx, "/foo", "bar")
-	if err != nil {
-		t.Fatalf("unexpected create error: %v", err)
-	}
+	require.NoErrorf(t, err, "unexpected create error")
 	cancel()
 	// ensure create has been applied in this machine
 	ctx, cancel = context.WithTimeout(context.Background(), integration.RequestTimeout)
@@ -143,12 +141,8 @@ func TestForceNewCluster(t *testing.T) {
 		if len(resp.Events) != 0 {
 			break
 		}
-		if resp.Err() != nil {
-			t.Fatalf("unexpected watch error: %q", resp.Err())
-		}
-		if resp.Canceled {
-			t.Fatalf("watch  cancelled")
-		}
+		require.NoErrorf(t, resp.Err(), "unexpected watch error")
+		require.Falsef(t, resp.Canceled, "watch  cancelled")
 	}
 	cancel()
 
@@ -157,9 +151,7 @@ func TestForceNewCluster(t *testing.T) {
 	c.Members[2].Terminate(t)
 	c.Members[0].ForceNewCluster = true
 	err = c.Members[0].Restart(t)
-	if err != nil {
-		t.Fatalf("unexpected ForceRestart error: %v", err)
-	}
+	require.NoErrorf(t, err, "unexpected ForceRestart error")
 	c.WaitMembersForLeader(t, c.Members[:1])
 
 	// use new http client to init new connection
@@ -170,12 +162,8 @@ func TestForceNewCluster(t *testing.T) {
 		if len(resp.Events) != 0 {
 			break
 		}
-		if resp.Err() != nil {
-			t.Fatalf("unexpected watch error: %q", resp.Err())
-		}
-		if resp.Canceled {
-			t.Fatalf("watch  cancelled")
-		}
+		require.NoErrorf(t, resp.Err(), "unexpected watch error")
+		require.Falsef(t, resp.Canceled, "watch  cancelled")
 	}
 	cancel()
 	clusterMustProgress(t, c.Members[:1])
@@ -325,9 +313,8 @@ func TestIssue3699(t *testing.T) {
 	t.Logf("Expecting successful put...")
 	// try to participate in Cluster
 	ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
-	if _, err := c.Members[0].Client.Put(ctx, "/foo", "bar"); err != nil {
-		t.Fatalf("unexpected error on Put (%v)", err)
-	}
+	_, err := c.Members[0].Client.Put(ctx, "/foo", "bar")
+	require.NoErrorf(t, err, "unexpected error on Put")
 	cancel()
 }
 
@@ -344,9 +331,7 @@ func TestRejectUnhealthyAdd(t *testing.T) {
 	// all attempts to add member should fail
 	for i := 1; i < len(c.Members); i++ {
 		err := c.AddMemberByURL(t, c.Members[i].Client, "unix://foo:12345")
-		if err == nil {
-			t.Fatalf("should have failed adding peer")
-		}
+		require.Errorf(t, err, "should have failed adding peer")
 		// TODO: client should return descriptive error codes for internal errors
 		if !strings.Contains(err.Error(), "unhealthy cluster") {
 			t.Errorf("unexpected error (%v)", err)
@@ -365,9 +350,7 @@ func TestRejectUnhealthyAdd(t *testing.T) {
 			break
 		}
 	}
-	if err != nil {
-		t.Fatalf("should have added peer to healthy Cluster (%v)", err)
-	}
+	require.NoErrorf(t, err, "should have added peer to healthy Cluster (%v)", err)
 }
 
 // TestRejectUnhealthyRemove ensures an unhealthy cluster rejects removing members
@@ -384,9 +367,7 @@ func TestRejectUnhealthyRemove(t *testing.T) {
 
 	// reject remove active member since (3,2)-(1,0) => (2,2) lacks quorum
 	err := c.RemoveMember(t, c.Members[leader].Client, uint64(c.Members[2].Server.MemberID()))
-	if err == nil {
-		t.Fatalf("should reject quorum breaking remove: %s", err)
-	}
+	require.Errorf(t, err, "should reject quorum breaking remove: %s", err)
 	// TODO: client should return more descriptive error codes for internal errors
 	if !strings.Contains(err.Error(), "unhealthy cluster") {
 		t.Errorf("unexpected error (%v)", err)
@@ -396,9 +377,8 @@ func TestRejectUnhealthyRemove(t *testing.T) {
 	time.Sleep(time.Duration(integration.ElectionTicks * int(config.TickDuration)))
 
 	// permit remove dead member since (3,2) - (0,1) => (3,1) has quorum
-	if err = c.RemoveMember(t, c.Members[2].Client, uint64(c.Members[0].Server.MemberID())); err != nil {
-		t.Fatalf("should accept removing down member: %s", err)
-	}
+	err = c.RemoveMember(t, c.Members[2].Client, uint64(c.Members[0].Server.MemberID()))
+	require.NoErrorf(t, err, "should accept removing down member")
 
 	// bring cluster to (4,1)
 	c.Members[0].Restart(t)
@@ -407,9 +387,8 @@ func TestRejectUnhealthyRemove(t *testing.T) {
 	time.Sleep((3 * etcdserver.HealthInterval) / 2)
 
 	// accept remove member since (4,1)-(1,0) => (3,1) has quorum
-	if err = c.RemoveMember(t, c.Members[1].Client, uint64(c.Members[0].Server.MemberID())); err != nil {
-		t.Fatalf("expected to remove member, got error %v", err)
-	}
+	err = c.RemoveMember(t, c.Members[1].Client, uint64(c.Members[0].Server.MemberID()))
+	require.NoErrorf(t, err, "expected to remove member, got error")
 }
 
 // TestRestartRemoved ensures that restarting removed member must exit
@@ -431,17 +410,15 @@ func TestRestartRemoved(t *testing.T) {
 	firstMember.KeepDataDirTerminate = true
 
 	// 3. remove first member, shut down without deleting data
-	if err := c.RemoveMember(t, c.Members[1].Client, uint64(firstMember.Server.MemberID())); err != nil {
-		t.Fatalf("expected to remove member, got error %v", err)
-	}
+	err := c.RemoveMember(t, c.Members[1].Client, uint64(firstMember.Server.MemberID()))
+	require.NoErrorf(t, err, "expected to remove member, got error")
 	c.WaitLeader(t)
 
 	// 4. restart first member with 'initial-cluster-state=new'
 	// wrong config, expects exit within ReqTimeout
 	firstMember.ServerConfig.NewCluster = false
-	if err := firstMember.Restart(t); err != nil {
-		t.Fatalf("unexpected ForceRestart error: %v", err)
-	}
+	err = firstMember.Restart(t)
+	require.NoErrorf(t, err, "unexpected ForceRestart error")
 	defer func() {
 		firstMember.Close()
 		os.RemoveAll(firstMember.ServerConfig.DataDir)
@@ -472,9 +449,7 @@ func clusterMustProgress(t *testing.T, members []*integration.Member) {
 		}
 		t.Logf("failed to create key on #0 (%v)", err)
 	}
-	if err != nil {
-		t.Fatalf("create on #0 error: %v", err)
-	}
+	require.NoErrorf(t, err, "create on #0 error")
 
 	for i, m := range members {
 		mctx, mcancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
@@ -483,12 +458,8 @@ func clusterMustProgress(t *testing.T, members []*integration.Member) {
 			if len(resp.Events) != 0 {
 				break
 			}
-			if resp.Err() != nil {
-				t.Fatalf("#%d: watch error: %q", i, resp.Err())
-			}
-			if resp.Canceled {
-				t.Fatalf("#%d: watch: cancelled", i)
-			}
+			require.NoErrorf(t, resp.Err(), "#%d: watch error", i)
+			require.Falsef(t, resp.Canceled, "#%d: watch: cancelled", i)
 		}
 		mcancel()
 	}
diff --git a/tests/integration/member_test.go b/tests/integration/member_test.go
index b012370ab..32a4c40a0 100644
--- a/tests/integration/member_test.go
+++ b/tests/integration/member_test.go
@@ -94,9 +94,7 @@ func TestSnapshotAndRestartMember(t *testing.T) {
 		ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
 		key := fmt.Sprintf("foo%d", i)
 		_, err = m.Client.Put(ctx, "/"+key, "bar")
-		if err != nil {
-			t.Fatalf("#%d: create on %s error: %v", i, m.URL(), err)
-		}
+		require.NoErrorf(t, err, "#%d: create on %s error", i, m.URL())
 		cancel()
 	}
 	m.Stop(t)
@@ -107,9 +105,7 @@ func TestSnapshotAndRestartMember(t *testing.T) {
 		ctx, cancel := context.WithTimeout(context.Background(), integration.RequestTimeout)
 		key := fmt.Sprintf("foo%d", i)
 		resp, err := m.Client.Get(ctx, "/"+key)
-		if err != nil {
-			t.Fatalf("#%d: get on %s error: %v", i, m.URL(), err)
-		}
+		require.NoErrorf(t, err, "#%d: get on %s error", i, m.URL())
 		cancel()
 
 		if len(resp.Kvs) != 1 || string(resp.Kvs[0].Value) != "bar" {
diff --git a/tests/integration/metrics_test.go b/tests/integration/metrics_test.go
index 8212bb773..1fc1117c5 100644
--- a/tests/integration/metrics_test.go
+++ b/tests/integration/metrics_test.go
@@ -40,9 +40,7 @@ func TestMetricDbSizeBoot(t *testing.T) {
 	v, err := clus.Members[0].Metric("etcd_debugging_mvcc_db_total_size_in_bytes")
 	require.NoError(t, err)
 
-	if v == "0" {
-		t.Fatalf("expected non-zero, got %q", v)
-	}
+	require.NotEqualf(t, "0", v, "expected non-zero, got %q", v)
 }
 
 func TestMetricDbSizeDefrag(t *testing.T) {
@@ -75,16 +73,12 @@ func testMetricDbSizeDefrag(t *testing.T, name string) {
 	require.NoError(t, err)
 	bv, err := strconv.Atoi(beforeDefrag)
 	require.NoError(t, err)
-	if bv < expected {
-		t.Fatalf("expected db size greater than %d, got %d", expected, bv)
-	}
+	require.GreaterOrEqualf(t, bv, expected, "expected db size greater than %d, got %d", expected, bv)
 	beforeDefragInUse, err := clus.Members[0].Metric("etcd_mvcc_db_total_size_in_use_in_bytes")
 	require.NoError(t, err)
 	biu, err := strconv.Atoi(beforeDefragInUse)
 	require.NoError(t, err)
-	if biu < expected {
-		t.Fatalf("expected db size in use is greater than %d, got %d", expected, biu)
-	}
+	require.GreaterOrEqualf(t, biu, expected, "expected db size in use is greater than %d, got %d", expected, biu)
 
 	// clear out historical keys, in use bytes should free pages
 	creq := &pb.CompactionRequest{Revision: int64(numPuts), Physical: true}
@@ -116,9 +110,7 @@ func testMetricDbSizeDefrag(t *testing.T, name string) {
 			break
 		}
 		retry++
-		if retry >= maxRetry {
-			t.Fatalf("%v", err.Error())
-		}
+		require.Lessf(t, retry, maxRetry, "%v", err.Error())
 	}
 
 	// defrag should give freed space back to fs
@@ -128,17 +120,13 @@ func testMetricDbSizeDefrag(t *testing.T, name string) {
 	require.NoError(t, err)
 	av, err := strconv.Atoi(afterDefrag)
 	require.NoError(t, err)
-	if bv <= av {
-		t.Fatalf("expected less than %d, got %d after defrag", bv, av)
-	}
+	require.Greaterf(t, bv, av, "expected less than %d, got %d after defrag", bv, av)
 
 	afterDefragInUse, err := clus.Members[0].Metric("etcd_mvcc_db_total_size_in_use_in_bytes")
 	require.NoError(t, err)
 	adiu, err := strconv.Atoi(afterDefragInUse)
 	require.NoError(t, err)
-	if adiu > av {
-		t.Fatalf("db size in use (%d) is expected less than db size (%d) after defrag", adiu, av)
-	}
+	require.LessOrEqualf(t, adiu, av, "db size in use (%d) is expected less than db size (%d) after defrag", adiu, av)
 }
 
 func TestMetricQuotaBackendBytes(t *testing.T) {
@@ -150,9 +138,7 @@ func TestMetricQuotaBackendBytes(t *testing.T) {
 	require.NoError(t, err)
 	qv, err := strconv.ParseFloat(qs, 64)
 	require.NoError(t, err)
-	if int64(qv) != storage.DefaultQuotaBytes {
-		t.Fatalf("expected %d, got %f", storage.DefaultQuotaBytes, qv)
-	}
+	require.Equalf(t, storage.DefaultQuotaBytes, int64(qv), "expected %d, got %f", storage.DefaultQuotaBytes, qv)
 }
 
 func TestMetricsHealth(t *testing.T) {
@@ -174,9 +160,7 @@ func TestMetricsHealth(t *testing.T) {
 
 	hv, err := clus.Members[0].Metric("etcd_server_health_failures")
 	require.NoError(t, err)
-	if hv != "0" {
-		t.Fatalf("expected '0' from etcd_server_health_failures, got %q", hv)
-	}
+	require.Equalf(t, "0", hv, "expected '0' from etcd_server_health_failures, got %q", hv)
 }
 
 func TestMetricsRangeDurationSeconds(t *testing.T) {
diff --git a/tests/integration/network_partition_test.go b/tests/integration/network_partition_test.go
index 059e93762..8c24fce2f 100644
--- a/tests/integration/network_partition_test.go
+++ b/tests/integration/network_partition_test.go
@@ -19,6 +19,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
+
 	"go.etcd.io/etcd/tests/v3/framework/integration"
 )
 
@@ -65,9 +67,7 @@ func TestNetworkPartition5MembersLeaderInMajority(t *testing.T) {
 		}
 		t.Logf("[%d] got %v", i, err)
 	}
-	if err != nil {
-		t.Fatalf("failed after 3 tries (%v)", err)
-	}
+	require.NoErrorf(t, err, "failed after 3 tries (%v)", err)
 }
 
 func testNetworkPartition5MembersLeaderInMajority(t *testing.T) error {
diff --git a/tests/integration/proxy/grpcproxy/cluster_test.go b/tests/integration/proxy/grpcproxy/cluster_test.go
index ca2fcb506..da0c58624 100644
--- a/tests/integration/proxy/grpcproxy/cluster_test.go
+++ b/tests/integration/proxy/grpcproxy/cluster_test.go
@@ -52,9 +52,7 @@ func TestClusterProxyMemberList(t *testing.T) {
 		DialTimeout: 5 * time.Second,
 	}
 	client, err := integration2.NewClient(t, cfg)
-	if err != nil {
-		t.Fatalf("err %v, want nil", err)
-	}
+	require.NoErrorf(t, err, "err %v, want nil", err)
 	defer client.Close()
 
 	// wait some time for register-loop to write keys
@@ -62,16 +60,10 @@ func TestClusterProxyMemberList(t *testing.T) {
 
 	var mresp *clientv3.MemberListResponse
 	mresp, err = client.Cluster.MemberList(context.Background())
-	if err != nil {
-		t.Fatalf("err %v, want nil", err)
-	}
+	require.NoErrorf(t, err, "err %v, want nil", err)
 
-	if len(mresp.Members) != 1 {
-		t.Fatalf("len(mresp.Members) expected 1, got %d (%+v)", len(mresp.Members), mresp.Members)
-	}
-	if len(mresp.Members[0].ClientURLs) != 1 {
-		t.Fatalf("len(mresp.Members[0].ClientURLs) expected 1, got %d (%+v)", len(mresp.Members[0].ClientURLs), mresp.Members[0].ClientURLs[0])
-	}
+	require.Lenf(t, mresp.Members, 1, "len(mresp.Members) expected 1, got %d (%+v)", len(mresp.Members), mresp.Members)
+	require.Lenf(t, mresp.Members[0].ClientURLs, 1, "len(mresp.Members[0].ClientURLs) expected 1, got %d (%+v)", len(mresp.Members[0].ClientURLs), mresp.Members[0].ClientURLs[0])
 	assert.Contains(t, mresp.Members, &pb.Member{Name: hostname, ClientURLs: []string{cts.caddr}})
 
 	// test proxy member add
@@ -82,12 +74,8 @@ func TestClusterProxyMemberList(t *testing.T) {
 
 	// check add member succ
 	mresp, err = client.Cluster.MemberList(context.Background())
-	if err != nil {
-		t.Fatalf("err %v, want nil", err)
-	}
-	if len(mresp.Members) != 2 {
-		t.Fatalf("len(mresp.Members) expected 2, got %d (%+v)", len(mresp.Members), mresp.Members)
-	}
+	require.NoErrorf(t, err, "err %v, want nil", err)
+	require.Lenf(t, mresp.Members, 2, "len(mresp.Members) expected 2, got %d (%+v)", len(mresp.Members), mresp.Members)
 	assert.Contains(t, mresp.Members, &pb.Member{Name: hostname, ClientURLs: []string{newMemberAddr}})
 
 	// test proxy member delete
@@ -97,12 +85,8 @@ func TestClusterProxyMemberList(t *testing.T) {
 
 	// check delete member succ
 	mresp, err = client.Cluster.MemberList(context.Background())
-	if err != nil {
-		t.Fatalf("err %v, want nil", err)
-	}
-	if len(mresp.Members) != 1 {
-		t.Fatalf("len(mresp.Members) expected 1, got %d (%+v)", len(mresp.Members), mresp.Members)
-	}
+	require.NoErrorf(t, err, "err %v, want nil", err)
+	require.Lenf(t, mresp.Members, 1, "len(mresp.Members) expected 1, got %d (%+v)", len(mresp.Members), mresp.Members)
 	assert.Contains(t, mresp.Members, &pb.Member{Name: hostname, ClientURLs: []string{cts.caddr}})
 }
 
@@ -162,10 +146,7 @@ func newClusterProxyServer(lg *zap.Logger, endpoints []string, prefix string, t
 
 func deregisterMember(c *clientv3.Client, prefix, addr string, t *testing.T) {
 	em, err := endpoints.NewManager(c, prefix)
-	if err != nil {
-		t.Fatalf("new endpoint manager failed, err %v", err)
-	}
-	if err = em.DeleteEndpoint(c.Ctx(), prefix+"/"+addr); err != nil {
-		t.Fatalf("delete endpoint failed, err %v", err)
-	}
+	require.NoErrorf(t, err, "new endpoint manager failed, err")
+	err = em.DeleteEndpoint(c.Ctx(), prefix+"/"+addr)
+	require.NoErrorf(t, err, "delete endpoint failed, err")
 }
diff --git a/tests/integration/proxy/grpcproxy/kv_test.go b/tests/integration/proxy/grpcproxy/kv_test.go
index b871eca95..00bb326ff 100644
--- a/tests/integration/proxy/grpcproxy/kv_test.go
+++ b/tests/integration/proxy/grpcproxy/kv_test.go
@@ -44,13 +44,9 @@ func TestKVProxyRange(t *testing.T) {
 		DialTimeout: 5 * time.Second,
 	}
 	client, err := integration2.NewClient(t, cfg)
-	if err != nil {
-		t.Fatalf("err = %v, want nil", err)
-	}
+	require.NoErrorf(t, err, "err = %v, want nil", err)
 	_, err = client.Get(context.Background(), "foo")
-	if err != nil {
-		t.Fatalf("err = %v, want nil", err)
-	}
+	require.NoErrorf(t, err, "err = %v, want nil", err)
 	client.Close()
 }
 
diff --git a/tests/integration/proxy/grpcproxy/register_test.go b/tests/integration/proxy/grpcproxy/register_test.go
index a0fb5272c..855975360 100644
--- a/tests/integration/proxy/grpcproxy/register_test.go
+++ b/tests/integration/proxy/grpcproxy/register_test.go
@@ -18,6 +18,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/require"
 	"go.uber.org/zap/zaptest"
 
 	clientv3 "go.etcd.io/etcd/client/v3"
@@ -40,12 +41,8 @@ func TestRegister(t *testing.T) {
 	donec := grpcproxy.Register(zaptest.NewLogger(t), cli, testPrefix, paddr, 5)
 
 	ups := <-wa
-	if len(ups) != 1 {
-		t.Fatalf("len(ups) expected 1, got %d (%v)", len(ups), ups)
-	}
-	if ups[0].Endpoint.Addr != paddr {
-		t.Fatalf("ups[0].Addr expected %q, got %q", paddr, ups[0].Endpoint.Addr)
-	}
+	require.Lenf(t, ups, 1, "len(ups) expected 1, got %d (%v)", len(ups), ups)
+	require.Equalf(t, ups[0].Endpoint.Addr, paddr, "ups[0].Addr expected %q, got %q", paddr, ups[0].Endpoint.Addr)
 
 	cli.Close()
 	clus.TakeClient(0)
@@ -58,12 +55,8 @@ func TestRegister(t *testing.T) {
 
 func mustCreateWatcher(t *testing.T, c *clientv3.Client, prefix string) endpoints.WatchChannel {
 	em, err := endpoints.NewManager(c, prefix)
-	if err != nil {
-		t.Fatalf("failed to create endpoints.Manager: %v", err)
-	}
+	require.NoErrorf(t, err, "failed to create endpoints.Manager")
 	wc, err := em.NewWatchChannel(c.Ctx())
-	if err != nil {
-		t.Fatalf("failed to resolve %q (%v)", prefix, err)
-	}
+	require.NoErrorf(t, err, "failed to resolve %q", prefix)
 	return wc
 }
diff --git a/tests/integration/revision_test.go b/tests/integration/revision_test.go
index fd53cfb37..49ce4d214 100644
--- a/tests/integration/revision_test.go
+++ b/tests/integration/revision_test.go
@@ -132,9 +132,7 @@ func getWorker(ctx context.Context, t *testing.T, clus *integration.Cluster) {
 		if resp == nil {
 			continue
 		}
-		if prevRev > resp.Header.Revision {
-			t.Fatalf("rev is less than previously observed revision, rev: %d, prevRev: %d", resp.Header.Revision, prevRev)
-		}
+		require.LessOrEqualf(t, prevRev, resp.Header.Revision, "rev is less than previously observed revision, rev: %d, prevRev: %d", resp.Header.Revision, prevRev)
 		prevRev = resp.Header.Revision
 	}
 }
diff --git a/tests/integration/snapshot/member_test.go b/tests/integration/snapshot/member_test.go
index be419efdc..1c889df75 100644
--- a/tests/integration/snapshot/member_test.go
+++ b/tests/integration/snapshot/member_test.go
@@ -93,9 +93,7 @@ func TestSnapshotV3RestoreMultiMemberAdd(t *testing.T) {
 	mresp, err := cli2.MemberList(ctx)
 	cancel()
 	require.NoError(t, err)
-	if len(mresp.Members) != 4 {
-		t.Fatalf("expected 4 members, got %+v", mresp)
-	}
+	require.Lenf(t, mresp.Members, 4, "expected 4 members, got %+v", mresp)
 
 	// make sure restored cluster has kept all data on recovery
 	var gresp *clientv3.GetResponse
@@ -104,11 +102,7 @@ func TestSnapshotV3RestoreMultiMemberAdd(t *testing.T) {
 	cancel()
 	require.NoError(t, err)
 	for i := range gresp.Kvs {
-		if string(gresp.Kvs[i].Key) != kvs[i].k {
-			t.Fatalf("#%d: key expected %s, got %s", i, kvs[i].k, gresp.Kvs[i].Key)
-		}
-		if string(gresp.Kvs[i].Value) != kvs[i].v {
-			t.Fatalf("#%d: value expected %s, got %s", i, kvs[i].v, gresp.Kvs[i].Value)
-		}
+		require.Equalf(t, string(gresp.Kvs[i].Key), kvs[i].k, "#%d: key expected %s, got %s", i, kvs[i].k, gresp.Kvs[i].Key)
+		require.Equalf(t, string(gresp.Kvs[i].Value), kvs[i].v, "#%d: value expected %s, got %s", i, kvs[i].v, gresp.Kvs[i].Value)
 	}
 }
diff --git a/tests/integration/snapshot/v3_snapshot_test.go b/tests/integration/snapshot/v3_snapshot_test.go
index 893fa4fd0..d016d3bce 100644
--- a/tests/integration/snapshot/v3_snapshot_test.go
+++ b/tests/integration/snapshot/v3_snapshot_test.go
@@ -88,9 +88,7 @@ func TestSnapshotV3RestoreSingle(t *testing.T) {
 		var gresp *clientv3.GetResponse
 		gresp, err = cli.Get(context.Background(), kvs[i].k)
 		require.NoError(t, err)
-		if string(gresp.Kvs[0].Value) != kvs[i].v {
-			t.Fatalf("#%d: value expected %s, got %s", i, kvs[i].v, gresp.Kvs[0].Value)
-		}
+		require.Equalf(t, string(gresp.Kvs[0].Value), kvs[i].v, "#%d: value expected %s, got %s", i, kvs[i].v, gresp.Kvs[0].Value)
 	}
 }
 
@@ -121,9 +119,7 @@ func TestSnapshotV3RestoreMulti(t *testing.T) {
 			var gresp *clientv3.GetResponse
 			gresp, err = cli.Get(context.Background(), kvs[i].k)
 			require.NoError(t, err)
-			if string(gresp.Kvs[0].Value) != kvs[i].v {
-				t.Fatalf("#%d: value expected %s, got %s", i, kvs[i].v, gresp.Kvs[0].Value)
-			}
+			require.Equalf(t, string(gresp.Kvs[0].Value), kvs[i].v, "#%d: value expected %s, got %s", i, kvs[i].v, gresp.Kvs[0].Value)
 		}
 	}
 }
@@ -132,12 +128,11 @@ func TestSnapshotV3RestoreMulti(t *testing.T) {
 func TestCorruptedBackupFileCheck(t *testing.T) {
 	dbPath := testutils.MustAbsPath("testdata/corrupted_backup.db")
 	integration2.BeforeTest(t)
-	if _, err := os.Stat(dbPath); err != nil {
-		t.Fatalf("test file [%s] does not exist: %v", dbPath, err)
-	}
+	_, err := os.Stat(dbPath)
+	require.NoErrorf(t, err, "test file [%s] does not exist: %v", dbPath, err)
 
 	sp := snapshot.NewV3(zaptest.NewLogger(t))
-	_, err := sp.Status(dbPath)
+	_, err = sp.Status(dbPath)
 	expectedErrKeywords := "snapshot file integrity check failed"
 	/* example error message:
 	snapshot file integrity check failed. 2 errors found.
diff --git a/tests/integration/v2store/store_test.go b/tests/integration/v2store/store_test.go
index 842b57deb..73f8fae83 100644
--- a/tests/integration/v2store/store_test.go
+++ b/tests/integration/v2store/store_test.go
@@ -101,7 +101,7 @@ func TestSet(t *testing.T) {
 	assert.Equal(t, "set", e.Action)
 	assert.Equal(t, "/foo", e.Node.Key)
 	assert.False(t, e.Node.Dir)
-	assert.Equal(t, "", *e.Node.Value)
+	assert.Empty(t, *e.Node.Value)
 	assert.Nil(t, e.Node.Nodes)
 	assert.Nil(t, e.Node.Expiration)
 	assert.Equal(t, int64(0), e.Node.TTL)
@@ -123,7 +123,7 @@ func TestSet(t *testing.T) {
 	// check prevNode
 	require.NotNil(t, e.PrevNode)
 	assert.Equal(t, "/foo", e.PrevNode.Key)
-	assert.Equal(t, "", *e.PrevNode.Value)
+	assert.Empty(t, *e.PrevNode.Value)
 	assert.Equal(t, uint64(1), e.PrevNode.ModifiedIndex)
 	// Set /foo="baz" (for testing prevNode)
 	eidx = 3
@@ -198,7 +198,7 @@ func TestStoreCreateValue(t *testing.T) {
 	assert.Equal(t, "create", e.Action)
 	assert.Equal(t, "/empty", e.Node.Key)
 	assert.False(t, e.Node.Dir)
-	assert.Equal(t, "", *e.Node.Value)
+	assert.Empty(t, *e.Node.Value)
 	assert.Nil(t, e.Node.Nodes)
 	assert.Nil(t, e.Node.Expiration)
 	assert.Equal(t, int64(0), e.Node.TTL)
@@ -271,7 +271,7 @@ func TestStoreUpdateValue(t *testing.T) {
 	assert.Equal(t, "update", e.Action)
 	assert.Equal(t, "/foo", e.Node.Key)
 	assert.False(t, e.Node.Dir)
-	assert.Equal(t, "", *e.Node.Value)
+	assert.Empty(t, *e.Node.Value)
 	assert.Equal(t, int64(0), e.Node.TTL)
 	assert.Equal(t, uint64(3), e.Node.ModifiedIndex)
 	// check prevNode
@@ -282,7 +282,7 @@ func TestStoreUpdateValue(t *testing.T) {
 
 	e, _ = s.Get("/foo", false, false)
 	assert.Equal(t, eidx, e.EtcdIndex)
-	assert.Equal(t, "", *e.Node.Value)
+	assert.Empty(t, *e.Node.Value)
 }
 
 // TestStoreUpdateFailsIfDirectory ensures that the store cannot update a directory.
diff --git a/tests/integration/v3_alarm_test.go b/tests/integration/v3_alarm_test.go
index 0f723b8f2..9c1f7f152 100644
--- a/tests/integration/v3_alarm_test.go
+++ b/tests/integration/v3_alarm_test.go
@@ -110,23 +110,20 @@ func TestV3StorageQuotaApply(t *testing.T) {
 	defer cancel()
 
 	// small quota machine should reject put
-	if _, err := kvc0.Put(ctx, &pb.PutRequest{Key: key, Value: smallbuf}); err == nil {
-		t.Fatalf("past-quota instance should reject put")
-	}
+	_, err = kvc0.Put(ctx, &pb.PutRequest{Key: key, Value: smallbuf})
+	require.Errorf(t, err, "past-quota instance should reject put")
 
 	// large quota machine should reject put
-	if _, err := kvc1.Put(ctx, &pb.PutRequest{Key: key, Value: smallbuf}); err == nil {
-		t.Fatalf("past-quota instance should reject put")
-	}
+	_, err = kvc1.Put(ctx, &pb.PutRequest{Key: key, Value: smallbuf})
+	require.Errorf(t, err, "past-quota instance should reject put")
 
 	// reset large quota node to ensure alarm persisted
 	clus.Members[1].Stop(t)
 	clus.Members[1].Restart(t)
 	clus.WaitMembersForLeader(t, clus.Members)
 
-	if _, err := kvc1.Put(context.TODO(), &pb.PutRequest{Key: key, Value: smallbuf}); err == nil {
-		t.Fatalf("alarmed instance should reject put after reset")
-	}
+	_, err = kvc1.Put(context.TODO(), &pb.PutRequest{Key: key, Value: smallbuf})
+	require.Errorf(t, err, "alarmed instance should reject put after reset")
 }
 
 // TestV3AlarmDeactivate ensures that space alarms can be deactivated so puts go through.
@@ -221,9 +218,7 @@ func TestV3CorruptAlarm(t *testing.T) {
 	resp1, err1 := clus.Client(1).Get(context.TODO(), "abc")
 	require.NoError(t, err1)
 
-	if resp0.Kvs[0].ModRevision == resp1.Kvs[0].ModRevision {
-		t.Fatalf("matching ModRevision values")
-	}
+	require.NotEqualf(t, resp0.Kvs[0].ModRevision, resp1.Kvs[0].ModRevision, "matching ModRevision values")
 
 	for i := 0; i < 5; i++ {
 		presp, perr := clus.Client(0).Put(context.TODO(), "abc", "aaa")
@@ -307,9 +302,7 @@ func TestV3CorruptAlarmWithLeaseCorrupted(t *testing.T) {
 	resp1, err1 := clus.Members[2].Client.KV.Get(context.TODO(), "foo")
 	require.NoError(t, err1)
 
-	if resp0.Header.Revision == resp1.Header.Revision {
-		t.Fatalf("matching Revision values")
-	}
+	require.NotEqualf(t, resp0.Header.Revision, resp1.Header.Revision, "matching Revision values")
 
 	// Wait for CorruptCheckTime
 	time.Sleep(time.Second)
diff --git a/tests/integration/v3_auth_test.go b/tests/integration/v3_auth_test.go
index 8d44d5473..abd6c5e41 100644
--- a/tests/integration/v3_auth_test.go
+++ b/tests/integration/v3_auth_test.go
@@ -44,9 +44,7 @@ func TestV3AuthEmptyUserGet(t *testing.T) {
 	authSetupRoot(t, api.Auth)
 
 	_, err := api.KV.Range(ctx, &pb.RangeRequest{Key: []byte("abc")})
-	if !eqErrGRPC(err, rpctypes.ErrUserEmpty) {
-		t.Fatalf("got %v, expected %v", err, rpctypes.ErrUserEmpty)
-	}
+	require.Truef(t, eqErrGRPC(err, rpctypes.ErrUserEmpty), "got %v, expected %v", err, rpctypes.ErrUserEmpty)
 }
 
 // TestV3AuthEmptyUserPut ensures that a put with an empty user will return an empty user error,
@@ -70,9 +68,7 @@ func TestV3AuthEmptyUserPut(t *testing.T) {
 	// cluster terminating.
 	for i := 0; i < 10; i++ {
 		_, err := api.KV.Put(ctx, &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
-		if !eqErrGRPC(err, rpctypes.ErrUserEmpty) {
-			t.Fatalf("got %v, expected %v", err, rpctypes.ErrUserEmpty)
-		}
+		require.Truef(t, eqErrGRPC(err, rpctypes.ErrUserEmpty), "got %v, expected %v", err, rpctypes.ErrUserEmpty)
 	}
 }
 
@@ -124,9 +120,7 @@ func TestV3AuthRevision(t *testing.T) {
 	aresp, aerr := api.Auth.UserAdd(ctx, &pb.AuthUserAddRequest{Name: "root", Password: "123", Options: &authpb.UserAddOptions{NoPassword: false}})
 	cancel()
 	require.NoError(t, aerr)
-	if aresp.Header.Revision != rev {
-		t.Fatalf("revision expected %d, got %d", rev, aresp.Header.Revision)
-	}
+	require.Equalf(t, aresp.Header.Revision, rev, "revision expected %d, got %d", rev, aresp.Header.Revision)
 }
 
 // TestV3AuthWithLeaseRevokeWithRoot ensures that granted leases
@@ -333,16 +327,12 @@ func TestV3AuthNonAuthorizedRPCs(t *testing.T) {
 	key := "foo"
 	val := "bar"
 	_, err := nonAuthedKV.Put(context.TODO(), key, val)
-	if err != nil {
-		t.Fatalf("couldn't put key (%v)", err)
-	}
+	require.NoErrorf(t, err, "couldn't put key (%v)", err)
 
 	authSetupRoot(t, integration.ToGRPC(clus.Client(0)).Auth)
 
 	respput, err := nonAuthedKV.Put(context.TODO(), key, val)
-	if !eqErrGRPC(err, rpctypes.ErrGRPCUserEmpty) {
-		t.Fatalf("could put key (%v), it should cause an error of permission denied", respput)
-	}
+	require.Truef(t, eqErrGRPC(err, rpctypes.ErrGRPCUserEmpty), "could put key (%v), it should cause an error of permission denied", respput)
 }
 
 func TestV3AuthOldRevConcurrent(t *testing.T) {
@@ -428,9 +418,7 @@ func TestV3AuthWatchErrorAndWatchId0(t *testing.T) {
 	require.Error(t, watchResponse.Err()) // permission denied
 
 	_, err := c.Put(ctx, "k1", "val")
-	if err != nil {
-		t.Fatalf("Unexpected error from Put: %v", err)
-	}
+	require.NoErrorf(t, err, "Unexpected error from Put: %v", err)
 
 	<-watchEndCh
 }
diff --git a/tests/integration/v3_election_test.go b/tests/integration/v3_election_test.go
index 94665dca6..d1d7e48d2 100644
--- a/tests/integration/v3_election_test.go
+++ b/tests/integration/v3_election_test.go
@@ -132,19 +132,14 @@ func TestElectionFailover(t *testing.T) {
 
 	// first leader (elected)
 	e := concurrency.NewElection(ss[0], "test-election")
-	if err := e.Campaign(context.TODO(), "foo"); err != nil {
-		t.Fatalf("failed volunteer (%v)", err)
-	}
+	err := e.Campaign(context.TODO(), "foo")
+	require.NoErrorf(t, err, "failed volunteer")
 
 	// check first leader
 	resp, ok := <-e.Observe(cctx)
-	if !ok {
-		t.Fatalf("could not wait for first election; channel closed")
-	}
+	require.Truef(t, ok, "could not wait for first election; channel closed")
 	s := string(resp.Kvs[0].Value)
-	if s != "foo" {
-		t.Fatalf("wrong election result. got %s, wanted foo", s)
-	}
+	require.Equalf(t, "foo", s, "wrong election result. got %s, wanted foo", s)
 
 	// next leader
 	electedErrC := make(chan error, 1)
@@ -155,19 +150,15 @@ func TestElectionFailover(t *testing.T) {
 	}()
 
 	// invoke leader failover
-	err := ss[0].Close()
+	err = ss[0].Close()
 	require.NoError(t, err)
 
 	// check new leader
 	e = concurrency.NewElection(ss[2], "test-election")
 	resp, ok = <-e.Observe(cctx)
-	if !ok {
-		t.Fatalf("could not wait for second election; channel closed")
-	}
+	require.Truef(t, ok, "could not wait for second election; channel closed")
 	s = string(resp.Kvs[0].Value)
-	if s != "bar" {
-		t.Fatalf("wrong election result. got %s, wanted bar", s)
-	}
+	require.Equalf(t, "bar", s, "wrong election result. got %s, wanted bar", s)
 
 	// leader must ack election (otherwise, Campaign may see closed conn)
 	eer := <-electedErrC
@@ -288,9 +279,7 @@ func TestElectionObserveCompacted(t *testing.T) {
 	if !ok {
 		t.Fatal("failed to observe on compacted revision")
 	}
-	if string(v.Kvs[0].Value) != "abc" {
-		t.Fatalf(`expected leader value "abc", got %q`, string(v.Kvs[0].Value))
-	}
+	require.Equalf(t, "abc", string(v.Kvs[0].Value), `expected leader value "abc", got %q`, string(v.Kvs[0].Value))
 }
 
 // TestElectionWithAuthEnabled verifies the election interface when auth is enabled.
diff --git a/tests/integration/v3_failover_test.go b/tests/integration/v3_failover_test.go
index f9d71ffda..03b829b74 100644
--- a/tests/integration/v3_failover_test.go
+++ b/tests/integration/v3_failover_test.go
@@ -61,9 +61,7 @@ func TestFailover(t *testing.T) {
 			// Create an etcd client before or after first server down
 			t.Logf("Creating an etcd client [%s]", tc.name)
 			cli, err := tc.testFunc(t, cc, clus)
-			if err != nil {
-				t.Fatalf("Failed to create client: %v", err)
-			}
+			require.NoErrorf(t, err, "Failed to create client")
 			defer cli.Close()
 
 			// Sanity test
@@ -142,12 +140,8 @@ func getWithRetries(t *testing.T, cli *clientv3.Client, key, val string, retryCo
 				t.Logf("Failed to get key (%v)", getErr)
 				return getErr
 			}
-			if len(resp.Kvs) != 1 {
-				t.Fatalf("Expected 1 key, got %d", len(resp.Kvs))
-			}
-			if !bytes.Equal([]byte(val), resp.Kvs[0].Value) {
-				t.Fatalf("Unexpected value, expected: %s, got: %s", val, resp.Kvs[0].Value)
-			}
+			require.Lenf(t, resp.Kvs, 1, "Expected 1 key, got %d", len(resp.Kvs))
+			require.Truef(t, bytes.Equal([]byte(val), resp.Kvs[0].Value), "Unexpected value, expected: %s, got: %s", val, resp.Kvs[0].Value)
 			return nil
 		}()
 		if err != nil {
diff --git a/tests/integration/v3_grpc_test.go b/tests/integration/v3_grpc_test.go
index fdf4d14b7..f7c331a55 100644
--- a/tests/integration/v3_grpc_test.go
+++ b/tests/integration/v3_grpc_test.go
@@ -52,32 +52,22 @@ func TestV3PutOverwrite(t *testing.T) {
 	reqput := &pb.PutRequest{Key: key, Value: []byte("bar"), PrevKv: true}
 
 	respput, err := kvc.Put(context.TODO(), reqput)
-	if err != nil {
-		t.Fatalf("couldn't put key (%v)", err)
-	}
+	require.NoErrorf(t, err, "couldn't put key")
 
 	// overwrite
 	reqput.Value = []byte("baz")
 	respput2, err := kvc.Put(context.TODO(), reqput)
-	if err != nil {
-		t.Fatalf("couldn't put key (%v)", err)
-	}
-	if respput2.Header.Revision <= respput.Header.Revision {
-		t.Fatalf("expected newer revision on overwrite, got %v <= %v",
-			respput2.Header.Revision, respput.Header.Revision)
-	}
+	require.NoErrorf(t, err, "couldn't put key")
+	require.Greaterf(t, respput2.Header.Revision, respput.Header.Revision, "expected newer revision on overwrite, got %v <= %v",
+		respput2.Header.Revision, respput.Header.Revision)
 	if pkv := respput2.PrevKv; pkv == nil || string(pkv.Value) != "bar" {
 		t.Fatalf("expected PrevKv=bar, got response %+v", respput2)
 	}
 
 	reqrange := &pb.RangeRequest{Key: key}
 	resprange, err := kvc.Range(context.TODO(), reqrange)
-	if err != nil {
-		t.Fatalf("couldn't get key (%v)", err)
-	}
-	if len(resprange.Kvs) != 1 {
-		t.Fatalf("expected 1 key, got %v", len(resprange.Kvs))
-	}
+	require.NoErrorf(t, err, "couldn't get key")
+	require.Lenf(t, resprange.Kvs, 1, "expected 1 key, got %v", len(resprange.Kvs))
 
 	kv := resprange.Kvs[0]
 	if kv.ModRevision <= kv.CreateRevision {
@@ -107,9 +97,7 @@ func TestV3PutRestart(t *testing.T) {
 	clus.Members[stopIdx].Stop(t)
 	clus.Members[stopIdx].Restart(t)
 	c, cerr := integration.NewClientV3(clus.Members[stopIdx])
-	if cerr != nil {
-		t.Fatalf("cannot create client: %v", cerr)
-	}
+	require.NoErrorf(t, cerr, "cannot create client")
 	clus.Members[stopIdx].ServerClient = c
 
 	ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second)
@@ -130,28 +118,21 @@ func TestV3CompactCurrentRev(t *testing.T) {
 	kvc := integration.ToGRPC(clus.RandClient()).KV
 	preq := &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")}
 	for i := 0; i < 3; i++ {
-		if _, err := kvc.Put(context.Background(), preq); err != nil {
-			t.Fatalf("couldn't put key (%v)", err)
-		}
+		_, err := kvc.Put(context.Background(), preq)
+		require.NoErrorf(t, err, "couldn't put key")
 	}
 	// get key to add to proxy cache, if any
 	_, err := kvc.Range(context.TODO(), &pb.RangeRequest{Key: []byte("foo")})
 	require.NoError(t, err)
 	// compact on current revision
 	_, err = kvc.Compact(context.Background(), &pb.CompactionRequest{Revision: 4})
-	if err != nil {
-		t.Fatalf("couldn't compact kv space (%v)", err)
-	}
+	require.NoErrorf(t, err, "couldn't compact kv space")
 	// key still exists when linearized?
 	_, err = kvc.Range(context.Background(), &pb.RangeRequest{Key: []byte("foo")})
-	if err != nil {
-		t.Fatalf("couldn't get key after compaction (%v)", err)
-	}
+	require.NoErrorf(t, err, "couldn't get key after compaction")
 	// key still exists when serialized?
 	_, err = kvc.Range(context.Background(), &pb.RangeRequest{Key: []byte("foo"), Serializable: true})
-	if err != nil {
-		t.Fatalf("couldn't get serialized key after compaction (%v)", err)
-	}
+	require.NoErrorf(t, err, "couldn't get serialized key after compaction")
 }
 
 // TestV3HashKV ensures that multiple calls of HashKV on same node return same hash and compact rev.
diff --git a/tests/integration/v3_leadership_test.go b/tests/integration/v3_leadership_test.go
index ea2b730c6..148c2edf8 100644
--- a/tests/integration/v3_leadership_test.go
+++ b/tests/integration/v3_leadership_test.go
@@ -236,14 +236,14 @@ func TestFirstCommitNotification(t *testing.T) {
 
 func checkFirstCommitNotification(
 	ctx context.Context,
-	t testing.TB,
+	tb testing.TB,
 	member *integration.Member,
 	leaderAppliedIndex uint64,
 	notifier <-chan struct{},
 ) error {
 	// wait until server applies all the changes of leader
 	for member.Server.AppliedIndex() < leaderAppliedIndex {
-		t.Logf("member.Server.AppliedIndex():%v <= leaderAppliedIndex:%v", member.Server.AppliedIndex(), leaderAppliedIndex)
+		tb.Logf("member.Server.AppliedIndex():%v <= leaderAppliedIndex:%v", member.Server.AppliedIndex(), leaderAppliedIndex)
 		select {
 		case <-ctx.Done():
 			return ctx.Err()
@@ -261,7 +261,7 @@ func checkFirstCommitNotification(
 			)
 		}
 	default:
-		t.Logf("member.Server.AppliedIndex():%v >= leaderAppliedIndex:%v", member.Server.AppliedIndex(), leaderAppliedIndex)
+		tb.Logf("member.Server.AppliedIndex():%v >= leaderAppliedIndex:%v", member.Server.AppliedIndex(), leaderAppliedIndex)
 		return fmt.Errorf(
 			"notification was not triggered, member ID: %d",
 			member.ID(),
diff --git a/tests/robustness/makefile.mk b/tests/robustness/Makefile
similarity index 82%
rename from tests/robustness/makefile.mk
rename to tests/robustness/Makefile
index c5a1dff14..787b7c8cf 100644
--- a/tests/robustness/makefile.mk
+++ b/tests/robustness/Makefile
@@ -1,68 +1,76 @@
+REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel)
+
 .PHONY: test-robustness-reports
-test-robustness-reports: export GOTOOLCHAIN := go$(shell cat .go-version)
+test-robustness-reports: export GOTOOLCHAIN := go$(shell cat $(REPOSITORY_ROOT)/.go-version)
 test-robustness-reports:
-	cd ./tests && go test ./robustness/validate -v --count 1 --run TestDataReports
+	cd $(REPOSITORY_ROOT)/tests && go test ./robustness/validate -v --count 1 --run TestDataReports
 
 # Test main and previous release branches
 
+# Note that executing make at the top-level repository needs a change in the
+# directory. So, instead of calling just $(MAKE) or make, use this
+# $(TOPLEVEL_MAKE) variable.
+TOPLEVEL_MAKE := $(MAKE) --directory=$(REPOSITORY_ROOT)
+
 .PHONY: test-robustness-main
 test-robustness-main: /tmp/etcd-main-failpoints/bin /tmp/etcd-release-3.5-failpoints/bin
-	GO_TEST_FLAGS="$${GO_TEST_FLAGS} --bin-dir=/tmp/etcd-main-failpoints/bin --bin-last-release=/tmp/etcd-release-3.5-failpoints/bin/etcd" $(MAKE) test-robustness
+	GO_TEST_FLAGS="$${GO_TEST_FLAGS} --bin-dir=/tmp/etcd-main-failpoints/bin --bin-last-release=/tmp/etcd-release-3.5-failpoints/bin/etcd" $(TOPLEVEL_MAKE) test-robustness
 
 .PHONY: test-robustness-release-3.5
 test-robustness-release-3.5: /tmp/etcd-release-3.5-failpoints/bin /tmp/etcd-release-3.4-failpoints/bin
-	GO_TEST_FLAGS="$${GO_TEST_FLAGS} --bin-dir=/tmp/etcd-release-3.5-failpoints/bin --bin-last-release=/tmp/etcd-release-3.4-failpoints/bin/etcd" $(MAKE) test-robustness
+	GO_TEST_FLAGS="$${GO_TEST_FLAGS} --bin-dir=/tmp/etcd-release-3.5-failpoints/bin --bin-last-release=/tmp/etcd-release-3.4-failpoints/bin/etcd" $(TOPLEVEL_MAKE) test-robustness
 
 .PHONY: test-robustness-release-3.4
 test-robustness-release-3.4: /tmp/etcd-release-3.4-failpoints/bin
-	GO_TEST_FLAGS="$${GO_TEST_FLAGS} --bin-dir=/tmp/etcd-release-3.4-failpoints/bin" $(MAKE) test-robustness
+	GO_TEST_FLAGS="$${GO_TEST_FLAGS} --bin-dir=/tmp/etcd-release-3.4-failpoints/bin" $(TOPLEVEL_MAKE) test-robustness
 
 # Reproduce historical issues
 
 .PHONY: test-robustness-issue14370
 test-robustness-issue14370: /tmp/etcd-v3.5.4-failpoints/bin
-	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue14370 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.4-failpoints/bin' $(MAKE) test-robustness && \
+	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue14370 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.4-failpoints/bin' $(TOPLEVEL_MAKE) test-robustness && \
 	 echo "Failed to reproduce" || echo "Successful reproduction"
 
 .PHONY: test-robustness-issue13766
 test-robustness-issue13766: /tmp/etcd-v3.5.2-failpoints/bin
-	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue13766 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.2-failpoints/bin' $(MAKE) test-robustness && \
+	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue13766 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.2-failpoints/bin' $(TOPLEVEL_MAKE) test-robustness && \
 	 echo "Failed to reproduce" || echo "Successful reproduction"
 
 .PHONY: test-robustness-issue14685
 test-robustness-issue14685: /tmp/etcd-v3.5.5-failpoints/bin
-	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue14685 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.5-failpoints/bin' $(MAKE) test-robustness && \
+	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue14685 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.5-failpoints/bin' $(TOPLEVEL_MAKE) test-robustness && \
 	 echo "Failed to reproduce" || echo "Successful reproduction"
 
 .PHONY: test-robustness-issue15271
 test-robustness-issue15271: /tmp/etcd-v3.5.7-failpoints/bin
-	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue15271 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.7-failpoints/bin' $(MAKE) test-robustness && \
+	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue15271 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.7-failpoints/bin' $(TOPLEVEL_MAKE) test-robustness && \
 	 echo "Failed to reproduce" || echo "Successful reproduction"
 
 .PHONY: test-robustness-issue17529
 test-robustness-issue17529: /tmp/etcd-v3.5.12-beforeSendWatchResponse/bin
-	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue17529 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.12-beforeSendWatchResponse/bin' $(MAKE) test-robustness && \
+	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue17529 --count 100 --failfast --bin-dir=/tmp/etcd-v3.5.12-beforeSendWatchResponse/bin' $(TOPLEVEL_MAKE) test-robustness && \
 	 echo "Failed to reproduce" || echo "Successful reproduction"
 
 .PHONY: test-robustness-issue17780
 test-robustness-issue17780: /tmp/etcd-v3.5.13-compactBeforeSetFinishedCompact/bin
-	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue17780 --count 200 --failfast --bin-dir=/tmp/etcd-v3.5.13-compactBeforeSetFinishedCompact/bin' make test-robustness && \
+	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue17780 --count 200 --failfast --bin-dir=/tmp/etcd-v3.5.13-compactBeforeSetFinishedCompact/bin' $(TOPLEVEL_MAKE) test-robustness && \
 	  echo "Failed to reproduce" || echo "Successful reproduction"
 
 .PHONY: test-robustness-issue18089
 test-robustness-issue18089: /tmp/etcd-v3.5.12-beforeSendWatchResponse/bin
-	GO_TEST_FLAGS='-v -run=TestRobustnessRegression/Issue18089 -count 100 -failfast --bin-dir=/tmp/etcd-v3.5.12-beforeSendWatchResponse/bin' make test-robustness && \
+	GO_TEST_FLAGS='-v -run=TestRobustnessRegression/Issue18089 -count 100 -failfast --bin-dir=/tmp/etcd-v3.5.12-beforeSendWatchResponse/bin' $(TOPLEVEL_MAKE) test-robustness && \
 	  echo "Failed to reproduce" || echo "Successful reproduction"
 
 .PHONY: test-robustness-issue19179
 test-robustness-issue19179: /tmp/etcd-v3.5.17-failpoints/bin
-	GO_TEST_FLAGS='-v -run=TestRobustnessRegression/Issue19179 -count 200 -failfast --bin-dir=/tmp/etcd-v3.5.17-failpoints/bin' make test-robustness && \
+	GO_TEST_FLAGS='-v -run=TestRobustnessRegression/Issue19179 -count 200 -failfast --bin-dir=/tmp/etcd-v3.5.17-failpoints/bin' $(TOPLEVEL_MAKE) test-robustness && \
 	  echo "Failed to reproduce" || echo "Successful reproduction"
 
+
 # Failpoints
 
 GOPATH = $(shell go env GOPATH)
-GOFAIL_VERSION = $(shell cd tools/mod && go list -m -f {{.Version}} go.etcd.io/gofail)
+GOFAIL_VERSION = $(shell cd $(REPOSITORY_ROOT)/tools/mod && go list -m -f {{.Version}} go.etcd.io/gofail)
 
 .PHONY:install-gofail
 install-gofail: $(GOPATH)/bin/gofail
@@ -70,20 +78,20 @@ install-gofail: $(GOPATH)/bin/gofail
 .PHONY: gofail-enable
 gofail-enable: $(GOPATH)/bin/gofail
 	$(GOPATH)/bin/gofail enable server/etcdserver/ server/lease/leasehttp server/storage/backend/ server/storage/mvcc/ server/storage/wal/ server/etcdserver/api/v3rpc/ server/etcdserver/api/membership/
-	cd ./server && go get go.etcd.io/gofail@${GOFAIL_VERSION}
-	cd ./etcdutl && go get go.etcd.io/gofail@${GOFAIL_VERSION}
-	cd ./etcdctl && go get go.etcd.io/gofail@${GOFAIL_VERSION}
-	cd ./tests && go get go.etcd.io/gofail@${GOFAIL_VERSION}
+	cd $(REPOSITORY_ROOT)/server && go get go.etcd.io/gofail@${GOFAIL_VERSION}
+	cd $(REPOSITORY_ROOT)/etcdutl && go get go.etcd.io/gofail@${GOFAIL_VERSION}
+	cd $(REPOSITORY_ROOT)/etcdctl && go get go.etcd.io/gofail@${GOFAIL_VERSION}
+	cd $(REPOSITORY_ROOT)/tests && go get go.etcd.io/gofail@${GOFAIL_VERSION}
 
 .PHONY: gofail-disable
 gofail-disable: $(GOPATH)/bin/gofail
 	$(GOPATH)/bin/gofail disable server/etcdserver/ server/lease/leasehttp server/storage/backend/ server/storage/mvcc/ server/storage/wal/ server/etcdserver/api/v3rpc/ server/etcdserver/api/membership/
-	cd ./server && go mod tidy
-	cd ./etcdutl && go mod tidy
-	cd ./etcdctl && go mod tidy
-	cd ./tests && go mod tidy
+	cd $(REPOSITORY_ROOT)/server && go mod tidy
+	cd $(REPOSITORY_ROOT)/etcdutl && go mod tidy
+	cd $(REPOSITORY_ROOT)/etcdctl && go mod tidy
+	cd $(REPOSITORY_ROOT)/tests && go mod tidy
 
-$(GOPATH)/bin/gofail: tools/mod/go.mod tools/mod/go.sum
+$(GOPATH)/bin/gofail: $(REPOSITORY_ROOT)/tools/mod/go.mod $(REPOSITORY_ROOT)/tools/mod/go.sum
 	go install go.etcd.io/gofail@${GOFAIL_VERSION}
 
 # Build main and previous releases for robustness tests
diff --git a/tests/robustness/README.md b/tests/robustness/README.md
index e47e61d15..b1ea6bae6 100644
--- a/tests/robustness/README.md
+++ b/tests/robustness/README.md
@@ -39,7 +39,7 @@ The purpose of these tests is to rigorously validate that etcd maintains its [KV
 
 ## How Robustness Tests Work
 
-Robustness tests compare etcd cluster behavior against a simplified model of its expected behavior.
+Robustness tests compare the etcd cluster behavior against a simplified model of its expected behavior.
 These tests cover various scenarios, including:
 
 * **Different etcd cluster setups:** Cluster sizes, configurations, and deployment topologies.
@@ -52,8 +52,8 @@ These tests cover various scenarios, including:
 2. **Traffic and Failures:** Client traffic is generated and sent to the cluster while failures are injected.
 3. **History Collection:** All client operations and their results are recorded.
 4. **Validation:** The collected history is validated against the etcd model and a set of validators to ensure consistency and correctness.
-5. **Report Generation:**  If a failure is detected and a detailed report is generated to help diagnose the issue.
-   This report includes information about the client operations, etcd data directories.
+5. **Report Generation:**  If a failure is detected then a detailed report is generated to help diagnose the issue.
+   This report includes information about the client operations and etcd data directories.
 
 ## Key Concepts
 
@@ -96,26 +96,25 @@ Etcd provides strict serializability for KV operations and eventual consistency
     make gofail-disable
     ```
 2. Run the tests
-
     ```bash
     make test-robustness
     ```
    
-    Optionally you can pass environment variables:
+    Optionally, you can pass environment variables:
     * `GO_TEST_FLAGS` - to pass additional arguments to `go test`. 
       It is recommended to run tests multiple times with failfast enabled. this can be done by setting `GO_TEST_FLAGS='--count=100 --failfast'`.
     * `EXPECT_DEBUG=true` - to get logs from the cluster.
-    * `RESULTS_DIR` - to change location where results report will be saved.
+    * `RESULTS_DIR` - to change the location where the results report will be saved.
     * `PERSIST_RESULTS` - to persist the results report of the test. By default this will not be persisted in the case of a successful run.
 
 ## Re-evaluate existing report
 
 Robustness test validation is constantly changing and improving.
-Errors in etcd model could be causing false positives, which makes the ability to re-evaluate the reports after we fix the issue important.
+Errors in the etcd model could be causing false positives, which makes the ability to re-evaluate the reports after we fix the issue important.
 
 > Note: Robustness test report format is not stable, and it's expected that not all old reports can be re-evaluated using the newest version.
 
-1. Identify location of the robustness test report.
+1. Identify the location of the robustness test report.
 
    > Note: By default robustness test report is only generated for failed test.
 
@@ -124,7 +123,7 @@ Errors in etcd model could be causing false positives, which makes the ability t
       logger.go:146: 2024-04-08T09:45:27.734+0200 INFO    Saving robustness test report   {"path": "/tmp/TestRobustnessExploratory_Etcd_HighTraffic_ClusterOfSize1"}
       ```
 
-   * **For remote runs on CI:** you need to go to the [Prow Dashboard](https://prow.k8s.io/job-history/gs/kubernetes-jenkins/logs/ci-etcd-robustness-amd64), go to a build, download one of the Artifacts (`artifacts/results.zip`), and extract it locally.
+   * **For remote runs on CI:** you need to go to the [Prow Dashboard](https://testgrid.k8s.io/sig-etcd-robustness#Summary), go to a build, download one of the Artifacts (`artifacts/results.zip`), and extract it locally.
 
      ![Prow job run page](readme-images/prow_job.png)
 
@@ -144,14 +143,14 @@ Errors in etcd model could be causing false positives, which makes the ability t
 
    The `testdata` directory can contain multiple robustness test reports.
    The name of the report directory doesn't matter, as long as it's unique to prevent clashing with reports already present in `testdata` directory.
-   For example path for `history.html` file could look like `$REPO_ROOT/tests/robustness/testdata/v3.5_failure_24_April/history.html`.
+   For example, the path for `history.html` file could look like `$REPO_ROOT/tests/robustness/testdata/v3.5_failure_24_April/history.html`.
 
 3. Run `make test-robustness-reports` to validate all reports in the `testdata` directory.
 
 ## Analysing failure
 
-If robustness tests fails we want to analyse the report to confirm if the issue is on etcd side. Location of the directory with the report
-is mentioned `Saving robustness test report` log. Logs from report generation should look like:
+If robustness tests fail, we want to analyse the report to confirm if the issue is on etcd side. The location of the directory with the report
+is mentioned in the `Saving robustness test report` log. Logs from report generation should look like:
 ```
     logger.go:146: 2024-05-08T10:42:54.429+0200 INFO    Saving robustness test report   {"path": "/tmp/TestRobustnessRegression_Issue14370/1715157774429416550"}
     logger.go:146: 2024-05-08T10:42:54.429+0200 INFO    Saving member data dir  {"member": "TestRobustnessRegressionIssue14370-test-0", "path": "/tmp/TestRobustnessRegression_Issue14370/1715157774429416550/server-TestRobustnessRegressionIssue14370-test-0"}
@@ -178,21 +177,21 @@ is mentioned `Saving robustness test report` log. Logs from report generation sh
     logger.go:146: 2024-05-08T10:42:54.441+0200 INFO    Saving visualization    {"path": "/tmp/TestRobustnessRegression_Issue14370/1715157774429416550/history.html"}
 ```
 
-Report follows the hierarchy:
+The report follows the hierarchy:
 * `server-*` - etcd server data directories, can be used to verify disk/memory corruption.
   * `member`
     * `wal` - Write Ahead Log (WAL) directory, that can be analysed using `etcd-dump-logs` command line tool available in `tools` directory.
     * `snap` - Snapshot directory, includes the bbolt database file `db`, that can be analysed using `etcd-dump-db` command line tool available in `tools` directory.
 * `client-*` - Client request and response dumps in json format.
-  * `watch.jon` - Watch requests and responses, can be used to validate [watch API guarantees].
+  * `watch.json` - Watch requests and responses, can be used to validate [watch API guarantees].
   * `operations.json` - KV operation history
 * `history.html` - Visualization of KV operation history, can be used to validate [KV API guarantees].
 
-### Example analysis of linearization issue
+### Example analysis of a linearization issue
 
 Let's reproduce and analyse robustness test report for issue [#14370].
 To reproduce the issue by yourself run `make test-robustness-issue14370`.
-After a couple of tries robustness tests should fail with a log `Linearization failed` and save report locally.
+After a couple of tries robustness tests should fail with a log `Linearization failed` and save the report locally.
 
 Example:
 ```
@@ -211,14 +210,14 @@ Jump to the error in linearization by clicking `[ jump to first error ]` on the
 You should see a graph similar to the one on the image below.
 ![issue14370](readme-images/issue14370.png)
 
-Last correct request (connected with grey line) is a `Put` request that succeeded and got revision `168`.
+The last correct request (connected with the grey line) is a `Put` request that succeeded and got revision `168`.
 All following requests are invalid (connected with red line) as they have revision `167`. 
-Etcd guarantee that revision is non-decreasing, so this shows a bug in etcd as there is no way revision should decrease.
-This is consistent with the root cause of [#14370] as it was issue with process crash causing last write to be lost.
+Etcd guarantees that revision is non-decreasing, so this shows a bug in etcd as there is no way revision should decrease.
+This is consistent with the root cause of [#14370] as it was an issue with the process crash causing the last write to be lost.
 
 [#14370]: https://github.com/etcd-io/etcd/issues/14370
 
-### Example analysis of watch issue
+### Example analysis of a watch issue
 
 Let's reproduce and analyse robustness test report for issue [#15271].
 To reproduce the issue by yourself run `make test-robustness-issue15271`.
@@ -236,22 +235,24 @@ Example:
 ```
 
 Watch issues are easiest to analyse by reading the recorded watch history.
-Watch history is recorded for each client separated in different subdirectory under `/tmp/TestRobustnessRegression_Issue15271/1715158215866033806`
-Open `watch.json` for client mentioned in log `Broke watch guarantee`.
+
+Watch history is recorded for each client separated in different subdirectory under `/tmp/TestRobustnessRegression_Issue15271/1715158215866033806`.
+
+Open `watch.json` for the client mentioned in the log `Broke watch guarantee`.
 For client `4` that broke the watch guarantee open `/tmp/TestRobustnessRegression_Issue15271/1715158215866033806/client-4/watch.json`.
 
-Each line consists of json blob corresponding to single watch request sent by client.
-Look for events with `Revision` equal to revision mentioned in the first log with `Broke watch guarantee`, in this case look for `"Revision":3,`.
+Each line consists of json blob corresponding to a single watch request sent by the client.
+Look for events with `Revision` equal to revision mentioned in the first log with `Broke watch guarantee`, in this case, look for `"Revision":3,`.
 You should see watch responses where the `Revision` decreases like ones below:
 ```
 {"Events":[{"Type":"put-operation","Key":"key5","Value":{"Value":"793","Hash":0},"Revision":799,"IsCreate":false,"PrevValue":null}],"IsProgressNotify":false,"Revision":799,"Time":3202907249,"Error":""}
 {"Events":[{"Type":"put-operation","Key":"key4","Value":{"Value":"1","Hash":0},"Revision":3,"IsCreate":true,"PrevValue":null}, ...
 ```
 
-Up to the first response the `Revision` of events only increased up to a value of `799`.
+Up to the first response, the `Revision` of events only increased up to a value of `799`.
 However, the following line includes an event with `Revision` equal `3`.
-If you follow the `revision` throughout the file you should notice that watch replayed revisions second time.
+If you follow the `revision` throughout the file you should notice that watch replayed revisions for a second time.
 This is incorrect and breaks `Ordered` [watch API guarantees].
-This is consistent with the root cause of [#14370] where member reconnecting to cluster will resend revisions.
+This is consistent with the root cause of [#14370] where the member reconnecting to cluster will resend revisions.
 
 [#15271]: https://github.com/etcd-io/etcd/issues/15271
diff --git a/tests/robustness/client/kvhash.go b/tests/robustness/client/kvhash.go
new file mode 100644
index 000000000..e4fc2b482
--- /dev/null
+++ b/tests/robustness/client/kvhash.go
@@ -0,0 +1,69 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"go.uber.org/zap"
+
+	clientv3 "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/tests/v3/framework/e2e"
+)
+
+func CheckEndOfTestHashKV(ctx context.Context, clus *e2e.EtcdProcessCluster) error {
+	c, err := clientv3.New(clientv3.Config{
+		Endpoints:            clus.EndpointsGRPC(),
+		Logger:               zap.NewNop(),
+		DialKeepAliveTime:    10 * time.Second,
+		DialKeepAliveTimeout: 100 * time.Millisecond,
+	})
+	if err != nil {
+		return err
+	}
+	defer c.Close()
+
+	hashKV, err := c.HashKV(ctx, clus.EndpointsGRPC()[0], 0)
+	if err != nil {
+		return err
+	}
+	rev := hashKV.Header.Revision
+
+	hashKVs := make([]*clientv3.HashKVResponse, 0)
+	hashKVs = append(hashKVs, hashKV)
+
+	for _, member := range clus.Procs {
+		hashKV, err := c.HashKV(ctx, member.EndpointsGRPC()[0], rev)
+		if err != nil {
+			return err
+		}
+		if hashKV.Header.Revision != rev {
+			return fmt.Errorf("latest revision at the end of the test between nodes should be the same. Want %v, get %v", rev, hashKV.Header.Revision)
+		}
+		hashKVs = append(hashKVs, hashKV)
+	}
+
+	for i := 1; i < len(hashKVs); i++ {
+		if hashKVs[i-1].CompactRevision != hashKVs[i].CompactRevision {
+			return fmt.Errorf("compactRevision mismatch, node %v has %+v, node %v has %+v", hashKVs[i-1].Header.MemberId, hashKVs[i-1], hashKVs[i].Header.MemberId, hashKVs[i])
+		}
+		if hashKVs[i-1].Hash != hashKVs[i].Hash {
+			return fmt.Errorf("hash mismatch, node %v has %+v, node %v has %+v", hashKVs[i-1].Header.MemberId, hashKVs[i-1], hashKVs[i].Header.MemberId, hashKVs[i])
+		}
+	}
+	return nil
+}
diff --git a/tests/robustness/main_test.go b/tests/robustness/main_test.go
index 4f61278f4..8a430234c 100644
--- a/tests/robustness/main_test.go
+++ b/tests/robustness/main_test.go
@@ -55,7 +55,7 @@ func TestRobustnessExploratory(t *testing.T) {
 		t.Run(s.Name, func(t *testing.T) {
 			lg := zaptest.NewLogger(t)
 			s.Cluster.Logger = lg
-			ctx := context.Background()
+			ctx := t.Context()
 			c, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithConfig(&s.Cluster))
 			require.NoError(t, err)
 			defer forcestopCluster(c)
@@ -74,7 +74,7 @@ func TestRobustnessRegression(t *testing.T) {
 		t.Run(s.Name, func(t *testing.T) {
 			lg := zaptest.NewLogger(t)
 			s.Cluster.Logger = lg
-			ctx := context.Background()
+			ctx := t.Context()
 			c, err := e2e.NewEtcdProcessCluster(ctx, t, e2e.WithConfig(&s.Cluster))
 			require.NoError(t, err)
 			defer forcestopCluster(c)
@@ -102,8 +102,15 @@ func testRobustness(ctx context.Context, t *testing.T, lg *zap.Logger, s scenari
 		client.ValidateGotAtLeastOneProgressNotify(t, r.Client, s.Watch.RequestProgress || watchProgressNotifyEnabled)
 	}
 	validateConfig := validate.Config{ExpectRevisionUnique: s.Traffic.ExpectUniqueRevision()}
-	r.Visualize = validate.ValidateAndReturnVisualize(t, lg, validateConfig, r.Client, persistedRequests, 5*time.Minute).Visualize
-
+	result, err := validate.ValidateAndReturnVisualize(lg, validateConfig, r.Client, persistedRequests, 5*time.Minute)
+	if err != nil {
+		t.Errorf("Validation error: %v", err)
+	}
+	r.Visualize = result.Linearization.Visualize
+	err = result.Error()
+	if err != nil {
+		t.Error(err)
+	}
 	panicked = false
 }
 
@@ -149,6 +156,11 @@ func runScenario(ctx context.Context, t *testing.T, s scenarios.TestScenario, lg
 		return nil
 	})
 	g.Wait()
+
+	err := client.CheckEndOfTestHashKV(ctx, clus)
+	if err != nil {
+		t.Error(err)
+	}
 	return append(operationReport, append(failpointClientReport, watchReport...)...)
 }
 
diff --git a/tests/robustness/model/deterministic.go b/tests/robustness/model/deterministic.go
index 9b61d13af..70aaa5819 100644
--- a/tests/robustness/model/deterministic.go
+++ b/tests/robustness/model/deterministic.go
@@ -19,6 +19,7 @@ import (
 	"errors"
 	"fmt"
 	"hash/fnv"
+	"html"
 	"maps"
 	"reflect"
 	"sort"
@@ -42,36 +43,48 @@ import (
 //     whole change history as real etcd does.
 var DeterministicModel = porcupine.Model{
 	Init: func() any {
-		data, err := json.Marshal(freshEtcdState())
-		if err != nil {
-			panic(err)
-		}
-		return string(data)
+		return freshEtcdState()
 	},
 	Step: func(st any, in any, out any) (bool, any) {
-		var s EtcdState
-		err := json.Unmarshal([]byte(st.(string)), &s)
-		if err != nil {
-			panic(err)
-		}
-		ok, s := s.apply(in.(EtcdRequest), out.(EtcdResponse))
-		data, err := json.Marshal(s)
-		if err != nil {
-			panic(err)
-		}
-		return ok, string(data)
+		return st.(EtcdState).apply(in.(EtcdRequest), out.(EtcdResponse))
+	},
+	Equal: func(st1, st2 any) bool {
+		return st1.(EtcdState).Equal(st2.(EtcdState))
 	},
 	DescribeOperation: func(in, out any) string {
 		return fmt.Sprintf("%s -> %s", describeEtcdRequest(in.(EtcdRequest)), describeEtcdResponse(in.(EtcdRequest), MaybeEtcdResponse{EtcdResponse: out.(EtcdResponse)}))
 	},
+	DescribeState: func(st any) string {
+		data, err := json.MarshalIndent(st, "", "  ")
+		if err != nil {
+			panic(err)
+		}
+		return "<pre>" + html.EscapeString(string(data)) + "</pre>"
+	},
 }
 
 type EtcdState struct {
-	Revision        int64
-	CompactRevision int64
-	KeyValues       map[string]ValueRevision
-	KeyLeases       map[string]int64
-	Leases          map[int64]EtcdLease
+	Revision        int64                    `json:",omitempty"`
+	CompactRevision int64                    `json:",omitempty"`
+	KeyValues       map[string]ValueRevision `json:",omitempty"`
+	KeyLeases       map[string]int64         `json:",omitempty"`
+	Leases          map[int64]EtcdLease      `json:",omitempty"`
+}
+
+func (s EtcdState) Equal(other EtcdState) bool {
+	if s.Revision != other.Revision {
+		return false
+	}
+	if s.CompactRevision != other.CompactRevision {
+		return false
+	}
+	if !reflect.DeepEqual(s.KeyValues, other.KeyValues) {
+		return false
+	}
+	if !reflect.DeepEqual(s.KeyLeases, other.KeyLeases) {
+		return false
+	}
+	return reflect.DeepEqual(s.Leases, other.Leases)
 }
 
 func (s EtcdState) apply(request EtcdRequest, response EtcdResponse) (bool, EtcdState) {
@@ -109,21 +122,23 @@ func freshEtcdState() EtcdState {
 
 // Step handles a successful request, returning updated state and response it would generate.
 func (s EtcdState) Step(request EtcdRequest) (EtcdState, MaybeEtcdResponse) {
-	newState := s.DeepCopy()
-
-	switch request.Type {
-	case Range:
-		if request.Range.Revision == 0 || request.Range.Revision == newState.Revision {
-			resp := newState.getRange(request.Range.RangeOptions)
-			return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Range: &resp, Revision: newState.Revision}}
+	// TODO: Avoid copying when TXN only has read operations
+	if request.Type == Range {
+		if request.Range.Revision == 0 || request.Range.Revision == s.Revision {
+			resp := s.getRange(request.Range.RangeOptions)
+			return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{Range: &resp, Revision: s.Revision}}
 		}
-		if request.Range.Revision > newState.Revision {
-			return newState, MaybeEtcdResponse{Error: ErrEtcdFutureRev.Error()}
+		if request.Range.Revision > s.Revision {
+			return s, MaybeEtcdResponse{Error: ErrEtcdFutureRev.Error()}
 		}
-		if request.Range.Revision < newState.CompactRevision {
-			return newState, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
+		if request.Range.Revision < s.CompactRevision {
+			return s, MaybeEtcdResponse{EtcdResponse: EtcdResponse{ClientError: mvcc.ErrCompacted.Error()}}
 		}
-		return newState, MaybeEtcdResponse{Persisted: true, PersistedRevision: newState.Revision}
+		return s, MaybeEtcdResponse{Persisted: true, PersistedRevision: s.Revision}
+	}
+
+	newState := s.DeepCopy()
+	switch request.Type {
 	case Txn:
 		failure := false
 		for _, cond := range request.Txn.Conditions {
@@ -444,14 +459,14 @@ func (el EtcdLease) DeepCopy() EtcdLease {
 }
 
 type ValueRevision struct {
-	Value       ValueOrHash
-	ModRevision int64
-	Version     int64
+	Value       ValueOrHash `json:",omitempty"`
+	ModRevision int64       `json:",omitempty"`
+	Version     int64       `json:",omitempty"`
 }
 
 type ValueOrHash struct {
-	Value string
-	Hash  uint32
+	Value string `json:",omitempty"`
+	Hash  uint32 `json:",omitempty"`
 }
 
 func ToValueOrHash(value string) ValueOrHash {
diff --git a/tests/robustness/model/history.go b/tests/robustness/model/history.go
index fc224a8af..f6119984f 100644
--- a/tests/robustness/model/history.go
+++ b/tests/robustness/model/history.go
@@ -41,14 +41,17 @@ type AppendableHistory struct {
 	streamID int
 	// If needed a new streamId is requested from idProvider.
 	idProvider identity.Provider
+	// lastOperation holds the last operation of each stream.
+	lastOperation map[int]*porcupine.Operation
 
 	History
 }
 
 func NewAppendableHistory(ids identity.Provider) *AppendableHistory {
 	return &AppendableHistory{
-		streamID:   ids.NewStreamID(),
-		idProvider: ids,
+		streamID:      ids.NewStreamID(),
+		idProvider:    ids,
+		lastOperation: make(map[int]*porcupine.Operation),
 		History: History{
 			operations: []porcupine.Operation{},
 		},
@@ -290,8 +293,6 @@ func (h *AppendableHistory) appendFailed(request EtcdRequest, start, end time.Du
 	}
 	isRead := request.IsRead()
 	if !isRead {
-		// Failed writes can still be persisted, setting -1 for now as don't know when request has took effect.
-		op.Return = -1
 		// Operations of single client needs to be sequential.
 		// As we don't know return time of failed operations, all new writes need to be done with new stream id.
 		h.streamID = h.idProvider.NewStreamID()
@@ -300,11 +301,11 @@ func (h *AppendableHistory) appendFailed(request EtcdRequest, start, end time.Du
 }
 
 func (h *AppendableHistory) append(op porcupine.Operation) {
-	if op.Return != -1 && op.Call >= op.Return {
+	if op.Call >= op.Return {
 		panic(fmt.Sprintf("Invalid operation, call(%d) >= return(%d)", op.Call, op.Return))
 	}
-	if len(h.operations) > 0 {
-		prev := h.operations[len(h.operations)-1]
+
+	if prev, ok := h.lastOperation[op.ClientId]; ok {
 		if op.Call <= prev.Call {
 			panic(fmt.Sprintf("Out of order append, new.call(%d) <= prev.call(%d)", op.Call, prev.Call))
 		}
@@ -312,6 +313,8 @@ func (h *AppendableHistory) append(op porcupine.Operation) {
 			panic(fmt.Sprintf("Overlapping operations, new.call(%d) <= prev.return(%d)", op.Call, prev.Return))
 		}
 	}
+	h.lastOperation[op.ClientId] = &op
+
 	h.operations = append(h.operations, op)
 }
 
@@ -488,34 +491,8 @@ func (h History) Len() int {
 
 func (h History) Operations() []porcupine.Operation {
 	operations := make([]porcupine.Operation, 0, len(h.operations))
-	maxTime := h.lastObservedTime()
-	for _, op := range h.operations {
-		// Failed requests don't have a known return time.
-		if op.Return == -1 {
-			// Simulate Infinity by using last observed time.
-			op.Return = maxTime + time.Second.Nanoseconds()
-		}
-		operations = append(operations, op)
-	}
-	return operations
-}
 
-func (h History) lastObservedTime() int64 {
-	var maxTime int64
-	for _, op := range h.operations {
-		if op.Return == -1 {
-			// Collect call time from failed operations
-			if op.Call > maxTime {
-				maxTime = op.Call
-			}
-		} else {
-			// Collect return time from successful operations
-			if op.Return > maxTime {
-				maxTime = op.Return
-			}
-		}
-	}
-	return maxTime
+	return append(operations, h.operations...)
 }
 
 func (h History) MaxRevision() int64 {
diff --git a/tests/robustness/model/history_test.go b/tests/robustness/model/history_test.go
new file mode 100644
index 000000000..9b6d22f31
--- /dev/null
+++ b/tests/robustness/model/history_test.go
@@ -0,0 +1,245 @@
+// Copyright 2025 The etcd Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package model
+
+import (
+	"testing"
+
+	"github.com/anishathalye/porcupine"
+	"github.com/stretchr/testify/assert"
+
+	"go.etcd.io/etcd/tests/v3/robustness/identity"
+)
+
+func TestHistoryAppendSuccess(t *testing.T) {
+	tcs := []struct {
+		name       string
+		operations []porcupine.Operation
+	}{
+		{
+			name: "append non-overlapping operations for the same clientId",
+			operations: []porcupine.Operation{
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     100,
+					Output:   nil,
+					Return:   200,
+				},
+				{
+					ClientId: 2,
+					Input:    nil,
+					Call:     1,
+					Output:   nil,
+					Return:   2,
+				},
+			},
+		},
+		{
+			name: "append overlapping operations in different ClientId",
+			operations: []porcupine.Operation{
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     1,
+					Output:   nil,
+					Return:   100,
+				},
+				{
+					ClientId: 2,
+					Input:    nil,
+					Call:     1,
+					Output:   nil,
+					Return:   2,
+				},
+				{
+					ClientId: 3,
+					Input:    nil,
+					Call:     10,
+					Output:   nil,
+					Return:   20,
+				},
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     101,
+					Output:   nil,
+					Return:   200,
+				},
+				{
+					ClientId: 2,
+					Input:    nil,
+					Call:     3,
+					Output:   nil,
+					Return:   4,
+				},
+				{
+					ClientId: 3,
+					Input:    nil,
+					Call:     30,
+					Output:   nil,
+					Return:   40,
+				},
+			},
+		},
+	}
+
+	for _, tc := range tcs {
+		h := NewAppendableHistory(identity.NewIDProvider())
+
+		for _, operation := range tc.operations[:len(tc.operations)-1] {
+			h.append(operation)
+		}
+	}
+}
+
+func TestHistoryAppendFailure(t *testing.T) {
+	tcs := []struct {
+		name        string
+		operations  []porcupine.Operation
+		expectError string
+	}{
+		{
+			name: "append operation with call time > return time",
+			operations: []porcupine.Operation{
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     2,
+					Output:   nil,
+					Return:   1,
+				},
+			},
+			expectError: "Invalid operation, call(2) >= return(1)",
+		},
+		{
+			name: "out of order append in the same ClientId",
+			operations: []porcupine.Operation{
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     10,
+					Output:   nil,
+					Return:   100,
+				},
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     1,
+					Output:   nil,
+					Return:   10,
+				},
+			},
+			expectError: "Out of order append, new.call(1) <= prev.call(10)",
+		},
+		{
+			name: "out of order append in one of the ClientIds",
+			operations: []porcupine.Operation{
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     10,
+					Output:   nil,
+					Return:   100,
+				},
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     101,
+					Output:   nil,
+					Return:   200,
+				},
+				{
+					ClientId: 2,
+					Input:    nil,
+					Call:     10,
+					Output:   nil,
+					Return:   100,
+				},
+				{
+					ClientId: 2,
+					Input:    nil,
+					Call:     1,
+					Output:   nil,
+					Return:   10,
+				},
+			},
+			expectError: "Out of order append, new.call(1) <= prev.call(10)",
+		},
+		{
+			name: "append overlapping operations in the same ClientId",
+			operations: []porcupine.Operation{
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     1,
+					Output:   nil,
+					Return:   100,
+				},
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     10,
+					Output:   nil,
+					Return:   20,
+				},
+			},
+			expectError: "Overlapping operations, new.call(10) <= prev.return(100)",
+		},
+		{
+			name: "append overlapping operations in one of the ClientIds",
+			operations: []porcupine.Operation{
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     1,
+					Output:   nil,
+					Return:   100,
+				},
+				{
+					ClientId: 1,
+					Input:    nil,
+					Call:     101,
+					Output:   nil,
+					Return:   200,
+				},
+				{
+					ClientId: 2,
+					Input:    nil,
+					Call:     1,
+					Output:   nil,
+					Return:   100,
+				},
+				{
+					ClientId: 2,
+					Input:    nil,
+					Call:     10,
+					Output:   nil,
+					Return:   20,
+				},
+			},
+			expectError: "Overlapping operations, new.call(10) <= prev.return(100)",
+		},
+	}
+
+	for _, tc := range tcs {
+		h := NewAppendableHistory(identity.NewIDProvider())
+
+		for _, operation := range tc.operations[:len(tc.operations)-1] {
+			h.append(operation)
+		}
+		assert.PanicsWithValue(t, tc.expectError, func() { h.append(tc.operations[len(tc.operations)-1]) })
+	}
+}
diff --git a/tests/robustness/model/non_deterministic.go b/tests/robustness/model/non_deterministic.go
index 3167e340f..1bdbe87cd 100644
--- a/tests/robustness/model/non_deterministic.go
+++ b/tests/robustness/model/non_deterministic.go
@@ -17,6 +17,7 @@ package model
 import (
 	"encoding/json"
 	"fmt"
+	"html"
 	"reflect"
 
 	"github.com/anishathalye/porcupine"
@@ -28,32 +29,50 @@ import (
 // Failed requests fork the possible states, while successful requests merge and filter them.
 var NonDeterministicModel = porcupine.Model{
 	Init: func() any {
-		data, err := json.Marshal(nonDeterministicState{freshEtcdState()})
-		if err != nil {
-			panic(err)
-		}
-		return string(data)
+		return nonDeterministicState{freshEtcdState()}
 	},
 	Step: func(st any, in any, out any) (bool, any) {
-		var states nonDeterministicState
-		err := json.Unmarshal([]byte(st.(string)), &states)
-		if err != nil {
-			panic(err)
-		}
-		ok, states := states.apply(in.(EtcdRequest), out.(MaybeEtcdResponse))
-		data, err := json.Marshal(states)
-		if err != nil {
-			panic(err)
-		}
-		return ok, string(data)
+		return st.(nonDeterministicState).apply(in.(EtcdRequest), out.(MaybeEtcdResponse))
+	},
+	Equal: func(st1, st2 any) bool {
+		return st1.(nonDeterministicState).Equal(st2.(nonDeterministicState))
 	},
 	DescribeOperation: func(in, out any) string {
 		return fmt.Sprintf("%s -> %s", describeEtcdRequest(in.(EtcdRequest)), describeEtcdResponse(in.(EtcdRequest), out.(MaybeEtcdResponse)))
 	},
+	DescribeState: func(st any) string {
+		data, err := json.MarshalIndent(st, "", "  ")
+		if err != nil {
+			panic(err)
+		}
+		return "<pre>" + html.EscapeString(string(data)) + "</pre>"
+	},
 }
 
 type nonDeterministicState []EtcdState
 
+func (states nonDeterministicState) Equal(other nonDeterministicState) bool {
+	if len(states) != len(other) {
+		return false
+	}
+
+	otherMatched := make([]bool, len(other))
+	for _, sItem := range states {
+		foundMatchInOther := false
+		for j, otherItem := range other {
+			if !otherMatched[j] && sItem.Equal(otherItem) {
+				otherMatched[j] = true
+				foundMatchInOther = true
+				break
+			}
+		}
+		if !foundMatchInOther {
+			return false
+		}
+	}
+	return true
+}
+
 func (states nonDeterministicState) apply(request EtcdRequest, response MaybeEtcdResponse) (bool, nonDeterministicState) {
 	var newStates nonDeterministicState
 	switch {
diff --git a/tests/robustness/model/replay.go b/tests/robustness/model/replay.go
index 7f2be26e8..bed69d12f 100644
--- a/tests/robustness/model/replay.go
+++ b/tests/robustness/model/replay.go
@@ -134,20 +134,20 @@ func toWatchEvents(prevState *EtcdState, request EtcdRequest, response MaybeEtcd
 }
 
 type WatchEvent struct {
-	PersistedEvent
-	PrevValue *ValueRevision
+	PersistedEvent `json:",omitempty"`
+	PrevValue      *ValueRevision `json:",omitempty"`
 }
 
 type PersistedEvent struct {
-	Event
-	Revision int64
-	IsCreate bool
+	Event    `json:",omitempty"`
+	Revision int64 `json:",omitempty"`
+	IsCreate bool  `json:",omitempty"`
 }
 
 type Event struct {
-	Type  OperationType
-	Key   string
-	Value ValueOrHash
+	Type  OperationType `json:",omitempty"`
+	Key   string        `json:",omitempty"`
+	Value ValueOrHash   `json:",omitempty"`
 }
 
 func (e Event) Match(request WatchRequest) bool {
diff --git a/tests/robustness/model/watch.go b/tests/robustness/model/watch.go
index fc880e30e..de2c54d1c 100644
--- a/tests/robustness/model/watch.go
+++ b/tests/robustness/model/watch.go
@@ -17,14 +17,14 @@ package model
 import "time"
 
 type WatchOperation struct {
-	Request   WatchRequest
-	Responses []WatchResponse
+	Request   WatchRequest    `json:",omitempty"`
+	Responses []WatchResponse `json:",omitempty"`
 }
 
 type WatchResponse struct {
-	Events           []WatchEvent
-	IsProgressNotify bool
-	Revision         int64
-	Time             time.Duration
-	Error            string
+	Events           []WatchEvent  `json:",omitempty"`
+	IsProgressNotify bool          `json:",omitempty"`
+	Revision         int64         `json:",omitempty"`
+	Time             time.Duration `json:",omitempty"`
+	Error            string        `json:",omitempty"`
 }
diff --git a/tests/robustness/report/client.go b/tests/robustness/report/client.go
index 48d29b8ae..9bfc7cbc6 100644
--- a/tests/robustness/report/client.go
+++ b/tests/robustness/report/client.go
@@ -138,11 +138,11 @@ func loadKeyValueOperations(path string) (operations []porcupine.Operation, err
 		if os.IsNotExist(err) {
 			return nil, nil
 		}
-		return nil, fmt.Errorf("failed to open watch operation file: %q, err: %w", path, err)
+		return nil, fmt.Errorf("failed to open KV operation file: %q, err: %w", path, err)
 	}
 	file, err := os.OpenFile(path, os.O_RDONLY, 0o755)
 	if err != nil {
-		return nil, fmt.Errorf("failed to open watch operation file: %q, err: %w", path, err)
+		return nil, fmt.Errorf("failed to open KV operation file: %q, err: %w", path, err)
 	}
 	defer file.Close()
 	decoder := json.NewDecoder(file)
@@ -156,7 +156,7 @@ func loadKeyValueOperations(path string) (operations []porcupine.Operation, err
 		}
 		err = decoder.Decode(&operation)
 		if err != nil {
-			return nil, fmt.Errorf("failed to decode watch operation, err: %w", err)
+			return nil, fmt.Errorf("failed to decode KV operation, err: %w", err)
 		}
 		operations = append(operations, porcupine.Operation{
 			ClientId: operation.ClientID,
@@ -177,12 +177,13 @@ func persistWatchOperations(t *testing.T, lg *zap.Logger, path string, responses
 		return
 	}
 	defer file.Close()
-	encoder := json.NewEncoder(file)
 	for _, resp := range responses {
-		err := encoder.Encode(resp)
+		data, err := json.MarshalIndent(resp, "", "  ")
 		if err != nil {
 			t.Errorf("Failed to encode operation: %v", err)
 		}
+		file.Write(data)
+		file.WriteString("\n")
 	}
 }
 
@@ -190,15 +191,16 @@ func persistKeyValueOperations(t *testing.T, lg *zap.Logger, path string, operat
 	lg.Info("Saving operation history", zap.String("path", path))
 	file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
 	if err != nil {
-		t.Errorf("Failed to save operation history: %v", err)
+		t.Errorf("Failed to save KV operation history: %v", err)
 		return
 	}
 	defer file.Close()
-	encoder := json.NewEncoder(file)
 	for _, op := range operations {
-		err := encoder.Encode(op)
+		data, err := json.MarshalIndent(op, "", "  ")
 		if err != nil {
-			t.Errorf("Failed to encode operation: %v", err)
+			t.Errorf("Failed to encode KV operation: %v", err)
 		}
+		file.Write(data)
+		file.WriteString("\n")
 	}
 }
diff --git a/tests/robustness/report/report.go b/tests/robustness/report/report.go
index b3d3d5a5e..4b3780c5d 100644
--- a/tests/robustness/report/report.go
+++ b/tests/robustness/report/report.go
@@ -32,7 +32,7 @@ type TestReport struct {
 	Logger    *zap.Logger
 	Cluster   *e2e.EtcdProcessCluster
 	Client    []ClientReport
-	Visualize func(path string) error
+	Visualize func(lg *zap.Logger, path string) error
 }
 
 func testResultsDirectory(t *testing.T) string {
@@ -69,7 +69,7 @@ func (r *TestReport) Report(t *testing.T, force bool) {
 		persistClientReports(t, r.Logger, path, r.Client)
 	}
 	if r.Visualize != nil {
-		err := r.Visualize(filepath.Join(path, "history.html"))
+		err := r.Visualize(r.Logger, filepath.Join(path, "history.html"))
 		if err != nil {
 			t.Error(err)
 		}
diff --git a/tests/robustness/report/wal.go b/tests/robustness/report/wal.go
index 077ddc889..bb9b56e9b 100644
--- a/tests/robustness/report/wal.go
+++ b/tests/robustness/report/wal.go
@@ -114,10 +114,13 @@ func ReadWAL(lg *zap.Logger, dataDir string) (state raftpb.HardState, ents []raf
 		_, state, ents, err = w.ReadAll()
 		w.Close()
 		if err != nil {
-			if errors.Is(err, wal.ErrSnapshotNotFound) || errors.Is(err, wal.ErrSliceOutOfRange) {
+			if errors.Is(err, wal.ErrSnapshotNotFound) {
 				lg.Info("Error occurred when reading WAL entries", zap.Error(err))
 				return state, ents, nil
 			}
+			if errors.Is(err, wal.ErrSliceOutOfRange) {
+				return state, nil, fmt.Errorf("failed to read WAL, err: %w", err)
+			}
 			// we can only repair ErrUnexpectedEOF and we never repair twice.
 			if repaired || !errors.Is(err, io.ErrUnexpectedEOF) {
 				return state, nil, fmt.Errorf("failed to read WAL, cannot be repaired, err: %w", err)
diff --git a/tests/robustness/traffic/etcd.go b/tests/robustness/traffic/etcd.go
index 906f3d2cb..e00b07018 100644
--- a/tests/robustness/traffic/etcd.go
+++ b/tests/robustness/traffic/etcd.go
@@ -75,6 +75,16 @@ var (
 			{Choice: Delete, Weight: 50},
 		},
 	}
+	EtcdAntithesis Traffic = etcdTraffic{
+		keyCount:     10,
+		largePutSize: 32769,
+		leaseTTL:     DefaultLeaseTTL,
+		// Please keep the sum of weights equal 100.
+		requests: []random.ChoiceWeight[etcdRequestType]{
+			{Choice: Get, Weight: 50},
+			{Choice: Put, Weight: 50},
+		},
+	}
 )
 
 type etcdTraffic struct {
@@ -139,7 +149,7 @@ func (t etcdTraffic) RunTrafficLoop(ctx context.Context, c *client.RecordingClie
 			}
 			requestType = random.PickRandom(choices)
 		} else {
-			requestType = Get
+			requestType = List
 		}
 		rev, err := client.Request(ctx, requestType, lastRev)
 		if shouldReturn {
diff --git a/tests/robustness/traffic/traffic.go b/tests/robustness/traffic/traffic.go
index c66aa4543..43ea6ddc2 100644
--- a/tests/robustness/traffic/traffic.go
+++ b/tests/robustness/traffic/traffic.go
@@ -29,6 +29,7 @@ import (
 	"go.etcd.io/etcd/tests/v3/robustness/identity"
 	"go.etcd.io/etcd/tests/v3/robustness/model"
 	"go.etcd.io/etcd/tests/v3/robustness/report"
+	"go.etcd.io/etcd/tests/v3/robustness/validate"
 )
 
 var (
@@ -59,16 +60,13 @@ func SimulateTraffic(ctx context.Context, t *testing.T, lg *zap.Logger, clus *e2
 	endpoints := clus.EndpointsGRPC()
 
 	lm := identity.NewLeaseIDStorage()
-	reports := []report.ClientReport{}
 	// Use the highest MaximalQPS of all traffic profiles as burst otherwise actual traffic may be accidentally limited
 	limiter := rate.NewLimiter(rate.Limit(profile.MaximalQPS), profile.BurstableQPS)
 
-	cc, err := client.NewRecordingClient(endpoints, ids, baseTime)
+	r, err := CheckEmptyDatabaseAtStart(ctx, lg, endpoints, ids, baseTime)
 	require.NoError(t, err)
-	defer cc.Close()
-	// Ensure that first operation succeeds
-	_, err = cc.Put(ctx, "start", "true")
-	require.NoErrorf(t, err, "First operation failed, validation requires first operation to succeed")
+	reports := []report.ClientReport{r}
+
 	wg := sync.WaitGroup{}
 	nonUniqueWriteLimiter := NewConcurrencyLimiter(profile.MaxNonUniqueRequestConcurrency)
 	finish := make(chan struct{})
@@ -124,6 +122,9 @@ func SimulateTraffic(ctx context.Context, t *testing.T, lg *zap.Logger, clus *e2
 
 	time.Sleep(time.Second)
 	// Ensure that last operation succeeds
+	cc, err := client.NewRecordingClient(endpoints, ids, baseTime)
+	require.NoError(t, err)
+	defer cc.Close()
 	_, err = cc.Put(ctx, "tombstone", "true")
 	require.NoErrorf(t, err, "Last operation failed, validation requires last operation to succeed")
 	reports = append(reports, cc.Report())
@@ -202,3 +203,25 @@ type Traffic interface {
 	RunCompactLoop(ctx context.Context, c *client.RecordingClient, period time.Duration, finish <-chan struct{})
 	ExpectUniqueRevision() bool
 }
+
+func CheckEmptyDatabaseAtStart(ctx context.Context, lg *zap.Logger, endpoints []string, ids identity.Provider, baseTime time.Time) (report.ClientReport, error) {
+	c, err := client.NewRecordingClient(endpoints, ids, baseTime)
+	if err != nil {
+		return report.ClientReport{}, err
+	}
+	defer c.Close()
+	for {
+		rCtx, cancel := context.WithTimeout(ctx, RequestTimeout)
+		resp, err := c.Get(rCtx, "key")
+		cancel()
+		if err != nil {
+			lg.Warn("Failed to check if database empty at start, retrying", zap.Error(err))
+			continue
+		}
+		if resp.Header.Revision != 1 {
+			return report.ClientReport{}, validate.ErrNotEmptyDatabase
+		}
+		break
+	}
+	return c.Report(), nil
+}
diff --git a/tests/robustness/validate/operations.go b/tests/robustness/validate/operations.go
index 605df732c..c738aae8a 100644
--- a/tests/robustness/validate/operations.go
+++ b/tests/robustness/validate/operations.go
@@ -24,7 +24,6 @@ import (
 	"go.uber.org/zap"
 
 	"go.etcd.io/etcd/tests/v3/robustness/model"
-	"go.etcd.io/etcd/tests/v3/robustness/report"
 )
 
 var (
@@ -32,15 +31,39 @@ var (
 	errFutureRevRespRequested = errors.New("request about a future rev with response")
 )
 
-type Results struct {
+type Result struct {
+	Linearization     LinearizationResult
+	WatchError        error
+	SerializableError error
+}
+
+func (r Result) Error() error {
+	switch r.Linearization.Linearizable {
+	case porcupine.Illegal:
+		return errors.New("linearization failed")
+	case porcupine.Unknown:
+		return errors.New("linearization timed out")
+	case porcupine.Ok:
+	default:
+		return fmt.Errorf("unknown linearization result %q", r.Linearization.Linearizable)
+	}
+	if r.WatchError != nil {
+		return fmt.Errorf("watch validation failed: %w", r.WatchError)
+	}
+	if r.SerializableError != nil {
+		return fmt.Errorf("serializable validation failed: %w", r.SerializableError)
+	}
+	return nil
+}
+
+type LinearizationResult struct {
 	Info         porcupine.LinearizationInfo
 	Model        porcupine.Model
 	Linearizable porcupine.CheckResult
-	Lg           *zap.Logger // TODO: Remove logger from struct and instead of making it an argument for Visualize
 }
 
-func (r Results) Visualize(path string) error {
-	r.Lg.Info("Saving visualization", zap.String("path", path))
+func (r LinearizationResult) Visualize(lg *zap.Logger, path string) error {
+	lg.Info("Saving visualization", zap.String("path", path))
 	err := porcupine.VisualizePath(r.Model, r.Info, path)
 	if err != nil {
 		return fmt.Errorf("failed to visualize, err: %w", err)
@@ -49,34 +72,18 @@ func (r Results) Visualize(path string) error {
 }
 
 func validateLinearizableOperationsAndVisualize(
-	lg *zap.Logger,
 	operations []porcupine.Operation,
 	timeout time.Duration,
-) (results Results) {
-	lg.Info("Validating linearizable operations", zap.Duration("timeout", timeout))
-	start := time.Now()
+) (results LinearizationResult) {
 	result, info := porcupine.CheckOperationsVerbose(model.NonDeterministicModel, operations, timeout)
-
-	switch result {
-	case porcupine.Illegal:
-		lg.Error("Linearization failed", zap.Duration("duration", time.Since(start)))
-	case porcupine.Unknown:
-		lg.Error("Linearization has timed out", zap.Duration("duration", time.Since(start)))
-	case porcupine.Ok:
-		lg.Info("Linearization success", zap.Duration("duration", time.Since(start)))
-	default:
-		panic(fmt.Sprintf("Unknown Linearization result %s", result))
-	}
-	return Results{
+	return LinearizationResult{
 		Info:         info,
 		Model:        model.NonDeterministicModel,
 		Linearizable: result,
-		Lg:           lg,
 	}
 }
 
 func validateSerializableOperations(lg *zap.Logger, operations []porcupine.Operation, replay *model.EtcdReplay) (lastErr error) {
-	lg.Info("Validating serializable operations")
 	for _, read := range operations {
 		request := read.Input.(model.EtcdRequest)
 		response := read.Output.(model.MaybeEtcdResponse)
@@ -88,19 +95,6 @@ func validateSerializableOperations(lg *zap.Logger, operations []porcupine.Opera
 	return lastErr
 }
 
-func filterSerializableOperations(clients []report.ClientReport) []porcupine.Operation {
-	resp := []porcupine.Operation{}
-	for _, client := range clients {
-		for _, op := range client.KeyValue {
-			request := op.Input.(model.EtcdRequest)
-			if request.Type == model.Range && request.Range.Revision != 0 {
-				resp = append(resp, op)
-			}
-		}
-	}
-	return resp
-}
-
 func validateSerializableRead(lg *zap.Logger, replay *model.EtcdReplay, request model.EtcdRequest, response model.MaybeEtcdResponse) error {
 	if response.Persisted || response.Error != "" {
 		return nil
diff --git a/tests/robustness/validate/operations_test.go b/tests/robustness/validate/operations_test.go
index a244d9f75..d096be6f1 100644
--- a/tests/robustness/validate/operations_test.go
+++ b/tests/robustness/validate/operations_test.go
@@ -17,9 +17,12 @@ package validate
 
 import (
 	"fmt"
+	"math/rand/v2"
 	"testing"
+	"time"
 
 	"github.com/anishathalye/porcupine"
+	"go.uber.org/zap"
 	"go.uber.org/zap/zaptest"
 
 	"go.etcd.io/etcd/tests/v3/robustness/model"
@@ -294,3 +297,107 @@ func keyValueRevision(key, value string, rev int64) model.KeyValue {
 		},
 	}
 }
+
+func BenchmarkValidateLinearizableOperations(b *testing.B) {
+	lg := zap.NewNop()
+	b.Run("Successes", func(b *testing.B) {
+		history := allPutSuccesses(1000)
+		shuffles := shuffleHistory(history, b.N)
+		b.ResetTimer()
+		validateShuffles(b, lg, shuffles, time.Second)
+	})
+	b.Run("AllFailures", func(b *testing.B) {
+		history := allPutFailures(10)
+		shuffles := shuffleHistory(history, b.N)
+		b.ResetTimer()
+		validateShuffles(b, lg, shuffles, time.Second)
+	})
+	b.Run("PutFailuresWithRead", func(b *testing.B) {
+		history := putFailuresWithRead(b, 8)
+		shuffles := shuffleHistory(history, b.N)
+		b.ResetTimer()
+		validateShuffles(b, lg, shuffles, time.Second)
+	})
+}
+
+func allPutSuccesses(concurrencyCount int) []porcupine.Operation {
+	ops := []porcupine.Operation{}
+	for i := 0; i < concurrencyCount; i++ {
+		ops = append(ops, porcupine.Operation{
+			ClientId: i,
+			Input:    putRequest("key", "value"),
+			Output:   putResponse(int64(i)+2, model.EtcdOperationResult{}),
+			Call:     int64(i),
+			Return:   int64(i) + int64(concurrencyCount),
+		})
+	}
+	return ops
+}
+
+func putFailuresWithRead(b *testing.B, concurrencyCount int) []porcupine.Operation {
+	ops := []porcupine.Operation{}
+	for i := 0; i < concurrencyCount; i++ {
+		ops = append(ops, porcupine.Operation{
+			ClientId: i,
+			Input:    putRequest(fmt.Sprintf("key%d", i), "value"),
+			Output:   errorResponse(fmt.Errorf("timeout")),
+			Call:     int64(i),
+			Return:   int64(i) + int64(concurrencyCount),
+		})
+	}
+	requests := []model.EtcdRequest{}
+	for _, op := range ops {
+		requests = append(requests, op.Input.(model.EtcdRequest))
+	}
+	replay := model.NewReplay(requests)
+	state, err := replay.StateForRevision(int64(concurrencyCount) + 1)
+	if err != nil {
+		b.Fatal(err)
+	}
+	request := rangeRequest("key", "kez", 0, 0)
+	_, resp := state.Step(request)
+	ops = append(ops, porcupine.Operation{
+		ClientId: 0,
+		Input:    request,
+		Output:   resp,
+		Call:     int64(concurrencyCount) + 1,
+		Return:   int64(concurrencyCount) + 2,
+	})
+	return ops
+}
+
+func allPutFailures(concurrencyCount int) []porcupine.Operation {
+	ops := []porcupine.Operation{}
+	for i := 0; i < concurrencyCount; i++ {
+		ops = append(ops, porcupine.Operation{
+			ClientId: i,
+			Input:    putRequest("key", "value"),
+			Output:   errorResponse(fmt.Errorf("timeout")),
+			Call:     int64(i),
+			Return:   int64(i) + int64(concurrencyCount),
+		})
+	}
+	return ops
+}
+
+func shuffleHistory(history []porcupine.Operation, shuffleCount int) [][]porcupine.Operation {
+	shuffles := make([][]porcupine.Operation, shuffleCount)
+	for i := 0; i < shuffleCount; i++ {
+		historyCopy := make([]porcupine.Operation, len(history))
+		copy(historyCopy, history)
+		rand.Shuffle(len(historyCopy), func(i, j int) {
+			historyCopy[i], historyCopy[j] = historyCopy[j], historyCopy[i]
+		})
+		shuffles[i] = historyCopy
+	}
+	return shuffles
+}
+
+func validateShuffles(b *testing.B, lg *zap.Logger, shuffles [][]porcupine.Operation, duration time.Duration) {
+	for i := 0; i < len(shuffles); i++ {
+		result := validateLinearizableOperationsAndVisualize(shuffles[i], duration)
+		if result.Linearizable != porcupine.Ok {
+			b.Fatalf("Not linearizable: %v", result.Linearizable)
+		}
+	}
+}
diff --git a/tests/robustness/validate/patch_history.go b/tests/robustness/validate/patch_history.go
index b4f6787db..0196369d8 100644
--- a/tests/robustness/validate/patch_history.go
+++ b/tests/robustness/validate/patch_history.go
@@ -16,6 +16,7 @@ package validate
 
 import (
 	"fmt"
+	"math"
 
 	"github.com/anishathalye/porcupine"
 
@@ -23,28 +24,12 @@ import (
 	"go.etcd.io/etcd/tests/v3/robustness/report"
 )
 
-func patchLinearizableOperations(reports []report.ClientReport, persistedRequests []model.EtcdRequest) []porcupine.Operation {
-	allOperations := relevantOperations(reports)
+func patchLinearizableOperations(operations []porcupine.Operation, reports []report.ClientReport, persistedRequests []model.EtcdRequest) []porcupine.Operation {
 	putRevision := putRevision(reports)
 	persistedPutCount := countPersistedPuts(persistedRequests)
 	clientPutCount := countClientPuts(reports)
-	putReturnTime := uniquePutReturnTime(allOperations, reports, persistedRequests, clientPutCount)
-	return patchOperations(allOperations, putRevision, putReturnTime, clientPutCount, persistedPutCount)
-}
-
-func relevantOperations(reports []report.ClientReport) []porcupine.Operation {
-	var ops []porcupine.Operation
-	for _, r := range reports {
-		for _, op := range r.KeyValue {
-			request := op.Input.(model.EtcdRequest)
-			resp := op.Output.(model.MaybeEtcdResponse)
-			// Remove failed read requests as they are not relevant for linearization.
-			if resp.Error == "" || !request.IsRead() {
-				ops = append(ops, op)
-			}
-		}
-	}
-	return ops
+	putReturnTime := uniquePutReturnTime(operations, reports, persistedRequests, clientPutCount)
+	return patchOperations(operations, putRevision, putReturnTime, clientPutCount, persistedPutCount)
 }
 
 func putRevision(reports []report.ClientReport) map[keyValue]int64 {
@@ -214,7 +199,9 @@ func uniquePutReturnTime(allOperations []porcupine.Operation, reports []report.C
 		request := persistedRequests[i]
 		switch request.Type {
 		case model.Txn:
-			lastReturnTime--
+			if lastReturnTime != math.MaxInt64 {
+				lastReturnTime--
+			}
 			for _, op := range request.Txn.OperationsOnSuccess {
 				if op.Type != model.PutOperation {
 					continue
diff --git a/tests/robustness/validate/patch_history_test.go b/tests/robustness/validate/patch_history_test.go
index b5611d602..2fa6a5fbb 100644
--- a/tests/robustness/validate/patch_history_test.go
+++ b/tests/robustness/validate/patch_history_test.go
@@ -17,6 +17,7 @@ package validate
 
 import (
 	"errors"
+	"math"
 	"testing"
 	"time"
 
@@ -30,7 +31,7 @@ import (
 	"go.etcd.io/etcd/tests/v3/robustness/report"
 )
 
-const infinite = 1000000000
+const infinite = math.MaxInt64
 
 func TestPatchHistory(t *testing.T) {
 	for _, tc := range []struct {
@@ -58,25 +59,25 @@ func TestPatchHistory(t *testing.T) {
 				putRequest("key", "value"),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 200, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 200, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed put remains if there is a matching event, return time untouched",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendPut("key", "value", 100, infinite, nil, errors.New("failed"))
+				h.AppendPut("key", "value", 100, 200, nil, errors.New("failed"))
 			},
 			persistedRequest: []model.EtcdRequest{
 				putRequest("key", "value"),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 99, Output: model.MaybeEtcdResponse{Persisted: true}},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Persisted: true}},
 			},
 		},
 		{
 			name: "failed put remains if there is a matching event, uniqueness allows for return time to be based on next persisted request",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendPut("key1", "value", 100, infinite, nil, errors.New("failed"))
+				h.AppendPut("key1", "value", 100, 200, nil, errors.New("failed"))
 				h.AppendPut("key2", "value", 300, 400, &clientv3.PutResponse{}, nil)
 			},
 			persistedRequest: []model.EtcdRequest{
@@ -85,13 +86,13 @@ func TestPatchHistory(t *testing.T) {
 			},
 			expectedRemainingOperations: []porcupine.Operation{
 				{Return: 399, Output: model.MaybeEtcdResponse{Persisted: true}},
-				{Return: 400, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 400, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed put remains if there is a matching persisted request, uniqueness allows for revision and return time to be based on watch",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendPut("key", "value", 100, infinite, nil, errors.New("failed"))
+				h.AppendPut("key", "value", 100, 200, nil, errors.New("failed"))
 			},
 			persistedRequest: []model.EtcdRequest{
 				putRequest("key", "value"),
@@ -113,52 +114,52 @@ func TestPatchHistory(t *testing.T) {
 			},
 			watchOperations: watchResponse(3, putEvent("key", "value", 2), putEvent("key", "value", 3)),
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 1000000004, Output: model.MaybeEtcdResponse{Error: "failed"}},
-				{Return: 4, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Error: "failed"}},
+				{Return: 4, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed put is dropped if event has different key",
 			historyFunc: func(h *model.AppendableHistory) {
 				h.AppendPut("key2", "value", 100, 200, &clientv3.PutResponse{}, nil)
-				h.AppendPut("key1", "value", 300, infinite, nil, errors.New("failed"))
+				h.AppendPut("key1", "value", 300, 400, nil, errors.New("failed"))
 			},
 			persistedRequest: []model.EtcdRequest{
 				putRequest("key2", "value"),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 200, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 200, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed put is dropped if event has different value",
 			historyFunc: func(h *model.AppendableHistory) {
 				h.AppendPut("key", "value2", 100, 200, &clientv3.PutResponse{}, nil)
-				h.AppendPut("key", "value1", 300, infinite, nil, errors.New("failed"))
+				h.AppendPut("key", "value1", 300, 400, nil, errors.New("failed"))
 			},
 			persistedRequest: []model.EtcdRequest{
 				putRequest("key", "value2"),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 200, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 200, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed put with lease remains if there is a matching event, return time untouched",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendPutWithLease("key", "value", 123, 100, infinite, nil, errors.New("failed"))
+				h.AppendPutWithLease("key", "value", 123, 100, 200, nil, errors.New("failed"))
 			},
 			persistedRequest: []model.EtcdRequest{
 				putRequestWithLease("key", "value", 123),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 99, Output: model.MaybeEtcdResponse{Persisted: true}},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Persisted: true}},
 			},
 		},
 		{
 			name: "failed put with lease remains if there is a matching event, uniqueness allows return time to be based on next persisted request",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendPutWithLease("key1", "value", 123, 100, infinite, nil, errors.New("failed"))
+				h.AppendPutWithLease("key1", "value", 123, 100, 200, nil, errors.New("failed"))
 				h.AppendPutWithLease("key2", "value", 234, 300, 400, &clientv3.PutResponse{}, nil)
 			},
 			persistedRequest: []model.EtcdRequest{
@@ -167,7 +168,7 @@ func TestPatchHistory(t *testing.T) {
 			},
 			expectedRemainingOperations: []porcupine.Operation{
 				{Return: 399, Output: model.MaybeEtcdResponse{Persisted: true}},
-				{Return: 400, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 400, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
@@ -195,21 +196,21 @@ func TestPatchHistory(t *testing.T) {
 			},
 			watchOperations: watchResponse(3, putEvent("key", "value", 2), putEvent("key", "value", 3)),
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 1000000004, Output: model.MaybeEtcdResponse{Error: "failed"}},
-				{Return: 4, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Error: "failed"}},
+				{Return: 4, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed put is dropped",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendPut("key", "value", 100, infinite, nil, errors.New("failed"))
+				h.AppendPut("key", "value", 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{},
 		},
 		{
 			name: "failed put with lease is dropped",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendPutWithLease("key", "value", 123, 100, infinite, nil, errors.New("failed"))
+				h.AppendPutWithLease("key", "value", 123, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{},
 		},
@@ -219,13 +220,13 @@ func TestPatchHistory(t *testing.T) {
 				h.AppendDelete("key", 100, 200, &clientv3.DeleteResponse{}, nil)
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 200, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 200, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed delete remains, time untouched regardless of persisted event and watch",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendDelete("key", 100, infinite, nil, errors.New("failed"))
+				h.AppendDelete("key", 100, 200, nil, errors.New("failed"))
 				h.AppendPut("key", "value", 300, 400, &clientv3.PutResponse{}, nil)
 			},
 			persistedRequest: []model.EtcdRequest{
@@ -233,43 +234,43 @@ func TestPatchHistory(t *testing.T) {
 			},
 			watchOperations: watchResponse(3, deleteEvent("key", 2)),
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 400, Output: model.MaybeEtcdResponse{Error: "failed"}},
-				{Return: 400, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Error: "failed"}},
+				{Return: 400, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed empty txn is dropped",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{}, []clientv3.Op{}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{}, []clientv3.Op{}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{},
 		},
 		{
 			name: "failed txn put is dropped",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{},
 		},
 		{
 			name: "failed txn put remains if there is a matching event",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{}, 100, 200, nil, errors.New("failed"))
 			},
 			persistedRequest: []model.EtcdRequest{
 				putRequest("key", "value"),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 99, Output: model.MaybeEtcdResponse{Persisted: true}},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Persisted: true}},
 			},
 		},
 		{
 			name: "failed txn delete remains",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{clientv3.OpDelete("key")}, []clientv3.Op{}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{clientv3.OpDelete("key")}, []clientv3.Op{}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 100, Output: model.MaybeEtcdResponse{Error: "failed"}},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Error: "failed"}},
 			},
 		},
 		{
@@ -278,50 +279,50 @@ func TestPatchHistory(t *testing.T) {
 				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{clientv3.OpDelete("key")}, 100, 200, &clientv3.TxnResponse{Succeeded: true}, nil)
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 200, Output: putResponse()},
+				{Return: 200, Output: putResponse(0)},
 			},
 		},
 		{
 			name: "failed txn put/delete remains",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{clientv3.OpDelete("key")}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{clientv3.OpDelete("key")}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 100, Output: model.MaybeEtcdResponse{Error: "failed"}},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Error: "failed"}},
 			},
 		},
 		{
 			name: "failed txn delete/put remains",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{clientv3.OpDelete("key")}, []clientv3.Op{clientv3.OpPut("key", "value")}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{clientv3.OpDelete("key")}, []clientv3.Op{clientv3.OpPut("key", "value")}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 100, Output: model.MaybeEtcdResponse{Error: "failed"}},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Error: "failed"}},
 			},
 		},
 		{
 			name: "failed txn empty/put is dropped",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{}, []clientv3.Op{clientv3.OpPut("key", "value")}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{}, []clientv3.Op{clientv3.OpPut("key", "value")}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{},
 		},
 		{
 			name: "failed txn empty/put remains if there is a matching event",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value")}, []clientv3.Op{}, 100, 200, nil, errors.New("failed"))
 			},
 			persistedRequest: []model.EtcdRequest{
 				putRequest("key", "value"),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 99, Output: model.MaybeEtcdResponse{Persisted: true}},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Persisted: true}},
 			},
 		},
 		{
 			name: "failed put remains if there is a matching persisted request, uniqueness of this operation and following operation allows patching based on following operation",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendPut("key1", "value1", 300, infinite, nil, errors.New("failed"))
+				h.AppendPut("key1", "value1", 300, 400, nil, errors.New("failed"))
 				h.AppendPut("key2", "value2", 500, 600, &clientv3.PutResponse{}, nil)
 			},
 			persistedRequest: []model.EtcdRequest{
@@ -330,14 +331,14 @@ func TestPatchHistory(t *testing.T) {
 			},
 			expectedRemainingOperations: []porcupine.Operation{
 				{Return: 599, Output: model.MaybeEtcdResponse{Persisted: true}},
-				{Return: 600, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 600, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed put remains if there is a matching persisted request, lack of uniqueness of this operation prevents patching based on following operation",
 			historyFunc: func(h *model.AppendableHistory) {
 				h.AppendPut("key1", "value1", 100, 200, &clientv3.PutResponse{}, nil)
-				h.AppendPut("key1", "value1", 300, infinite, nil, errors.New("failed"))
+				h.AppendPut("key1", "value1", 300, 400, nil, errors.New("failed"))
 				h.AppendPut("key2", "value2", 500, 600, &clientv3.PutResponse{}, nil)
 			},
 			persistedRequest: []model.EtcdRequest{
@@ -346,16 +347,16 @@ func TestPatchHistory(t *testing.T) {
 				putRequest("key2", "value2"),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 200, Output: putResponse(model.EtcdOperationResult{})},
-				{Return: infinite + 600, Output: model.MaybeEtcdResponse{Error: "failed"}},
-				{Return: 600, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 200, Output: putResponse(0, model.EtcdOperationResult{})},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Error: "failed"}},
+				{Return: 600, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed put remains if there is a matching persisted request, lack of uniqueness of following operation prevents patching based on following operation",
 			historyFunc: func(h *model.AppendableHistory) {
 				h.AppendPut("key2", "value2", 100, 200, &clientv3.PutResponse{}, nil)
-				h.AppendPut("key1", "value1", 300, infinite, nil, errors.New("failed"))
+				h.AppendPut("key1", "value1", 300, 400, nil, errors.New("failed"))
 				h.AppendPut("key2", "value2", 500, 600, &clientv3.PutResponse{}, nil)
 			},
 			persistedRequest: []model.EtcdRequest{
@@ -364,39 +365,39 @@ func TestPatchHistory(t *testing.T) {
 				putRequest("key2", "value2"),
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: 200, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: 200, Output: putResponse(0, model.EtcdOperationResult{})},
 				// TODO: We can infer that failed operation finished before last operation matching following.
-				{Return: infinite + 598, Output: model.MaybeEtcdResponse{Persisted: true}},
-				{Return: 600, Output: putResponse(model.EtcdOperationResult{})},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Persisted: true}},
+				{Return: 600, Output: putResponse(0, model.EtcdOperationResult{})},
 			},
 		},
 		{
 			name: "failed txn empty/delete remains",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{}, []clientv3.Op{clientv3.OpDelete("key")}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{}, []clientv3.Op{clientv3.OpDelete("key")}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{
-				{Return: infinite + 100, Output: model.MaybeEtcdResponse{Error: "failed"}},
+				{Return: infinite, Output: model.MaybeEtcdResponse{Error: "failed"}},
 			},
 		},
 		{
 			name: "failed txn put&delete is dropped",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value1"), clientv3.OpDelete("key")}, []clientv3.Op{}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value1"), clientv3.OpDelete("key")}, []clientv3.Op{}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{},
 		},
 		{
 			name: "failed txn empty/put&delete is dropped",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{}, []clientv3.Op{clientv3.OpPut("key", "value1"), clientv3.OpDelete("key")}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{}, []clientv3.Op{clientv3.OpPut("key", "value1"), clientv3.OpDelete("key")}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{},
 		},
 		{
 			name: "failed txn put&delete/put&delete is dropped",
 			historyFunc: func(h *model.AppendableHistory) {
-				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value1"), clientv3.OpDelete("key")}, []clientv3.Op{clientv3.OpPut("key", "value2"), clientv3.OpDelete("key")}, 100, infinite, nil, errors.New("failed"))
+				h.AppendTxn(nil, []clientv3.Op{clientv3.OpPut("key", "value1"), clientv3.OpDelete("key")}, []clientv3.Op{clientv3.OpPut("key", "value2"), clientv3.OpDelete("key")}, 100, 200, nil, errors.New("failed"))
 			},
 			expectedRemainingOperations: []porcupine.Operation{},
 		},
@@ -404,14 +405,16 @@ func TestPatchHistory(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			history := model.NewAppendableHistory(identity.NewIDProvider())
 			tc.historyFunc(history)
-			operations := patchLinearizableOperations([]report.ClientReport{
+			reports := []report.ClientReport{
 				{
 					ClientID: 0,
 					KeyValue: history.History.Operations(),
 					Watch:    tc.watchOperations,
 				},
-			}, tc.persistedRequest)
-			if diff := cmp.Diff(tc.expectedRemainingOperations, operations,
+			}
+			operations, _ := prepareAndCategorizeOperations(reports)
+			patched := patchLinearizableOperations(operations, reports, tc.persistedRequest)
+			if diff := cmp.Diff(tc.expectedRemainingOperations, patched,
 				cmpopts.EquateEmpty(),
 				cmpopts.IgnoreFields(porcupine.Operation{}, "Input", "Call", "ClientId"),
 			); diff != "" {
@@ -421,8 +424,8 @@ func TestPatchHistory(t *testing.T) {
 	}
 }
 
-func putResponse(result ...model.EtcdOperationResult) model.MaybeEtcdResponse {
-	return model.MaybeEtcdResponse{EtcdResponse: model.EtcdResponse{Txn: &model.TxnResponse{Results: result}}}
+func putResponse(rev int64, result ...model.EtcdOperationResult) model.MaybeEtcdResponse {
+	return model.MaybeEtcdResponse{EtcdResponse: model.EtcdResponse{Revision: rev, Txn: &model.TxnResponse{Results: result}}}
 }
 
 func watchResponse(responseTime int64, events ...model.WatchEvent) []model.WatchOperation {
diff --git a/tests/robustness/validate/validate.go b/tests/robustness/validate/validate.go
index a23734953..944c2f21f 100644
--- a/tests/robustness/validate/validate.go
+++ b/tests/robustness/validate/validate.go
@@ -15,59 +15,105 @@
 package validate
 
 import (
-	"encoding/json"
+	"errors"
 	"fmt"
-	"testing"
+	"math"
 	"time"
 
 	"github.com/anishathalye/porcupine"
-	"github.com/stretchr/testify/require"
 	"go.uber.org/zap"
 
 	"go.etcd.io/etcd/tests/v3/robustness/model"
 	"go.etcd.io/etcd/tests/v3/robustness/report"
 )
 
-func ValidateAndReturnVisualize(t *testing.T, lg *zap.Logger, cfg Config, reports []report.ClientReport, persistedRequests []model.EtcdRequest, timeout time.Duration) Results {
-	err := checkValidationAssumptions(reports, persistedRequests)
-	require.NoErrorf(t, err, "Broken validation assumptions")
-	linearizableOperations := patchLinearizableOperations(reports, persistedRequests)
-	serializableOperations := filterSerializableOperations(reports)
+var ErrNotEmptyDatabase = errors.New("non empty database at start, required by model used for linearizability validation")
 
-	results := validateLinearizableOperationsAndVisualize(lg, linearizableOperations, timeout)
-	if results.Linearizable != porcupine.Ok {
-		t.Error("Failed linearization, skipping further validation")
-		return results
-	}
-
-	// TODO: Use requests from linearization for replay.
-	replay := model.NewReplay(persistedRequests)
-
-	err = validateWatch(lg, cfg, reports, replay)
+func ValidateAndReturnVisualize(lg *zap.Logger, cfg Config, reports []report.ClientReport, persistedRequests []model.EtcdRequest, timeout time.Duration) (result Result, err error) {
+	err = checkValidationAssumptions(reports)
 	if err != nil {
-		t.Errorf("Failed validating watch history, err: %s", err)
-	}
-	err = validateSerializableOperations(lg, serializableOperations, replay)
-	if err != nil {
-		t.Errorf("Failed validating serializable operations, err: %s", err)
+		return result, err
+	}
+	linearizableOperations, serializableOperations := prepareAndCategorizeOperations(reports)
+	// We are passing in the original reports and linearizableOperations with modified return time.
+	// The reason is that linearizableOperations are those dedicated for linearization, which requires them to have returnTime set to infinity as required by pourcupine.
+	// As for the report, the original report is used so the consumer doesn't need to track what patching was done or not.
+	if persistedRequests != nil {
+		linearizableOperations = patchLinearizableOperations(linearizableOperations, reports, persistedRequests)
+	}
+
+	lg.Info("Validating linearizable operations", zap.Duration("timeout", timeout))
+	start := time.Now()
+	result.Linearization = validateLinearizableOperationsAndVisualize(linearizableOperations, timeout)
+	switch result.Linearization.Linearizable {
+	case porcupine.Illegal:
+		lg.Error("Linearization failed", zap.Duration("duration", time.Since(start)))
+	case porcupine.Unknown:
+		lg.Error("Linearization has timed out", zap.Duration("duration", time.Since(start)))
+	case porcupine.Ok:
+		lg.Info("Linearization success", zap.Duration("duration", time.Since(start)))
+	default:
+		panic(fmt.Sprintf("Unknown Linearization result %s", result.Linearization.Linearizable))
+	}
+
+	if persistedRequests != nil {
+		replay := model.NewReplay(persistedRequests)
+
+		lg.Info("Validating watch")
+		start = time.Now()
+		result.WatchError = validateWatch(lg, cfg, reports, replay)
+		if result.WatchError == nil {
+			lg.Info("Watch validation success", zap.Duration("duration", time.Since(start)))
+		} else {
+			lg.Error("Watch validation failed", zap.Duration("duration", time.Since(start)), zap.Error(result.WatchError))
+		}
+
+		lg.Info("Validating serializable operations")
+		start = time.Now()
+		result.SerializableError = validateSerializableOperations(lg, serializableOperations, replay)
+		if result.SerializableError == nil {
+			lg.Info("Serializable validation success", zap.Duration("duration", time.Since(start)))
+		} else {
+			lg.Error("Serializable validation failed", zap.Duration("duration", time.Since(start)), zap.Error(result.SerializableError))
+		}
 	}
-	return results
+
+	return result, nil
 }
 
 type Config struct {
 	ExpectRevisionUnique bool
 }
 
-func checkValidationAssumptions(reports []report.ClientReport, persistedRequests []model.EtcdRequest) error {
-	err := validateEmptyDatabaseAtStart(reports)
-	if err != nil {
-		return err
+func prepareAndCategorizeOperations(reports []report.ClientReport) (linearizable []porcupine.Operation, serializable []porcupine.Operation) {
+	for _, report := range reports {
+		for _, op := range report.KeyValue {
+			request := op.Input.(model.EtcdRequest)
+			response := op.Output.(model.MaybeEtcdResponse)
+			// serializable operations include only Range requests on non-zero revision
+			if request.Type == model.Range && request.Range.Revision != 0 {
+				serializable = append(serializable, op)
+			}
+			// Remove failed read requests as they are not relevant for linearization.
+			if response.Error == "" || !request.IsRead() {
+				// For linearization, we set the return time of failed requests to MaxInt64.
+				// Failed requests can still be persisted, however we don't know when request has taken effect.
+				if response.Error != "" {
+					op.Return = math.MaxInt64
+				}
+				linearizable = append(linearizable, op)
+			}
+		}
 	}
+	return linearizable, serializable
+}
 
-	err = validatePersistedRequestMatchClientRequests(reports, persistedRequests)
+func checkValidationAssumptions(reports []report.ClientReport) error {
+	err := validateEmptyDatabaseAtStart(reports)
 	if err != nil {
 		return err
 	}
+
 	err = validateNonConcurrentClientRequests(reports)
 	if err != nil {
 		return err
@@ -76,84 +122,19 @@ func checkValidationAssumptions(reports []report.ClientReport, persistedRequests
 }
 
 func validateEmptyDatabaseAtStart(reports []report.ClientReport) error {
-	for _, r := range reports {
-		for _, op := range r.KeyValue {
-			request := op.Input.(model.EtcdRequest)
-			response := op.Output.(model.MaybeEtcdResponse)
-			if response.Revision == 2 && !request.IsRead() {
-				return nil
-			}
-		}
+	if len(reports) == 0 {
+		return nil
 	}
-	return fmt.Errorf("non empty database at start or first write didn't succeed, required by model implementation")
-}
-
-func validatePersistedRequestMatchClientRequests(reports []report.ClientReport, persistedRequests []model.EtcdRequest) error {
-	persistedRequestSet := map[string]model.EtcdRequest{}
-	for _, request := range persistedRequests {
-		data, err := json.Marshal(request)
-		if err != nil {
-			return err
-		}
-		persistedRequestSet[string(data)] = request
-	}
-	clientRequests := map[string]porcupine.Operation{}
-	for _, r := range reports {
-		for _, op := range r.KeyValue {
-			request := op.Input.(model.EtcdRequest)
-			data, err := json.Marshal(request)
-			if err != nil {
-				return err
-			}
-			clientRequests[string(data)] = op
-		}
-	}
-
-	for requestDump, request := range persistedRequestSet {
-		_, found := clientRequests[requestDump]
-		// We cannot validate if persisted leaseGrant was sent by client as failed leaseGrant will not return LeaseID to clients.
-		if request.Type == model.LeaseGrant {
-			continue
-		}
-
-		if !found {
-			return fmt.Errorf("request %+v was not sent by client, required to validate", requestDump)
-		}
-	}
-
-	var firstOp, lastOp porcupine.Operation
 	for _, r := range reports {
 		for _, op := range r.KeyValue {
 			request := op.Input.(model.EtcdRequest)
 			response := op.Output.(model.MaybeEtcdResponse)
-			if response.Error != "" || request.IsRead() {
-				continue
-			}
-			if firstOp.Call == 0 || op.Call < firstOp.Call {
-				firstOp = op
-			}
-			if lastOp.Call == 0 || op.Call > lastOp.Call {
-				lastOp = op
+			if response.Revision == 1 && request.IsRead() {
+				return nil
 			}
 		}
 	}
-	firstOpData, err := json.Marshal(firstOp.Input.(model.EtcdRequest))
-	if err != nil {
-		return err
-	}
-	_, found := persistedRequestSet[string(firstOpData)]
-	if !found {
-		return fmt.Errorf("first succesful client write %s was not persisted, required to validate", firstOpData)
-	}
-	lastOpData, err := json.Marshal(lastOp.Input.(model.EtcdRequest))
-	if err != nil {
-		return err
-	}
-	_, found = persistedRequestSet[string(lastOpData)]
-	if !found {
-		return fmt.Errorf("last succesful client write %s was not persisted, required to validate", lastOpData)
-	}
-	return nil
+	return ErrNotEmptyDatabase
 }
 
 func validateNonConcurrentClientRequests(reports []report.ClientReport) error {
diff --git a/tests/robustness/validate/validate_test.go b/tests/robustness/validate/validate_test.go
index bd6709af9..2af78b915 100644
--- a/tests/robustness/validate/validate_test.go
+++ b/tests/robustness/validate/validate_test.go
@@ -21,6 +21,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/anishathalye/porcupine"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/zap/zaptest"
 
@@ -45,14 +46,150 @@ func TestDataReports(t *testing.T) {
 
 			persistedRequests, err := report.LoadClusterPersistedRequests(lg, path)
 			require.NoError(t, err)
-			visualize := ValidateAndReturnVisualize(t, zaptest.NewLogger(t), Config{}, reports, persistedRequests, 5*time.Minute).Visualize
+			result, err := ValidateAndReturnVisualize(zaptest.NewLogger(t), Config{}, reports, persistedRequests, 5*time.Minute)
+			require.NoError(t, err)
+
+			err = result.Linearization.Visualize(lg, filepath.Join(path, "history.html"))
+			require.NoError(t, err)
+		})
+	}
+}
+
+func TestValidateAndReturnVisualize(t *testing.T) {
+	tcs := []struct {
+		name              string
+		reports           []report.ClientReport
+		persistedRequests []model.EtcdRequest
+		config            Config
+		expectError       string
+	}{
+		{
+			name: "Success with no persisted requests",
+			reports: []report.ClientReport{
+				{
+					KeyValue: []porcupine.Operation{
+						{
+							ClientId: 0,
+							Input:    getRequest("key"),
+							Call:     100,
+							Output:   getResponse(1),
+							Return:   200,
+						},
+					},
+				},
+			},
+			expectError: "",
+		},
+		{
+			name: "Failure with not empty database",
+			reports: []report.ClientReport{
+				{
+					KeyValue: []porcupine.Operation{
+						{
+							ClientId: 0,
+							Input:    getRequest("key"),
+							Call:     100,
+							// Empty database should have revision 1
+							Output: getResponse(2),
+							Return: 200,
+						},
+					},
+				},
+			},
+			expectError: "non empty database at start, required by model used for linearizability validation",
+		},
+		{
+			name: "Success",
+			reports: []report.ClientReport{
+				{
+					KeyValue: []porcupine.Operation{
+						{
+							ClientId: 0,
+							Input:    getRequest("key"),
+							Call:     100,
+							Output:   getResponse(1),
+							Return:   200,
+						},
+						{
+							ClientId: 0,
+							Input:    putRequest("key", "value"),
+							Call:     300,
+							Output:   putResponse(2, model.EtcdOperationResult{}),
+							Return:   400,
+						},
+					},
+					Watch: []model.WatchOperation{
+						{
+							Request: model.WatchRequest{Key: "key"},
+							Responses: []model.WatchResponse{
+								{Events: []model.WatchEvent{watchEvent(2, true, model.PutOperation, "key", "value")}},
+							},
+						},
+					},
+				},
+			},
+			persistedRequests: []model.EtcdRequest{putRequest("key", "value")},
+			expectError:       "",
+		},
+		{
+			name: "Failure of watch",
+			reports: []report.ClientReport{
+				{
+					KeyValue: []porcupine.Operation{
+						{
+							ClientId: 0,
+							Input:    getRequest("key"),
+							Call:     100,
+							Output:   getResponse(1),
+							Return:   200,
+						},
+						{
+							ClientId: 0,
+							Input:    putRequest("key", "value"),
+							Call:     300,
+							Output:   putResponse(2, model.EtcdOperationResult{}),
+							Return:   400,
+						},
+					},
+					Watch: []model.WatchOperation{
+						{
+							Request: model.WatchRequest{Key: "key"},
+							Responses: []model.WatchResponse{
+								{Events: []model.WatchEvent{watchEvent(2, true, model.PutOperation, "key", "value2")}},
+							},
+						},
+					},
+				},
+			},
+			persistedRequests: []model.EtcdRequest{putRequest("key", "value")},
+			expectError:       "watch validation failed",
+		},
+	}
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			lg := zaptest.NewLogger(t)
+			result, err := ValidateAndReturnVisualize(lg, Config{}, tc.reports, tc.persistedRequests, 5*time.Second)
 
-			err = visualize(filepath.Join(path, "history.html"))
+			if tc.expectError != "" {
+				if err != nil {
+					require.ErrorContains(t, err, tc.expectError)
+				} else {
+					require.ErrorContains(t, result.Error(), tc.expectError)
+				}
+			} else {
+				require.NoError(t, err)
+				require.NoError(t, result.Error())
+			}
+			err = result.Linearization.Visualize(lg, filepath.Join(t.TempDir(), "history.html"))
 			require.NoError(t, err)
 		})
 	}
 }
 
+func watchEvent(rev int64, isCreate bool, eventType model.OperationType, key, value string) model.WatchEvent {
+	return model.WatchEvent{PersistedEvent: model.PersistedEvent{Revision: rev, IsCreate: isCreate, Event: model.Event{Type: eventType, Key: key, Value: model.ToValueOrHash(value)}}}
+}
+
 func TestValidateWatch(t *testing.T) {
 	tcs := []struct {
 		name              string
@@ -1915,6 +2052,21 @@ func deletePersistedEvent(key string, rev int64) model.PersistedEvent {
 	}
 }
 
+func getRequest(key string) model.EtcdRequest {
+	return model.EtcdRequest{
+		Type: model.Range,
+		Range: &model.RangeRequest{
+			RangeOptions: model.RangeOptions{
+				Start: "key",
+			},
+		},
+	}
+}
+
+func getResponse(rev int64) model.MaybeEtcdResponse {
+	return model.MaybeEtcdResponse{EtcdResponse: model.EtcdResponse{Revision: rev, Range: &model.RangeResponse{KVs: []model.KeyValue{}}}}
+}
+
 func putRequest(key, value string) model.EtcdRequest {
 	return model.EtcdRequest{
 		Type:        model.Txn,
diff --git a/tests/robustness/validate/watch.go b/tests/robustness/validate/watch.go
index 506cbeca4..bba43c66e 100644
--- a/tests/robustness/validate/watch.go
+++ b/tests/robustness/validate/watch.go
@@ -38,7 +38,6 @@ var (
 )
 
 func validateWatch(lg *zap.Logger, cfg Config, reports []report.ClientReport, replay *model.EtcdReplay) error {
-	lg.Info("Validating watch")
 	// Validate etcd watch properties defined in https://etcd.io/docs/v3.6/learning/api_guarantees/#watch-apis
 	for _, r := range reports {
 		err := validateFilter(lg, r)
diff --git a/tools/etcd-dump-logs/etcd-dump-log_test.go b/tools/etcd-dump-logs/etcd-dump-log_test.go
index bb08ec114..c2581cebb 100644
--- a/tools/etcd-dump-logs/etcd-dump-log_test.go
+++ b/tools/etcd-dump-logs/etcd-dump-log_test.go
@@ -82,10 +82,10 @@ func TestEtcdDumpLogEntryType(t *testing.T) {
 			expected, err := os.ReadFile(path.Join(binDir, argtest.fileExpected))
 			require.NoError(t, err)
 
-			assert.EqualValues(t, string(expected), string(actual))
+			assert.Equal(t, string(expected), string(actual))
 			// The output files contains a lot of trailing whitespaces... difficult to diagnose without printing them explicitly.
 			// TODO(ptabor): Get rid of the whitespaces both in code and the test-files.
-			assert.EqualValues(t, strings.ReplaceAll(string(expected), " ", "_"), strings.ReplaceAll(string(actual), " ", "_"))
+			assert.Equal(t, strings.ReplaceAll(string(expected), " ", "_"), strings.ReplaceAll(string(actual), " ", "_"))
 		})
 	}
 }