Repository: etcd-io/etcd

Issue:
Organize etcdctl commands and add more documents
### What would you like to be added?

I want commands of etcdctl to be organized and documented in a better way.

1. For organizing i suggest to remove nested part of commands for example instead of having this:
```
        user add                Adds a new user
        user delete             Deletes a user
        user get                Gets detailed information of a user
        user grant-role         Grants a role to a user
        user list               Lists all users
        user passwd             Changes password of user
        user revoke-role        Revokes a role from a user
```

We can do something like this:
```
      Basic KV commands:
        del                  Removes the specified key or range of keys [key, range_end)
        get                  Gets the key or a range of keys
        put                  Puts the given key into the store

      Management Commands:
        user                  User related commands
        role                  Role related commands
```
And rest of this for other commands. 

2. For documenting i suggest to make the documents and help texts for commands better and more helpful throw some examples and define better.
For example when we invoke `etcdctl leahe -h` we get 
```
NAME:
        lease - Lease related commands

USAGE:
        etcdctl lease <subcommand> [flags]
```

Maybe we can do something like this:
```
NAME:
        lease - Lease related commands

Description:
        Leases in etcd act as timers for data, automatically deleting keys when their associated lease expires.

USAGE:
        etcdctl lease <subcommand> [flags]

        etcdctl lease grant 60   # Creates a lease that lasts 60 seconds

        etcdctl lease keep-alive <lease_id> --once # Resets the keep-alive time to its original value and cobrautl.Exits immediately

```
Add a one line description so new users can understand what it is without going and searching for what is the lease, it's a tradeoff we don't want to throw the whole document here just a little sip of what is it. 
And put some popular usages in the USAGE section would be nice too.

### Why is this needed?

As a newcomer to etcdctl it was hard for me to understand what it does at first, i was constantly searching the websites for commands to understand some of it. To enhance user experience, I propose implementing measures to clarify command functionalities upfront, easing the learning curve.

Also i think the first view of `etcdctl -h` is a little bit crowded and i believe with removing some subcommands that are not necessarry to show at first and style it a little bit this way users are greeted with a more intuitive interface.

Discussion:
@ah8ad3: I want to get more involve in etcd community and i think with this issue i can help to make `etcdctl` better and also understand the basics and hopefully start working on core of etcd. If team approves, i would like to define some tasks and start working on this issue.@ah8ad3: /area documentation
/area etcdctl@ivanvc: I can't think of any reason we wouldn't want to do this. I'd suggest opening PRs; the task sounds massive, though, so maybe we should follow a targeted approach to individual subcommands to open it up to discussion.

@jmhbnz, any thoughts on this?@jmhbnz: Hey @ah8ad3 - Thanks for raising this. I am absolutely on board with improving the built in documentation. I do actually like seeing all the available subcommands at once rather than gating them behind additional `-h` commands but that is just personal preference and if community consensus is to change it that is no problem.

If you can see gaps for documentation please feel free to start raising some pull requests, thanks! 

/assign @ah8ad3 @ah8ad3: Thanks @jmhbnz @ivanvc, I'll try to create a TODO list and start working on it.@ah8ad3: My perspective is these tasks:

- [ ] organize the commands of `etcdctl` just grouping similar commands together.

add description and some sample usages/result in these files

- [ ] `alarm` - `alarm disarm`
- [ ] `auth`
- [ ] `check` - `check datascale` - `check perf`
- [ ] `compaction`
- [ ] `defrag`
- [ ] `downgrade`
- [ ] `elect`
- [ ] `endpoint`
- [ ] `lease`
- [ ] `lock`
- [ ] `make-mirror`
- [ ] `save`
- [ ] `txn`
- [ ] `user`


Just in case as an example the document of `etcdctl snapshot` is 
```
COMMANDS:
	save	Stores an etcd node backend snapshot to a given file

```
I expect `etcdctl snapshot save` would run but after running i get `Error: snapshot save expects one argument`.
So the goal is to clarify that in the documents of `etcdctl snapshot`.@stale: 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.@hwdef: Hi, I would like to know the progress on this issue. Are there any unfinished parts that need help? I am happy to provide support.
@ah8ad3 @hwdef: @ivanvc @jmhbnz 
I noticed that this issue has been inactive for a long time. Can I go ahead and finish it?
@ah8ad3: Sorry @hwdef i was very busy for the last few months, you can go ahead and work on them if you like, 
I suggest to add document to sub commands separately and i will help on review if you need any.@hwdef: @ah8ad3 Thank you! I'll try some@hwdef: Hi, all. @ah8ad3 @ivanvc @jmhbnz 
I try to group the subcommand , and I'm not sure if this is ok. Because some groups only have one option.
```
Basic KV Operations:
  get
  del
  put
  watch

Cluster Management:
  elect
  make-mirror
  member
  move-leader

Maintenance, Health & Diagnostics: 
  check
  compaction
  defrag
  endpoint
  snapshot

Authentication & Authorization:
  auth
  role
  user

Lease Management:
  lease

Alarm Management:
  alarm

Downgrade Management:
  downgrade

Distributed Operations:
  lock
  txn

Utility & Help:
  completion
  version
  help
```@hwdef: /cc @ahrtr 
Please take a look :)@ivanvc: Overall, I think it makes sense and LGTM. But I defer to @ahrtr / @serathius.@ahrtr: I’m fine as long as the command usage doesn’t change. thx for enhancing this.@ah8ad3: Thanks for the reorganization @hwdef 
I'm all on board with it, also I think starting with adding help messaging into sub commands can speed up the process. @hwdef: I found a grouped docs in https://github.com/etcd-io/etcd/tree/main/etcdctl. I decided to modify the code in this way
I'll try to create a PR in this week.
```
Key-value commands:
  put
  get
  del
  txn
  compaction
  watch
  lease
Cluster maintenance commands:
  member
  endpoint
  alarm
  defrag
  snapshot
  move-leader
  downgrade
Concurrency commands:
  lock
  elect
Authentication commands:
  auth
  role
  user
Utility commands:
  make-mirror
  version
  check
  completion
  help
```@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.@ahrtr: Please add a changelog for this, thx@ahrtr: Resolved in https://github.com/etcd-io/etcd/pull/20486, thx

Solution patch:
diff --git a/.github/workflows/antithesis-test.yml b/.github/workflows/antithesis-test.yml
index ffba5e6c7..9c64672cf 100644
--- a/.github/workflows/antithesis-test.yml
+++ b/.github/workflows/antithesis-test.yml
@@ -51,7 +51,7 @@ jobs:
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
       - name: Login to Antithesis Docker Registry
-        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
+        uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
         with:
           registry: ${{ env.REGISTRY }}
           username: _json_key
diff --git a/.github/workflows/antithesis-verify.yml b/.github/workflows/antithesis-verify.yml
new file mode 100644
index 000000000..7db1fd88f
--- /dev/null
+++ b/.github/workflows/antithesis-verify.yml
@@ -0,0 +1,69 @@
+---
+name: Verify Antithesis Docker Compose Pipeline
+
+on:
+  push:
+    branches:
+      - main
+    paths:
+      - 'tests/antithesis/**'
+      - '.github/workflows/antithesis-verify.yml'
+  pull_request:
+    paths:
+      - 'tests/antithesis/**'
+      - '.github/workflows/antithesis-verify.yml'
+
+jobs:
+  test-docker-compose:
+    strategy:
+      matrix:
+        node-count: [1, 3]
+    name: Test ${{ matrix.node-count }}-node cluster
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Build etcd-server and etcd-client images
+        run: |
+          make -C tests/antithesis antithesis-build-etcd-image
+          make -C tests/antithesis antithesis-build-client-docker-image CFG_NODE_COUNT=${{ matrix.node-count }}
+
+      - name: Run docker-compose up
+        working-directory: ./tests/antithesis
+        run: |
+          make antithesis-docker-compose-up CFG_NODE_COUNT=${{ matrix.node-count }} &
+
+      - name: Check for healthy cluster
+        working-directory: ./tests/antithesis
+        run: |
+          timeout=120
+          interval=10
+          end_time=$(( $(date +%s) + timeout ))
+
+          while [ $(date +%s) -lt $end_time ]; do
+            # The client container might not be running yet, so ignore errors from docker compose logs
+            if docker compose -f config/docker-compose-${{ matrix.node-count }}-node.yml logs client 2>/dev/null | grep -q "Client \[entrypoint\]: cluster is healthy!"; then
+              echo "Cluster is healthy!"
+              exit 0
+            fi
+            echo "Waiting for cluster to become healthy..."
+            sleep $interval
+          done
+
+          echo "Cluster did not become healthy in ${timeout} seconds."
+          docker compose -f config/docker-compose-${{ matrix.node-count }}-node.yml logs
+          exit 1
+
+      - name: Run traffic
+        working-directory: ./tests/antithesis
+        run: make antithesis-run-container-traffic CFG_NODE_COUNT=${{ matrix.node-count }}
+
+      - name: Run validation
+        working-directory: ./tests/antithesis
+        run: make antithesis-run-container-validation CFG_NODE_COUNT=${{ matrix.node-count }}
+
+      - name: Clean up
+        if: always()
+        working-directory: ./tests/antithesis
+        run: make antithesis-clean CFG_NODE_COUNT=${{ matrix.node-count }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index d73af216e..9ce6bb03e 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@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3
+        uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
         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@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3
+        uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3
+        uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index de20fc0d9..83cd3f889 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -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@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3
+        uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
         with:
           sarif_file: results.sarif
diff --git a/CHANGELOG/CHANGELOG-3.6.md b/CHANGELOG/CHANGELOG-3.6.md
index ce6596e16..1745ad84c 100644
--- a/CHANGELOG/CHANGELOG-3.6.md
+++ b/CHANGELOG/CHANGELOG-3.6.md
@@ -6,6 +6,10 @@ Previous change logs can be found at [CHANGELOG-3.5](https://github.com/etcd-io/
 
 ## v3.6.5 (TBA)
 
+### etcd server
+
+- [Remove the flag `--experimental-snapshot-catch-up-entries` from `etcd --help` output](https://github.com/etcd-io/etcd/pull/20422)
+
 ---
 
 ## v3.6.4 (2025-07-25)
diff --git a/CHANGELOG/CHANGELOG-3.7.md b/CHANGELOG/CHANGELOG-3.7.md
index 78685988f..20b328654 100644
--- a/CHANGELOG/CHANGELOG-3.7.md
+++ b/CHANGELOG/CHANGELOG-3.7.md
@@ -15,14 +15,17 @@ Previous change logs can be found at [CHANGELOG-3.6](https://github.com/etcd-io/
 
 - [Update go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc to v0.61.0 and replaced the deprecated `UnaryServerInterceptor` and `StreamServerInterceptor` with `NewServerHandler`](https://github.com/etcd-io/etcd/pull/20017)
 - [Add Support for Unix Socket endpoints](https://github.com/etcd-io/etcd/pull/19760)
+- [Improves performance of lease and user/role operations (up to 2x) by updating `(*readView) Rev()` to use `SharedBufReadTxMode`](https://github.com/etcd-io/etcd/pull/20411)
 
 ### Package `pkg`
 
 - [Optimize find performance by splitting intervals with the same left endpoint by their right endpoints](https://github.com/etcd-io/etcd/pull/19768)
+- [netutil: Refactor IPv6 address comparison logic](https://github.com/etcd-io/etcd/pull/20365)
 
 ### Dependencies
 
 - Compile binaries using [go 1.24.5](https://github.com/etcd-io/etcd/pull/20320)
+- [Migrate the deprecated go-grpc-middleware v1 logging and tags libraries to v2 interceptors](https://github.com/etcd-io/etcd/pull/20420)
 
 ### Deprecations
 
diff --git a/Dockerfile b/Dockerfile
index f29109e4a..f6ee26f2b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 ARG ARCH=amd64
-FROM --platform=linux/${ARCH} gcr.io/distroless/static-debian12@sha256:b7b9a6953e7bed6baaf37329331051d7bdc1b99c885f6dbeb72d75b1baad54f9
+FROM --platform=linux/${ARCH} gcr.io/distroless/static-debian12@sha256:2e114d20aa6371fd271f854aa3d6b2b7d2e70e797bb3ea44fb677afec60db22c
 
 ADD etcd /usr/local/bin/
 ADD etcdctl /usr/local/bin/
diff --git a/api/go.mod b/api/go.mod
index 651001a0b..af804c12e 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -10,8 +10,8 @@ require (
 	github.com/golang/protobuf v1.5.4
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	github.com/stretchr/testify v1.10.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822
-	google.golang.org/grpc v1.73.0
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0
+	google.golang.org/grpc v1.74.2
 	google.golang.org/protobuf v1.36.6
 )
 
@@ -24,7 +24,7 @@ require (
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/api/go.sum b/api/go.sum
index c9ade099a..cd8bc821d 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -73,12 +73,12 @@ 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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
diff --git a/bill-of-materials.json b/bill-of-materials.json
index 8a601abd9..8991e0c87 100644
--- a/bill-of-materials.json
+++ b/bill-of-materials.json
@@ -216,7 +216,7 @@
 		]
 	},
 	{
-		"project": "github.com/grpc-ecosystem/go-grpc-middleware",
+		"project": "github.com/grpc-ecosystem/go-grpc-middleware/logging/settable",
 		"licenses": [
 			{
 				"type": "Apache License 2.0",
diff --git a/cache/cache.go b/cache/cache.go
index fd049c5f5..8b9abd44a 100644
--- a/cache/cache.go
+++ b/cache/cache.go
@@ -22,7 +22,7 @@ import (
 	"sync"
 	"time"
 
-	rpctypes "go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
 	clientv3 "go.etcd.io/etcd/client/v3"
 )
 
@@ -39,24 +39,18 @@ type Cache struct {
 	prefix      string // prefix is the key-prefix this shard is responsible for ("" = root).
 	cfg         Config // immutable runtime configuration
 	watcher     clientv3.Watcher
+	kv          clientv3.KV
 	demux       *demux // demux fans incoming events out to active watchers and manages resync.
-	ready       chan struct{}
+	store       *store // last‑observed snapshot
+	ready       *ready
 	stop        context.CancelFunc
 	waitGroup   sync.WaitGroup
 	internalCtx context.Context
 }
 
-// watchCtx collects all the knobs that both serveWatchEvents and watchRetryLoop need.
-type watchCtx struct {
-	cache           *Cache
-	backoffStart    time.Duration
-	backoffMax      time.Duration
-	onFirstResponse func() // callback to fire once on first upstream response
-}
-
 // New builds a cache shard that watches only the requested prefix.
 // For the root cache pass "".
-func New(watcher clientv3.Watcher, prefix string, opts ...Option) (*Cache, error) {
+func New(client *clientv3.Client, prefix string, opts ...Option) (*Cache, error) {
 	cfg := defaultConfig()
 	for _, opt := range opts {
 		opt(&cfg)
@@ -71,8 +65,10 @@ func New(watcher clientv3.Watcher, prefix string, opts ...Option) (*Cache, error
 	cache := &Cache{
 		prefix:      prefix,
 		cfg:         cfg,
-		watcher:     watcher,
-		ready:       make(chan struct{}),
+		watcher:     client.Watcher,
+		kv:          client.KV,
+		store:       newStore(),
+		ready:       newReady(),
 		stop:        cancel,
 		internalCtx: internalCtx,
 	}
@@ -82,15 +78,7 @@ func New(watcher clientv3.Watcher, prefix string, opts ...Option) (*Cache, error
 	cache.waitGroup.Add(1)
 	go func() {
 		defer cache.waitGroup.Done()
-		readyOnce := sync.Once{}
-
-		watchCtx := &watchCtx{
-			cache:           cache,
-			backoffStart:    cfg.InitialBackoff,
-			backoffMax:      cfg.MaxBackoff,
-			onFirstResponse: func() { readyOnce.Do(func() { close(cache.ready) }) },
-		}
-		serveWatchEvents(internalCtx, watchCtx)
+		cache.getWatchLoop(internalCtx)
 	}()
 
 	return cache, nil
@@ -99,9 +87,7 @@ func New(watcher clientv3.Watcher, prefix string, opts ...Option) (*Cache, error
 // Watch registers a cache-backed watcher for a given key or prefix.
 // It returns a WatchChan that streams WatchResponses containing events.
 func (c *Cache) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan {
-	select {
-	case <-c.ready:
-	case <-ctx.Done():
+	if err := c.WaitReady(ctx); err != nil {
 		emptyWatchChan := make(chan clientv3.WatchResponse)
 		close(emptyWatchChan)
 		return emptyWatchChan
@@ -162,23 +148,50 @@ func (c *Cache) Watch(ctx context.Context, key string, opts ...clientv3.OpOption
 	return responseChan
 }
 
-// Ready reports whether the cache has finished its initial load.
-func (c *Cache) Ready() bool {
-	select {
-	case <-c.ready:
-		return true
-	default:
-		return false
+func (c *Cache) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
+	if err := c.WaitReady(ctx); err != nil {
+		return nil, err
+	}
+	op := clientv3.OpGet(key, opts...)
+
+	if _, err := c.validateGet(key, op); err != nil {
+		return nil, err
 	}
+
+	startKey := []byte(key)
+	endKey := op.RangeBytes()
+	kvs, rev, err := c.store.Get(startKey, endKey)
+	if err != nil {
+		return nil, err
+	}
+
+	return &clientv3.GetResponse{
+		Header: &pb.ResponseHeader{Revision: rev},
+		Kvs:    kvs,
+		Count:  int64(len(kvs)),
+	}, nil
+}
+
+// Ready returns true if the snapshot has been loaded and the first watch has been confirmed.
+func (c *Cache) Ready() bool {
+	return c.ready.Ready()
 }
 
 // WaitReady blocks until the cache is ready or the ctx is cancelled.
 func (c *Cache) WaitReady(ctx context.Context) error {
-	select {
-	case <-c.ready:
-		return nil
-	case <-ctx.Done():
-		return ctx.Err()
+	return c.ready.WaitReady(ctx)
+}
+
+func (c *Cache) WaitForRevision(ctx context.Context, rev int64) error {
+	for {
+		if c.store.LatestRev() >= rev {
+			return nil
+		}
+		select {
+		case <-time.After(10 * time.Millisecond):
+		case <-ctx.Done():
+			return ctx.Err()
+		}
 	}
 }
 
@@ -188,70 +201,91 @@ func (c *Cache) Close() {
 	c.waitGroup.Wait()
 }
 
-func serveWatchEvents(ctx context.Context, watchCtx *watchCtx) {
-	backoff := watchCtx.backoffStart
+func (c *Cache) getWatchLoop(ctx context.Context) {
+	cfg := defaultConfig()
+	backoff := cfg.InitialBackoff
 	for {
-		opts := []clientv3.OpOption{
-			clientv3.WithPrefix(),
-			clientv3.WithProgressNotify(),
-			clientv3.WithCreatedNotify(),
-		}
-		if oldestRev := watchCtx.cache.demux.PeekOldest(); oldestRev != 0 {
-			opts = append(opts,
-				clientv3.WithRev(oldestRev+1))
-		}
-		watchCh := watchCtx.cache.watcher.Watch(ctx, watchCtx.cache.prefix, opts...)
-
-		if err := readWatchChannel(watchCh, watchCtx.cache, watchCtx.cache.demux, watchCtx.onFirstResponse); err == nil {
+		if err := ctx.Err(); err != nil {
 			return
 		}
-
+		if err := c.getWatch(ctx); err != nil {
+			fmt.Printf("getWatch failed, will retry after %v: %v\n", backoff, err)
+		}
 		select {
 		case <-ctx.Done():
 			return
 		case <-time.After(backoff):
 		}
-		if backoff < watchCtx.backoffMax {
-			backoff *= 2
-		}
 	}
 }
 
-// readWatchChannel reads an etcd Watch stream into History and enqueueCh, returning nil on cancel or first error.
-func readWatchChannel(
-	watchChan clientv3.WatchChan,
-	cache *Cache,
-	demux *demux,
-	onFirstResponse func(),
-) error {
-	for resp := range watchChan {
-		onFirstResponse()
-
-		if err := resp.Err(); err != nil {
-			if errors.Is(err, rpctypes.ErrCompacted) {
-				select {
-				case <-cache.ready:
-					// TODO: Reinitialize cache.ready safely; current direct channel assignment can race with concurrent watchers
-					cache.ready = make(chan struct{})
-				default:
-				}
-				demux.Purge()
+func (c *Cache) getWatch(ctx context.Context) error {
+	getResp, err := c.get(ctx)
+	if err != nil {
+		return err
+	}
+	return c.watch(ctx, getResp.Header.Revision+1)
+}
+
+func (c *Cache) get(ctx context.Context) (*clientv3.GetResponse, error) {
+	resp, err := c.kv.Get(ctx, c.prefix, clientv3.WithPrefix())
+	if err != nil {
+		return nil, err
+	}
+	c.store.Restore(resp.Kvs, resp.Header.Revision)
+	return resp, nil
+}
+
+func (c *Cache) watch(ctx context.Context, rev int64) error {
+	readyOnce := sync.Once{}
+	for {
+		watchCh := c.watcher.Watch(
+			ctx,
+			c.prefix,
+			clientv3.WithPrefix(),
+			clientv3.WithRev(rev),
+			clientv3.WithProgressNotify(),
+			clientv3.WithCreatedNotify(),
+		)
+
+		for resp := range watchCh {
+			readyOnce.Do(func() { c.ready.Set() })
+			if err := resp.Err(); err != nil {
+				c.ready.Reset()
+				c.demux.Purge()
+				c.store.Reset()
+				return err
 			}
-			return err
+
+			if err := c.store.Apply(resp.Events); err != nil {
+				c.ready.Reset()
+				c.demux.Purge()
+				c.store.Reset()
+				return err
+			}
+			c.demux.Broadcast(resp.Events)
+		}
+
+		if ctx.Err() != nil {
+			return ctx.Err()
 		}
-		demux.Broadcast(resp.Events)
 	}
-	return nil
 }
 
 func (c *Cache) validateWatch(key string, op clientv3.Op) (pred KeyPredicate, err error) {
-	if op.IsPrevKV() ||
-		op.IsFragment() ||
-		op.IsProgressNotify() ||
-		op.IsCreatedNotify() ||
-		op.IsFilterPut() ||
-		op.IsFilterDelete() {
-		return nil, ErrUnsupportedRequest
+	switch {
+	case op.IsPrevKV():
+		return nil, fmt.Errorf("%w: PrevKV not supported", ErrUnsupportedRequest)
+	case op.IsFragment():
+		return nil, fmt.Errorf("%w: Fragment not supported", ErrUnsupportedRequest)
+	case op.IsProgressNotify():
+		return nil, fmt.Errorf("%w: ProgressNotify not supported", ErrUnsupportedRequest)
+	case op.IsCreatedNotify():
+		return nil, fmt.Errorf("%w: CreatedNotify not supported", ErrUnsupportedRequest)
+	case op.IsFilterPut():
+		return nil, fmt.Errorf("%w: FilterPut not supported", ErrUnsupportedRequest)
+	case op.IsFilterDelete():
+		return nil, fmt.Errorf("%w: FilterDelete not supported", ErrUnsupportedRequest)
 	}
 
 	startKey := []byte(key)
@@ -263,6 +297,41 @@ func (c *Cache) validateWatch(key string, op clientv3.Op) (pred KeyPredicate, er
 	return KeyPredForRange(startKey, endKey), nil
 }
 
+func (c *Cache) validateGet(key string, op clientv3.Op) (KeyPredicate, error) {
+	switch {
+	case op.IsCountOnly():
+		return nil, fmt.Errorf("%w: CountOnly not supported", ErrUnsupportedRequest)
+	case op.IsPrevKV():
+		return nil, fmt.Errorf("%w: PrevKV not supported", ErrUnsupportedRequest)
+	case op.IsSortSet():
+		return nil, fmt.Errorf("%w: SortSet not supported", ErrUnsupportedRequest)
+	case op.Limit() != 0:
+		return nil, fmt.Errorf("%w: Limit(%d) not supported", ErrUnsupportedRequest, op.Limit())
+	case op.MinModRev() != 0:
+		return nil, fmt.Errorf("%w: MinModRev(%d) not supported", ErrUnsupportedRequest, op.MinModRev())
+	case op.MaxModRev() != 0:
+		return nil, fmt.Errorf("%w: MaxModRev(%d) not supported", ErrUnsupportedRequest, op.MaxModRev())
+	case op.MinCreateRev() != 0:
+		return nil, fmt.Errorf("%w: MinCreateRev(%d) not supported", ErrUnsupportedRequest, op.MinCreateRev())
+	case op.MaxCreateRev() != 0:
+		return nil, fmt.Errorf("%w: MaxCreateRev(%d) not supported", ErrUnsupportedRequest, op.MaxCreateRev())
+	// cache now only serves serializable reads of the latest revision (rev == 0).
+	case op.Rev() != 0:
+		return nil, fmt.Errorf("%w: Rev(%d) not supported", ErrUnsupportedRequest, op.Rev())
+	case !op.IsSerializable():
+		return nil, fmt.Errorf("%w: non-serializable request", ErrUnsupportedRequest)
+	}
+
+	startKey := []byte(key)
+	endKey := op.RangeBytes()
+
+	if err := c.validateRange(startKey, endKey); err != nil {
+		return nil, err
+	}
+
+	return KeyPredForRange(startKey, endKey), nil
+}
+
 func (c *Cache) validateRange(startKey, endKey []byte) error {
 	prefixStart := []byte(c.prefix)
 	prefixEnd := []byte(clientv3.GetPrefixRangeEnd(c.prefix))
diff --git a/cache/config.go b/cache/config.go
index ed0af99f4..b51a52a70 100644
--- a/cache/config.go
+++ b/cache/config.go
@@ -29,6 +29,8 @@ type Config struct {
 	InitialBackoff time.Duration
 	// MaxBackoff caps the exponential back-off between successive upstream watch retries.
 	MaxBackoff time.Duration
+	// GetTimeout is the timeout applied to the first Get() used to bootstrap the cache.
+	GetTimeout time.Duration
 }
 
 // TODO: tune via performance/load tests.
@@ -39,6 +41,7 @@ func defaultConfig() Config {
 		ResyncInterval:       50 * time.Millisecond,
 		InitialBackoff:       50 * time.Millisecond,
 		MaxBackoff:           2 * time.Second,
+		GetTimeout:           5 * time.Second,
 	}
 }
 
@@ -63,3 +66,7 @@ func WithInitialBackoff(d time.Duration) Option {
 func WithMaxBackoff(d time.Duration) Option {
 	return func(c *Config) { c.MaxBackoff = d }
 }
+
+func WithGetTimeout(d time.Duration) Option {
+	return func(c *Config) { c.GetTimeout = d }
+}
diff --git a/cache/go.mod b/cache/go.mod
index 6ec513ea3..c6db5c4d4 100644
--- a/cache/go.mod
+++ b/cache/go.mod
@@ -13,7 +13,6 @@ require (
 require (
 	github.com/coreos/go-semver v0.3.1 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
-	github.com/go-logr/logr v1.4.3 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
@@ -23,9 +22,9 @@ require (
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/grpc v1.73.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/grpc v1.74.2 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 )
 
diff --git a/cache/go.sum b/cache/go.sum
index 96db706a2..6260175ed 100644
--- a/cache/go.sum
+++ b/cache/go.sum
@@ -33,14 +33,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/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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
-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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -94,12 +94,12 @@ 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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
diff --git a/cache/ready.go b/cache/ready.go
new file mode 100644
index 000000000..96a1c6883
--- /dev/null
+++ b/cache/ready.go
@@ -0,0 +1,73 @@
+// 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 cache
+
+import (
+	"context"
+	"sync"
+)
+
+// ready tracks the transition from “not ready” → “ready” and allows waiters to block
+// until the first transition has happened, and to be reset on error.
+type ready struct {
+	mu        sync.Mutex
+	isReady   bool
+	readyChan chan struct{} // closed exactly once per ready transition
+}
+
+func newReady() *ready {
+	return &ready{readyChan: make(chan struct{})}
+}
+
+func (r *ready) Ready() bool {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	return r.isReady
+}
+
+func (r *ready) WaitReady(ctx context.Context) error {
+	r.mu.Lock()
+	ch := r.readyChan
+	ready := r.isReady
+	r.mu.Unlock()
+
+	if ready {
+		return nil
+	}
+	select {
+	case <-ch:
+		return ctx.Err()
+	case <-ctx.Done():
+		return ctx.Err()
+	}
+}
+
+func (r *ready) Set() {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if !r.isReady {
+		r.isReady = true
+		close(r.readyChan)
+	}
+}
+
+func (r *ready) Reset() {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if r.isReady {
+		r.isReady = false
+		r.readyChan = make(chan struct{})
+	}
+}
diff --git a/cache/store.go b/cache/store.go
new file mode 100644
index 000000000..1bea4a1f1
--- /dev/null
+++ b/cache/store.go
@@ -0,0 +1,145 @@
+// 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 cache
+
+import (
+	"bytes"
+	"fmt"
+	"sort"
+	"sync"
+
+	"go.etcd.io/etcd/api/v3/mvccpb"
+	clientv3 "go.etcd.io/etcd/client/v3"
+)
+
+var ErrNotReady = fmt.Errorf("cache: store not ready")
+
+type store struct {
+	mu        sync.RWMutex
+	kvs       map[string]*mvccpb.KeyValue
+	latestRev int64
+}
+
+func newStore() *store {
+	return &store{kvs: make(map[string]*mvccpb.KeyValue)}
+}
+
+func (s *store) Get(startKey, endKey []byte) ([]*mvccpb.KeyValue, int64, error) {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	if s.latestRev == 0 {
+		return nil, 0, ErrNotReady
+	}
+
+	var out []*mvccpb.KeyValue
+	switch {
+	case len(endKey) == 0:
+		out = s.getSingle(startKey)
+	case isPrefixScan(endKey):
+		out = s.scanPrefix(startKey)
+	default:
+		out = s.scanRange(startKey, endKey)
+	}
+
+	sort.Slice(out, func(i, j int) bool {
+		return bytes.Compare(out[i].Key, out[j].Key) < 0 // default: lexicographical, ascending‐by‐key sort
+	})
+	return out, s.latestRev, nil
+}
+
+func (s *store) Restore(kvs []*mvccpb.KeyValue, rev int64) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.kvs = make(map[string]*mvccpb.KeyValue, len(kvs))
+	for _, kv := range kvs {
+		s.kvs[string(kv.Key)] = kv
+	}
+	s.latestRev = rev
+}
+
+// Reset purges all in-memory state when the upstream watch stream reports compaction.
+func (s *store) Reset() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.kvs = make(map[string]*mvccpb.KeyValue)
+	s.latestRev = 0
+}
+
+func (s *store) Apply(events []*clientv3.Event) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	for _, ev := range events {
+		if ev.Kv.ModRevision < s.latestRev {
+			return fmt.Errorf("cache: stale event batch (rev %d < latest %d)", ev.Kv.ModRevision, s.latestRev)
+		}
+	}
+
+	for _, ev := range events {
+		switch ev.Type {
+		case clientv3.EventTypeDelete:
+			delete(s.kvs, string(ev.Kv.Key))
+		case clientv3.EventTypePut:
+			s.kvs[string(ev.Kv.Key)] = ev.Kv
+		}
+		if ev.Kv.ModRevision > s.latestRev {
+			s.latestRev = ev.Kv.ModRevision
+		}
+	}
+	return nil
+}
+
+func (s *store) LatestRev() int64 {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+	return s.latestRev
+}
+
+// getSingle fetches one key or nil
+func (s *store) getSingle(key []byte) []*mvccpb.KeyValue {
+	if kv, ok := s.kvs[string(key)]; ok {
+		return []*mvccpb.KeyValue{kv}
+	}
+	return nil
+}
+
+// scanPrefix returns all keys >= startKey
+func (s *store) scanPrefix(startKey []byte) []*mvccpb.KeyValue {
+	var res []*mvccpb.KeyValue
+	for _, kv := range s.kvs {
+		if bytes.Compare(kv.Key, startKey) >= 0 {
+			res = append(res, kv)
+		}
+	}
+	return res
+}
+
+// scanRange returns all keys in [startKey, endKey)
+func (s *store) scanRange(startKey, endKey []byte) []*mvccpb.KeyValue {
+	var res []*mvccpb.KeyValue
+	for _, kv := range s.kvs {
+		if bytes.Compare(kv.Key, startKey) >= 0 && bytes.Compare(kv.Key, endKey) < 0 {
+			res = append(res, kv)
+		}
+	}
+	return res
+}
+
+// isPrefixScan detects endKey=={0} semantics
+func isPrefixScan(endKey []byte) bool {
+	return len(endKey) == 1 && endKey[0] == 0
+}
diff --git a/client/v3/go.mod b/client/v3/go.mod
index b2ac102aa..e5c801a92 100644
--- a/client/v3/go.mod
+++ b/client/v3/go.mod
@@ -8,13 +8,13 @@ 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.1.0
-	github.com/prometheus/client_golang v1.22.0
+	github.com/prometheus/client_golang v1.23.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.73.0
-	sigs.k8s.io/yaml v1.5.0
+	google.golang.org/grpc v1.74.2
+	sigs.k8s.io/yaml v1.6.0
 )
 
 require (
@@ -30,7 +30,7 @@ require (
 	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.65.0 // indirect
-	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
 	go.opentelemetry.io/otel/metric v1.37.0 // indirect
 	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
 	go.opentelemetry.io/otel/trace v1.37.0 // indirect
@@ -39,8 +39,8 @@ require (
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // 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 0ac3c115e..dd832364a 100644
--- a/client/v3/go.sum
+++ b/client/v3/go.sum
@@ -43,14 +43,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/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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
-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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 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=
@@ -110,12 +110,12 @@ 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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
@@ -123,5 +123,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/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
-sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/etcdctl/ctlv3/command/alarm_command.go b/etcdctl/ctlv3/command/alarm_command.go
index 679f9d98f..9bf6ffbda 100644
--- a/etcdctl/ctlv3/command/alarm_command.go
+++ b/etcdctl/ctlv3/command/alarm_command.go
@@ -26,8 +26,10 @@ import (
 // NewAlarmCommand returns the cobra command for "alarm".
 func NewAlarmCommand() *cobra.Command {
 	ac := &cobra.Command{
-		Use:   "alarm <subcommand>",
-		Short: "Alarm related commands",
+		Use:     "alarm <subcommand>",
+		Short:   "Alarm related commands. Use `etcdctl alarm --help` to see subcommands",
+		Long:    "Alarm related commands",
+		GroupID: groupClusterMaintenanceID,
 	}
 
 	ac.AddCommand(NewAlarmDisarmCommand())
diff --git a/etcdctl/ctlv3/command/auth_command.go b/etcdctl/ctlv3/command/auth_command.go
index 13ccda44d..6a414f7aa 100644
--- a/etcdctl/ctlv3/command/auth_command.go
+++ b/etcdctl/ctlv3/command/auth_command.go
@@ -27,8 +27,10 @@ import (
 // NewAuthCommand returns the cobra command for "auth".
 func NewAuthCommand() *cobra.Command {
 	ac := &cobra.Command{
-		Use:   "auth <enable or disable>",
-		Short: "Enable or disable authentication",
+		Use:     "auth <enable or disable>",
+		Short:   "Enable or disable authentication. Use `etcdctl auth --help` to see subcommands",
+		Long:    "Enable or disable authentication",
+		GroupID: groupAuthenticationID,
 	}
 
 	ac.AddCommand(newAuthEnableCommand())
diff --git a/etcdctl/ctlv3/command/check.go b/etcdctl/ctlv3/command/check.go
index c88015400..904120982 100644
--- a/etcdctl/ctlv3/command/check.go
+++ b/etcdctl/ctlv3/command/check.go
@@ -107,8 +107,10 @@ var checkDatascaleCfgMap = map[string]checkDatascaleCfg{
 // NewCheckCommand returns the cobra command for "check".
 func NewCheckCommand() *cobra.Command {
 	cc := &cobra.Command{
-		Use:   "check <subcommand>",
-		Short: "commands for checking properties of the etcd cluster",
+		Use:     "check <subcommand>",
+		Short:   "commands for checking properties of the etcd cluster. Use `etcdctl check --help` to see subcommands",
+		Long:    "commands for checking properties of the etcd cluster",
+		GroupID: groupUtilityID,
 	}
 
 	cc.AddCommand(NewCheckPerfCommand())
diff --git a/etcdctl/ctlv3/command/compaction_command.go b/etcdctl/ctlv3/command/compaction_command.go
index 5c0bb1019..457538a59 100644
--- a/etcdctl/ctlv3/command/compaction_command.go
+++ b/etcdctl/ctlv3/command/compaction_command.go
@@ -29,9 +29,10 @@ var compactPhysical bool
 // NewCompactionCommand returns the cobra command for "compaction".
 func NewCompactionCommand() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "compaction [options] <revision>",
-		Short: "Compacts the event history in etcd",
-		Run:   compactionCommandFunc,
+		Use:     "compaction [options] <revision>",
+		Short:   "Compacts the event history in etcd",
+		Run:     compactionCommandFunc,
+		GroupID: groupKVID,
 	}
 	cmd.Flags().BoolVar(&compactPhysical, "physical", false, "'true' to wait for compaction to physically remove all old revisions")
 	return cmd
diff --git a/etcdctl/ctlv3/command/completion_command.go b/etcdctl/ctlv3/command/completion_command.go
index cb98d4a7d..45f284426 100644
--- a/etcdctl/ctlv3/command/completion_command.go
+++ b/etcdctl/ctlv3/command/completion_command.go
@@ -78,6 +78,7 @@ PowerShell:
 				cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
 			}
 		},
+		GroupID: groupUtilityID,
 	}
 
 	return cmd
diff --git a/etcdctl/ctlv3/command/defrag_command.go b/etcdctl/ctlv3/command/defrag_command.go
index 36116d488..c68de9cd1 100644
--- a/etcdctl/ctlv3/command/defrag_command.go
+++ b/etcdctl/ctlv3/command/defrag_command.go
@@ -27,9 +27,10 @@ import (
 // NewDefragCommand returns the cobra command for "Defrag".
 func NewDefragCommand() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "defrag",
-		Short: "Defragments the storage of the etcd members with given endpoints",
-		Run:   defragCommandFunc,
+		Use:     "defrag",
+		Short:   "Defragments the storage of the etcd members with given endpoints",
+		Run:     defragCommandFunc,
+		GroupID: groupClusterMaintenanceID,
 	}
 	cmd.PersistentFlags().BoolVar(&epClusterEndpoints, "cluster", false, "use all endpoints from the cluster member list")
 	return cmd
diff --git a/etcdctl/ctlv3/command/del_command.go b/etcdctl/ctlv3/command/del_command.go
index 51b7abb3e..5fa39f807 100644
--- a/etcdctl/ctlv3/command/del_command.go
+++ b/etcdctl/ctlv3/command/del_command.go
@@ -35,9 +35,10 @@ var (
 // NewDelCommand returns the cobra command for "del".
 func NewDelCommand() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "del [options] <key> [range_end]",
-		Short: "Removes the specified key or range of keys [key, range_end)",
-		Run:   delCommandFunc,
+		Use:     "del [options] <key> [range_end]",
+		Short:   "Removes the specified key or range of keys [key, range_end)",
+		Run:     delCommandFunc,
+		GroupID: groupKVID,
 	}
 
 	cmd.Flags().BoolVar(&delPrefix, "prefix", false, "delete keys with matching prefix")
diff --git a/etcdctl/ctlv3/command/downgrade_command.go b/etcdctl/ctlv3/command/downgrade_command.go
index 8b6ab9cd1..c56b7749c 100644
--- a/etcdctl/ctlv3/command/downgrade_command.go
+++ b/etcdctl/ctlv3/command/downgrade_command.go
@@ -26,8 +26,10 @@ import (
 // NewDowngradeCommand returns the cobra command for "downgrade".
 func NewDowngradeCommand() *cobra.Command {
 	dc := &cobra.Command{
-		Use:   "downgrade <TARGET_VERSION>",
-		Short: "Downgrade related commands",
+		Use:     "downgrade <TARGET_VERSION>",
+		Short:   "Downgrade related commands. Use `etcdctl downgrade --help` to see subcommands",
+		Long:    "Downgrade related commands",
+		GroupID: groupClusterMaintenanceID,
 	}
 
 	dc.AddCommand(NewDowngradeValidateCommand())
diff --git a/etcdctl/ctlv3/command/elect_command.go b/etcdctl/ctlv3/command/elect_command.go
index eee4289e7..6ff828843 100644
--- a/etcdctl/ctlv3/command/elect_command.go
+++ b/etcdctl/ctlv3/command/elect_command.go
@@ -33,9 +33,10 @@ var electListen bool
 // NewElectCommand returns the cobra command for "elect".
 func NewElectCommand() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "elect <election-name> [proposal]",
-		Short: "Observes and participates in leader election",
-		Run:   electCommandFunc,
+		Use:     "elect <election-name> [proposal]",
+		Short:   "Observes and participates in leader election",
+		Run:     electCommandFunc,
+		GroupID: groupConcurrencyID,
 	}
 	cmd.Flags().BoolVarP(&electListen, "listen", "l", false, "observation mode")
 	return cmd
diff --git a/etcdctl/ctlv3/command/ep_command.go b/etcdctl/ctlv3/command/ep_command.go
index d82194d32..e074a6218 100644
--- a/etcdctl/ctlv3/command/ep_command.go
+++ b/etcdctl/ctlv3/command/ep_command.go
@@ -39,8 +39,10 @@ var (
 // NewEndpointCommand returns the cobra command for "endpoint".
 func NewEndpointCommand() *cobra.Command {
 	ec := &cobra.Command{
-		Use:   "endpoint <subcommand>",
-		Short: "Endpoint related commands",
+		Use:     "endpoint <subcommand>",
+		Short:   "Endpoint related commands. Use `etcdctl endpoint --help` to see subcommands",
+		Long:    "Endpoint related commands",
+		GroupID: groupClusterMaintenanceID,
 	}
 
 	ec.PersistentFlags().BoolVar(&epClusterEndpoints, "cluster", false, "use all endpoints from the cluster member list")
diff --git a/etcdctl/ctlv3/command/get_command.go b/etcdctl/ctlv3/command/get_command.go
index 7d687aa5c..94d34dac0 100644
--- a/etcdctl/ctlv3/command/get_command.go
+++ b/etcdctl/ctlv3/command/get_command.go
@@ -44,9 +44,10 @@ var (
 // NewGetCommand returns the cobra command for "get".
 func NewGetCommand() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "get [options] <key> [range_end]",
-		Short: "Gets the key or a range of keys",
-		Run:   getCommandFunc,
+		Use:     "get [options] <key> [range_end]",
+		Short:   "Gets the key or a range of keys",
+		Run:     getCommandFunc,
+		GroupID: groupKVID,
 	}
 
 	cmd.Flags().StringVar(&getConsistency, "consistency", "l", "Linearizable(l) or Serializable(s)")
diff --git a/etcdctl/ctlv3/command/groups.go b/etcdctl/ctlv3/command/groups.go
new file mode 100644
index 000000000..ca0229d3c
--- /dev/null
+++ b/etcdctl/ctlv3/command/groups.go
@@ -0,0 +1,60 @@
+// 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 command
+
+import "github.com/spf13/cobra"
+
+const (
+	groupKVID                 = "kv"
+	groupClusterMaintenanceID = "cluster maintenance"
+	groupConcurrencyID        = "concurrency"
+	groupAuthenticationID     = "authentication"
+	groupUtilityID            = "utility"
+)
+
+func NewKVGroup() *cobra.Group {
+	return &cobra.Group{
+		ID:    groupKVID,
+		Title: "Key-value commands",
+	}
+}
+
+func NewClusterMaintenanceGroup() *cobra.Group {
+	return &cobra.Group{
+		ID:    groupClusterMaintenanceID,
+		Title: "Cluster maintenance commands",
+	}
+}
+
+func NewConcurrencyGroup() *cobra.Group {
+	return &cobra.Group{
+		ID:    groupConcurrencyID,
+		Title: "Concurrency commands",
+	}
+}
+
+func NewAuthenticationGroup() *cobra.Group {
+	return &cobra.Group{
+		ID:    groupAuthenticationID,
+		Title: "Authentication commands",
+	}
+}
+
+func NewUtilityGroup() *cobra.Group {
+	return &cobra.Group{
+		ID:    groupUtilityID,
+		Title: "Utility commands",
+	}
+}
diff --git a/etcdctl/ctlv3/command/help_command.go b/etcdctl/ctlv3/command/help_command.go
new file mode 100644
index 000000000..259069791
--- /dev/null
+++ b/etcdctl/ctlv3/command/help_command.go
@@ -0,0 +1,21 @@
+// 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 command
+
+import "github.com/spf13/cobra"
+
+func SetHelpCmdGroup(rootCmd *cobra.Command) {
+	rootCmd.SetHelpCommandGroupID(groupUtilityID)
+}
diff --git a/etcdctl/ctlv3/command/lease_command.go b/etcdctl/ctlv3/command/lease_command.go
index 8e70670b6..0fccc5f64 100644
--- a/etcdctl/ctlv3/command/lease_command.go
+++ b/etcdctl/ctlv3/command/lease_command.go
@@ -28,8 +28,10 @@ import (
 // NewLeaseCommand returns the cobra command for "lease".
 func NewLeaseCommand() *cobra.Command {
 	lc := &cobra.Command{
-		Use:   "lease <subcommand>",
-		Short: "Lease related commands",
+		Use:     "lease <subcommand>",
+		Short:   "Lease related commands. Use `etcdctl lease --help` to see subcommands",
+		Long:    "Lease related commands",
+		GroupID: groupKVID,
 	}
 
 	lc.AddCommand(NewLeaseGrantCommand())
diff --git a/etcdctl/ctlv3/command/lock_command.go b/etcdctl/ctlv3/command/lock_command.go
index 64fb0ee81..0faa76fe6 100644
--- a/etcdctl/ctlv3/command/lock_command.go
+++ b/etcdctl/ctlv3/command/lock_command.go
@@ -35,9 +35,10 @@ var lockTTL = 10
 // NewLockCommand returns the cobra command for "lock".
 func NewLockCommand() *cobra.Command {
 	c := &cobra.Command{
-		Use:   "lock <lockname> [exec-command arg1 arg2 ...]",
-		Short: "Acquires a named lock",
-		Run:   lockCommandFunc,
+		Use:     "lock <lockname> [exec-command arg1 arg2 ...]",
+		Short:   "Acquires a named lock",
+		Run:     lockCommandFunc,
+		GroupID: groupConcurrencyID,
 	}
 	c.Flags().IntVarP(&lockTTL, "ttl", "", lockTTL, "timeout for session")
 	return c
diff --git a/etcdctl/ctlv3/command/make_mirror_command.go b/etcdctl/ctlv3/command/make_mirror_command.go
index c8b6220d5..eea3d293e 100644
--- a/etcdctl/ctlv3/command/make_mirror_command.go
+++ b/etcdctl/ctlv3/command/make_mirror_command.go
@@ -53,9 +53,10 @@ var (
 // NewMakeMirrorCommand returns the cobra command for "makeMirror".
 func NewMakeMirrorCommand() *cobra.Command {
 	c := &cobra.Command{
-		Use:   "make-mirror [options] <destination>",
-		Short: "Makes a mirror at the destination etcd cluster",
-		Run:   makeMirrorCommandFunc,
+		Use:     "make-mirror [options] <destination>",
+		Short:   "Makes a mirror at the destination etcd cluster",
+		Run:     makeMirrorCommandFunc,
+		GroupID: groupUtilityID,
 	}
 
 	c.Flags().StringVar(&mmprefix, "prefix", "", "Key-value prefix to mirror")
diff --git a/etcdctl/ctlv3/command/member_command.go b/etcdctl/ctlv3/command/member_command.go
index 256bf2b9c..0ead08f43 100644
--- a/etcdctl/ctlv3/command/member_command.go
+++ b/etcdctl/ctlv3/command/member_command.go
@@ -35,8 +35,10 @@ var (
 // NewMemberCommand returns the cobra command for "member".
 func NewMemberCommand() *cobra.Command {
 	mc := &cobra.Command{
-		Use:   "member <subcommand>",
-		Short: "Membership related commands",
+		Use:     "member <subcommand>",
+		Short:   "Membership related commands. Use `etcdctl member --help` to see subcommands",
+		Long:    "Membership related commands",
+		GroupID: groupClusterMaintenanceID,
 	}
 
 	mc.AddCommand(NewMemberAddCommand())
diff --git a/etcdctl/ctlv3/command/move_leader_command.go b/etcdctl/ctlv3/command/move_leader_command.go
index a7b4f397b..320d9ff39 100644
--- a/etcdctl/ctlv3/command/move_leader_command.go
+++ b/etcdctl/ctlv3/command/move_leader_command.go
@@ -27,9 +27,10 @@ import (
 // NewMoveLeaderCommand returns the cobra command for "move-leader".
 func NewMoveLeaderCommand() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "move-leader <transferee-member-id>",
-		Short: "Transfers leadership to another etcd cluster member.",
-		Run:   transferLeadershipCommandFunc,
+		Use:     "move-leader <transferee-member-id>",
+		Short:   "Transfers leadership to another etcd cluster member.",
+		Run:     transferLeadershipCommandFunc,
+		GroupID: groupClusterMaintenanceID,
 	}
 	return cmd
 }
diff --git a/etcdctl/ctlv3/command/printer_json.go b/etcdctl/ctlv3/command/printer_json.go
index be83433f7..4e6f9bf71 100644
--- a/etcdctl/ctlv3/command/printer_json.go
+++ b/etcdctl/ctlv3/command/printer_json.go
@@ -73,9 +73,11 @@ func (p *jsonPrinter) EndpointHealth(r []epHealth) { printJSON(r) }
 func (p *jsonPrinter) EndpointStatus(r []epStatus) { printJSON(r) }
 func (p *jsonPrinter) EndpointHashKV(r []epHashKV) { printJSON(r) }
 
-func (p *jsonPrinter) MemberAdd(r clientv3.MemberAddResponse)                 { p.printJSON(r) }
-func (p *jsonPrinter) MemberRemove(_ uint64, r clientv3.MemberRemoveResponse) { p.printJSON(r) }
-func (p *jsonPrinter) MemberList(r clientv3.MemberListResponse)               { p.printJSON(r) }
+func (p *jsonPrinter) MemberAdd(r clientv3.MemberAddResponse)                   { p.printJSON(r) }
+func (p *jsonPrinter) MemberRemove(_ uint64, r clientv3.MemberRemoveResponse)   { p.printJSON(r) }
+func (p *jsonPrinter) MemberUpdate(_ uint64, r clientv3.MemberUpdateResponse)   { p.printJSON(r) }
+func (p *jsonPrinter) MemberPromote(_ uint64, r clientv3.MemberPromoteResponse) { p.printJSON(r) }
+func (p *jsonPrinter) MemberList(r clientv3.MemberListResponse)                 { p.printJSON(r) }
 
 func printJSONTo(w io.Writer, v any) {
 	b, err := json.Marshal(v)
@@ -117,7 +119,30 @@ func (p *jsonPrinter) printJSON(v any) {
 
 		data = &struct {
 			Header  *HexResponseHeader `json:"header"`
-			Member  *HexMember         `json:"member"`
+			Members []*HexMember       `json:"members"`
+			*Alias
+		}{
+			Header:  (*HexResponseHeader)(r.Header),
+			Members: toHexMembers(r.Members),
+			Alias:   (*Alias)(&r),
+		}
+	case clientv3.MemberUpdateResponse:
+		type Alias clientv3.MemberUpdateResponse
+
+		data = &struct {
+			Header  *HexResponseHeader `json:"header"`
+			Members []*HexMember       `json:"members"`
+			*Alias
+		}{
+			Header:  (*HexResponseHeader)(r.Header),
+			Members: toHexMembers(r.Members),
+			Alias:   (*Alias)(&r),
+		}
+	case clientv3.MemberPromoteResponse:
+		type Alias clientv3.MemberPromoteResponse
+
+		data = &struct {
+			Header  *HexResponseHeader `json:"header"`
 			Members []*HexMember       `json:"members"`
 			*Alias
 		}{
diff --git a/etcdctl/ctlv3/command/put_command.go b/etcdctl/ctlv3/command/put_command.go
index 4814db559..084815013 100644
--- a/etcdctl/ctlv3/command/put_command.go
+++ b/etcdctl/ctlv3/command/put_command.go
@@ -56,7 +56,8 @@ For example,
 $ cat file | put <key>
 will store the content of the file to <key>.
 `,
-		Run: putCommandFunc,
+		Run:     putCommandFunc,
+		GroupID: groupKVID,
 	}
 	cmd.Flags().StringVar(&leaseStr, "lease", "0", "lease ID (in hexadecimal) to attach to the key")
 	cmd.Flags().BoolVar(&putPrevKV, "prev-kv", false, "return the previous key-value pair before modification")
diff --git a/etcdctl/ctlv3/command/role_command.go b/etcdctl/ctlv3/command/role_command.go
index 705d88d88..5fd96ca1c 100644
--- a/etcdctl/ctlv3/command/role_command.go
+++ b/etcdctl/ctlv3/command/role_command.go
@@ -32,8 +32,10 @@ var (
 // NewRoleCommand returns the cobra command for "role".
 func NewRoleCommand() *cobra.Command {
 	ac := &cobra.Command{
-		Use:   "role <subcommand>",
-		Short: "Role related commands",
+		Use:     "role <subcommand>",
+		Short:   "Role related commands. Use `etcdctl role --help` to see subcommands",
+		Long:    "Role related commands",
+		GroupID: groupAuthenticationID,
 	}
 
 	ac.AddCommand(newRoleAddCommand())
diff --git a/etcdctl/ctlv3/command/snapshot_command.go b/etcdctl/ctlv3/command/snapshot_command.go
index 66934bede..d98c93b0b 100644
--- a/etcdctl/ctlv3/command/snapshot_command.go
+++ b/etcdctl/ctlv3/command/snapshot_command.go
@@ -52,6 +52,7 @@ func NewSnapshotCommand() *cobra.Command {
 		Use:     "snapshot <subcommand>",
 		Short:   "Manages etcd node snapshots",
 		Example: snapshotExample,
+		GroupID: groupClusterMaintenanceID,
 	}
 	cmd.AddCommand(NewSnapshotSaveCommand())
 	return cmd
diff --git a/etcdctl/ctlv3/command/txn_command.go b/etcdctl/ctlv3/command/txn_command.go
index 5433f82bd..df746a228 100644
--- a/etcdctl/ctlv3/command/txn_command.go
+++ b/etcdctl/ctlv3/command/txn_command.go
@@ -59,7 +59,8 @@ put key2 "some extra key"
 ---
 
 Refer to https://github.com/etcd-io/etcd/blob/main/etcdctl/README.md#txn-options.`,
-		Run: txnCommandFunc,
+		Run:     txnCommandFunc,
+		GroupID: groupKVID,
 	}
 	cmd.Flags().BoolVarP(&txnInteractive, "interactive", "i", false, "Input transaction in interactive mode")
 	return cmd
@@ -150,16 +151,19 @@ func parseRequestUnion(line string) (*clientv3.Op, error) {
 	opc := make(chan clientv3.Op, 1)
 
 	put := NewPutCommand()
+	put.GroupID = ""
 	put.Run = func(cmd *cobra.Command, args []string) {
 		key, value, opts := getPutOp(args)
 		opc <- clientv3.OpPut(key, value, opts...)
 	}
 	get := NewGetCommand()
+	get.GroupID = ""
 	get.Run = func(cmd *cobra.Command, args []string) {
 		key, opts := getGetOp(args)
 		opc <- clientv3.OpGet(key, opts...)
 	}
 	del := NewDelCommand()
+	del.GroupID = ""
 	del.Run = func(cmd *cobra.Command, args []string) {
 		key, opts := getDelOp(args)
 		opc <- clientv3.OpDelete(key, opts...)
diff --git a/etcdctl/ctlv3/command/user_command.go b/etcdctl/ctlv3/command/user_command.go
index f3e59a704..20e4d3e8c 100644
--- a/etcdctl/ctlv3/command/user_command.go
+++ b/etcdctl/ctlv3/command/user_command.go
@@ -31,8 +31,10 @@ var userShowDetail bool
 // NewUserCommand returns the cobra command for "user".
 func NewUserCommand() *cobra.Command {
 	ac := &cobra.Command{
-		Use:   "user <subcommand>",
-		Short: "User related commands",
+		Use:     "user <subcommand>",
+		Short:   "User related commands. Use `etcdctl user --help` to see subcommands",
+		Long:    "User related commands",
+		GroupID: groupAuthenticationID,
 	}
 
 	ac.AddCommand(newUserAddCommand())
diff --git a/etcdctl/ctlv3/command/version_command.go b/etcdctl/ctlv3/command/version_command.go
index e57567d6a..d5be27adb 100644
--- a/etcdctl/ctlv3/command/version_command.go
+++ b/etcdctl/ctlv3/command/version_command.go
@@ -25,9 +25,10 @@ import (
 // NewVersionCommand prints out the version of etcd.
 func NewVersionCommand() *cobra.Command {
 	return &cobra.Command{
-		Use:   "version",
-		Short: "Prints the version of etcdctl",
-		Run:   versionCommandFunc,
+		Use:     "version",
+		Short:   "Prints the version of etcdctl",
+		Run:     versionCommandFunc,
+		GroupID: groupUtilityID,
 	}
 }
 
diff --git a/etcdctl/ctlv3/command/watch_command.go b/etcdctl/ctlv3/command/watch_command.go
index fa8fc72d8..b8c8bdf13 100644
--- a/etcdctl/ctlv3/command/watch_command.go
+++ b/etcdctl/ctlv3/command/watch_command.go
@@ -47,9 +47,10 @@ var (
 // NewWatchCommand returns the cobra command for "watch".
 func NewWatchCommand() *cobra.Command {
 	cmd := &cobra.Command{
-		Use:   "watch [options] [key or prefix] [range_end] [--] [exec-command arg1 arg2 ...]",
-		Short: "Watches events stream on keys or prefixes",
-		Run:   watchCommandFunc,
+		Use:     "watch [options] [key or prefix] [range_end] [--] [exec-command arg1 arg2 ...]",
+		Short:   "Watches events stream on keys or prefixes",
+		Run:     watchCommandFunc,
+		GroupID: groupKVID,
 	}
 
 	cmd.Flags().BoolVarP(&watchInteractive, "interactive", "i", false, "Interactive mode")
diff --git a/etcdctl/ctlv3/ctl.go b/etcdctl/ctlv3/ctl.go
index e992fa4fa..0fc657071 100644
--- a/etcdctl/ctlv3/ctl.go
+++ b/etcdctl/ctlv3/ctl.go
@@ -21,9 +21,7 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"go.etcd.io/etcd/api/v3/version"
 	"go.etcd.io/etcd/etcdctl/v3/ctlv3/command"
-	"go.etcd.io/etcd/etcdctl/v3/util"
 	"go.etcd.io/etcd/pkg/v3/cobrautl"
 )
 
@@ -76,6 +74,14 @@ func init() {
 	rootCmd.PersistentFlags().StringVarP(&globalFlags.TLS.ServerName, "discovery-srv", "d", "", "domain name to query for SRV records describing cluster endpoints")
 	rootCmd.PersistentFlags().StringVarP(&globalFlags.DNSClusterServiceName, "discovery-srv-name", "", "", "service name to query when using DNS discovery")
 
+	rootCmd.AddGroup(
+		command.NewKVGroup(),
+		command.NewClusterMaintenanceGroup(),
+		command.NewConcurrencyGroup(),
+		command.NewAuthenticationGroup(),
+		command.NewUtilityGroup(),
+	)
+
 	rootCmd.AddCommand(
 		command.NewGetCommand(),
 		command.NewPutCommand(),
@@ -101,16 +107,11 @@ func init() {
 		command.NewCompletionCommand(),
 		command.NewDowngradeCommand(),
 	)
-}
 
-func usageFunc(c *cobra.Command) error {
-	return util.UsageFunc(c, version.Version, version.APIVersion)
+	command.SetHelpCmdGroup(rootCmd)
 }
 
 func Start() error {
-	rootCmd.SetUsageFunc(usageFunc)
-	// Make help just show the usage
-	rootCmd.SetHelpTemplate(`{{.UsageString}}`)
 	return rootCmd.Execute()
 }
 
diff --git a/etcdctl/go.mod b/etcdctl/go.mod
index 33bd503e9..a1b910d4f 100644
--- a/etcdctl/go.mod
+++ b/etcdctl/go.mod
@@ -8,9 +8,9 @@ require (
 	github.com/bgentry/speakeasy v0.2.0
 	github.com/cheggaaa/pb/v3 v3.1.7
 	github.com/dustin/go-humanize v1.0.1
-	github.com/olekukonko/tablewriter v1.0.8
+	github.com/olekukonko/tablewriter v1.0.9
 	github.com/spf13/cobra v1.9.1
-	github.com/spf13/pflag v1.0.6
+	github.com/spf13/pflag v1.0.7
 	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
@@ -18,7 +18,7 @@ require (
 	go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0
 	go.uber.org/zap v1.27.0
 	golang.org/x/time v0.12.0
-	google.golang.org/grpc v1.73.0
+	google.golang.org/grpc v1.74.2
 )
 
 require (
@@ -27,7 +27,6 @@ require (
 	github.com/coreos/go-systemd/v22 v22.5.0 // 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.3 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
@@ -35,16 +34,16 @@ 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/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
-	github.com/olekukonko/ll v0.0.8 // indirect
+	github.com/olekukonko/errors v1.1.0 // indirect
+	github.com/olekukonko/ll v0.0.9 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // 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 583aea7d3..cb4e4b58a 100644
--- a/etcdctl/go.sum
+++ b/etcdctl/go.sum
@@ -54,22 +54,22 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 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/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
-github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
-github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
-github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
-github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
+github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
+github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
+github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
 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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
-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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 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=
@@ -78,8 +78,9 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
 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=
-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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -136,12 +137,12 @@ 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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
diff --git a/etcdctl/util/help.go b/etcdctl/util/help.go
deleted file mode 100644
index 0e8035313..000000000
--- a/etcdctl/util/help.go
+++ /dev/null
@@ -1,180 +0,0 @@
-// 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.
-
-// copied from https://github.com/rkt/rkt/blob/master/rkt/help.go
-
-package util
-
-import (
-	"fmt"
-	"io"
-	"os"
-	"strings"
-	"text/tabwriter"
-	"text/template"
-
-	"github.com/spf13/cobra"
-	"github.com/spf13/pflag"
-)
-
-var (
-	commandUsageTemplate *template.Template
-	templFuncs           = template.FuncMap{
-		"descToLines": func(s string) []string {
-			// trim leading/trailing whitespace and split into slice of lines
-			return strings.Split(strings.Trim(s, "\n\t "), "\n")
-		},
-		"cmdName": func(cmd *cobra.Command, startCmd *cobra.Command) string {
-			parts := []string{cmd.Name()}
-			for cmd.HasParent() && cmd.Parent().Name() != startCmd.Name() {
-				cmd = cmd.Parent()
-				parts = append([]string{cmd.Name()}, parts...)
-			}
-			return strings.Join(parts, " ")
-		},
-		"indent": func(s string) string {
-			pad := strings.Repeat(" ", 2)
-			return pad + strings.Replace(s, "\n", "\n"+pad, -1)
-		},
-	}
-)
-
-func init() {
-	commandUsage := `
-{{ $cmd := .Cmd }}\
-{{ $cmdname := cmdName .Cmd .Cmd.Root }}\
-NAME:
-{{if not .Cmd.HasParent}}\
-{{printf "%s - %s" .Cmd.Name .Cmd.Short | indent}}
-{{else}}\
-{{printf "%s - %s" $cmdname .Cmd.Short | indent}}
-{{end}}\
-
-USAGE:
-{{printf "%s" .Cmd.UseLine | indent}}
-{{ if not .Cmd.HasParent }}\
-
-VERSION:
-{{printf "%s" .Version | indent}}
-{{end}}\
-{{if .Cmd.HasSubCommands}}\
-
-API VERSION:
-{{.APIVersion | indent}}
-{{end}}\
-{{if .Cmd.HasExample}}\
-
-Examples:
-{{.Cmd.Example}}
-{{end}}\
-{{if .Cmd.HasSubCommands}}\
-
-COMMANDS:
-{{range .SubCommands}}\
-{{ $cmdname := cmdName . $cmd }}\
-{{ if .Runnable }}\
-{{printf "%s\t%s" $cmdname .Short | indent}}
-{{end}}\
-{{end}}\
-{{end}}\
-{{ if .Cmd.Long }}\
-
-DESCRIPTION:
-{{range $line := descToLines .Cmd.Long}}{{printf "%s" $line | indent}}
-{{end}}\
-{{end}}\
-{{if .Cmd.HasLocalFlags}}\
-
-OPTIONS:
-{{.LocalFlags}}\
-{{end}}\
-{{if .Cmd.HasInheritedFlags}}\
-
-GLOBAL OPTIONS:
-{{.GlobalFlags}}\
-{{end}}
-`[1:]
-
-	commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(strings.ReplaceAll(commandUsage, "\\\n", "")))
-}
-
-func etcdFlagUsages(flagSet *pflag.FlagSet) string {
-	x := new(strings.Builder)
-
-	flagSet.VisitAll(func(flag *pflag.Flag) {
-		if len(flag.Deprecated) > 0 {
-			return
-		}
-		var format string
-		if len(flag.Shorthand) > 0 {
-			format = "  -%s, --%s"
-		} else {
-			format = "   %s   --%s"
-		}
-		if len(flag.NoOptDefVal) > 0 {
-			format = format + "["
-		}
-		if flag.Value.Type() == "string" {
-			// put quotes on the value
-			format = format + "=%q"
-		} else {
-			format = format + "=%s"
-		}
-		if len(flag.NoOptDefVal) > 0 {
-			format = format + "]"
-		}
-		format = format + "\t%s\n"
-		shorthand := flag.Shorthand
-		fmt.Fprintf(x, format, shorthand, flag.Name, flag.DefValue, flag.Usage)
-	})
-
-	return x.String()
-}
-
-func getSubCommands(cmd *cobra.Command) []*cobra.Command {
-	var subCommands []*cobra.Command
-	for _, subCmd := range cmd.Commands() {
-		subCommands = append(subCommands, subCmd)
-		subCommands = append(subCommands, getSubCommands(subCmd)...)
-	}
-	return subCommands
-}
-
-func UsageFunc(cmd *cobra.Command, version, APIVersion string) error {
-	subCommands := getSubCommands(cmd)
-	tabOut := getTabOutWithWriter(os.Stdout)
-	commandUsageTemplate.Execute(tabOut, struct {
-		Cmd         *cobra.Command
-		LocalFlags  string
-		GlobalFlags string
-		SubCommands []*cobra.Command
-		Version     string
-		APIVersion  string
-	}{
-		cmd,
-		etcdFlagUsages(cmd.LocalFlags()),
-		etcdFlagUsages(cmd.InheritedFlags()),
-		subCommands,
-		version,
-		APIVersion,
-	})
-	tabOut.Flush()
-	return nil
-}
-
-func getTabOutWithWriter(writer io.Writer) *tabwriter.Writer {
-	aTabOut := new(tabwriter.Writer)
-	aTabOut.Init(writer, 0, 8, 1, '\t', 0)
-	return aTabOut
-}
diff --git a/etcdutl/go.mod b/etcdutl/go.mod
index 3f36f3b1f..685b2f1f6 100644
--- a/etcdutl/go.mod
+++ b/etcdutl/go.mod
@@ -23,7 +23,7 @@ replace (
 require (
 	github.com/coreos/go-semver v0.3.1
 	github.com/dustin/go-humanize v1.0.1
-	github.com/olekukonko/tablewriter v1.0.8
+	github.com/olekukonko/tablewriter v1.0.9
 	github.com/spf13/cobra v1.9.1
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/bbolt v1.4.2
@@ -46,7 +46,7 @@ require (
 	github.com/go-logr/logr v1.4.3 // 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.2 // indirect
+	github.com/golang-jwt/jwt/v5 v5.3.0 // 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
@@ -61,17 +61,17 @@ require (
 	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/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
-	github.com/olekukonko/ll v0.0.8 // indirect
+	github.com/olekukonko/errors v1.1.0 // indirect
+	github.com/olekukonko/ll v0.0.9 // 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_golang v1.23.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/common v0.65.0 // indirect
-	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/soheilhy/cmux v0.1.5 // indirect
-	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/spf13/pflag v1.0.7 // indirect
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@@ -82,7 +82,7 @@ require (
 	go.opentelemetry.io/otel/metric v1.37.0 // indirect
 	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
 	go.opentelemetry.io/otel/trace v1.37.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.7.1 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/crypto v0.40.0 // indirect
@@ -90,11 +90,11 @@ require (
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
 	golang.org/x/time v0.12.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/grpc v1.73.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/grpc v1.74.2 // 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/yaml v1.5.0 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
 )
diff --git a/etcdutl/go.sum b/etcdutl/go.sum
index be8ddb8b4..22523a695 100644
--- a/etcdutl/go.sum
+++ b/etcdutl/go.sum
@@ -27,8 +27,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.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 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=
@@ -67,23 +67,23 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 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/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
-github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
-github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
-github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
-github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
+github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
+github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
+github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
 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/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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
-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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 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=
@@ -96,8 +96,9 @@ github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
 github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@@ -130,8 +131,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
 go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
 go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
 go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
-go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
-go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
 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=
@@ -182,12 +183,12 @@ 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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
@@ -198,5 +199,5 @@ 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/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
-sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/go.mod b/go.mod
index 792e09864..1ad0cc3a6 100644
--- a/go.mod
+++ b/go.mod
@@ -35,7 +35,7 @@ require (
 	go.etcd.io/raft/v3 v3.6.0
 	go.uber.org/zap v1.27.0
 	golang.org/x/time v0.12.0
-	google.golang.org/grpc v1.73.0
+	google.golang.org/grpc v1.74.2
 	google.golang.org/protobuf v1.36.6
 )
 
@@ -50,7 +50,7 @@ require (
 	github.com/go-logr/logr v1.4.3 // 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.2 // indirect
+	github.com/golang-jwt/jwt/v5 v5.3.0 // 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
@@ -67,18 +67,18 @@ require (
 	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/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
-	github.com/olekukonko/ll v0.0.8 // indirect
-	github.com/olekukonko/tablewriter v1.0.8 // indirect
+	github.com/olekukonko/errors v1.1.0 // indirect
+	github.com/olekukonko/ll v0.0.9 // indirect
+	github.com/olekukonko/tablewriter v1.0.9 // 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_golang v1.23.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/common v0.65.0 // indirect
-	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/soheilhy/cmux v0.1.5 // indirect
-	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/spf13/pflag v1.0.7 // indirect
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.etcd.io/gofail v0.2.0 // indirect
@@ -90,16 +90,16 @@ require (
 	go.opentelemetry.io/otel/metric v1.37.0 // indirect
 	go.opentelemetry.io/otel/sdk v1.37.0 // indirect
 	go.opentelemetry.io/otel/trace v1.37.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.7.1 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/crypto v0.40.0 // indirect
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/yaml v1.5.0 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
 )
diff --git a/go.sum b/go.sum
index e8a7d5da0..83e3b321c 100644
--- a/go.sum
+++ b/go.sum
@@ -45,8 +45,8 @@ 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.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 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=
@@ -97,26 +97,26 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 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/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
-github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
-github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
-github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
-github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
+github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
+github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
+github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
 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/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/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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
-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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 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=
@@ -130,8 +130,9 @@ github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
 github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -169,8 +170,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
 go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
 go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
 go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
-go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
-go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
 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=
@@ -246,17 +247,17 @@ 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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/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.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
@@ -270,5 +271,5 @@ 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/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
-sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/pkg/go.mod b/pkg/go.mod
index 3d3bbf0e4..163b3660d 100644
--- a/pkg/go.mod
+++ b/pkg/go.mod
@@ -8,12 +8,12 @@ require (
 	github.com/creack/pty v1.1.18
 	github.com/dustin/go-humanize v1.0.1
 	github.com/spf13/cobra v1.9.1
-	github.com/spf13/pflag v1.0.6
+	github.com/spf13/pflag v1.0.7
 	github.com/stretchr/testify v1.10.0
 	go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0
 	go.opentelemetry.io/otel/trace v1.37.0
 	go.uber.org/zap v1.27.0
-	google.golang.org/grpc v1.73.0
+	google.golang.org/grpc v1.74.2
 )
 
 require (
@@ -27,7 +27,7 @@ require (
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // 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 92cc09d58..a44832afa 100644
--- a/pkg/go.sum
+++ b/pkg/go.sum
@@ -31,8 +31,9 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
 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=
-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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 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=
@@ -59,10 +60,10 @@ golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
 golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
 golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
diff --git a/pkg/netutil/host_normalize.go b/pkg/netutil/host_normalize.go
new file mode 100644
index 000000000..39f6bba60
--- /dev/null
+++ b/pkg/netutil/host_normalize.go
@@ -0,0 +1,56 @@
+// 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 netutil
+
+import (
+	"net"
+	"net/url"
+	"strings"
+)
+
+// urlsHostNormalizedEqual compares two URLs for scheme, normalized host (including IPv6), and path equality.
+func urlsHostNormalizedEqual(a, b url.URL) bool {
+	return a.Scheme == b.Scheme &&
+		normalizeHost(a.Host) == normalizeHost(b.Host) &&
+		a.Path == b.Path
+}
+
+// normalizeHost returns the canonical string for the host and normalizes IPv6 and IPv4 addresses.
+func normalizeHost(host string) string {
+	hostOnly, port, err := net.SplitHostPort(host)
+	if err != nil {
+		hostOnly = host
+		port = ""
+	}
+
+	// Check if hostOnly is an IPv6 address. It could be with or without brackets.
+	ipStr := strings.Trim(hostOnly, "[]")
+	if ip := net.ParseIP(ipStr); ip != nil {
+		if ip.To4() == nil {
+			// For IPv6 address, always use brackets when there is a port.
+			return "[" + ip.String() + "]" + normalizePort(port)
+		}
+		// IPv4 address
+		return ip.String() + normalizePort(port)
+	}
+	return host
+}
+
+func normalizePort(port string) string {
+	if port == "" {
+		return ""
+	}
+	return ":" + port
+}
diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go
index 0f1a68585..7d8fcb95f 100644
--- a/pkg/netutil/netutil.go
+++ b/pkg/netutil/netutil.go
@@ -20,7 +20,6 @@ import (
 	"fmt"
 	"net"
 	"net/url"
-	"reflect"
 	"sort"
 	"time"
 
@@ -154,7 +153,7 @@ func urlsEqual(ctx context.Context, lg *zap.Logger, a []url.URL, b []url.URL) (b
 	sort.Sort(types.URLs(b))
 	var needResolve bool
 	for i := range a {
-		if !reflect.DeepEqual(a[i], b[i]) {
+		if !urlsHostNormalizedEqual(a[i], b[i]) {
 			needResolve = true
 			break
 		}
@@ -172,7 +171,7 @@ func urlsEqual(ctx context.Context, lg *zap.Logger, a []url.URL, b []url.URL) (b
 	sort.Sort(types.URLs(a))
 	sort.Sort(types.URLs(b))
 	for i := range a {
-		if !reflect.DeepEqual(a[i], b[i]) {
+		if !urlsHostNormalizedEqual(a[i], b[i]) {
 			return false, fmt.Errorf("resolved urls: %q != %q", a[i].String(), b[i].String())
 		}
 	}
diff --git a/server/etcdmain/grpc_proxy.go b/server/etcdmain/grpc_proxy.go
index 9aeae162c..b4300ab4b 100644
--- a/server/etcdmain/grpc_proxy.go
+++ b/server/etcdmain/grpc_proxy.go
@@ -29,9 +29,8 @@ import (
 	"path/filepath"
 	"time"
 
-	grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
 	grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
-	grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
+	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/soheilhy/cmux"
 	"github.com/spf13/cobra"
@@ -504,8 +503,6 @@ func newGRPCProxyServer(lg *zap.Logger, client *clientv3.Client) *grpc.Server {
 	electionp := grpcproxy.NewElectionProxy(client)
 	lockp := grpcproxy.NewLockProxy(client)
 
-	alwaysLoggingDeciderServer := func(ctx context.Context, fullMethodName string, servingObject any) bool { return true }
-
 	serverMetrics := grpc_prometheus.NewServerMetrics()
 	prometheus.MustRegister(serverMetrics)
 
@@ -517,12 +514,10 @@ func newGRPCProxyServer(lg *zap.Logger, client *clientv3.Client) *grpc.Server {
 	}
 	if grpcProxyEnableLogging {
 		grpcChainStreamList = append(grpcChainStreamList,
-			grpc_ctxtags.StreamServerInterceptor(),
-			grpc_zap.PayloadStreamServerInterceptor(lg, alwaysLoggingDeciderServer),
+			interceptors.StreamServerInterceptor(reportable(lg)),
 		)
 		grpcChainUnaryList = append(grpcChainUnaryList,
-			grpc_ctxtags.UnaryServerInterceptor(),
-			grpc_zap.PayloadUnaryServerInterceptor(lg, alwaysLoggingDeciderServer),
+			interceptors.UnaryServerInterceptor(reportable(lg)),
 		)
 	}
 
diff --git a/server/etcdmain/grpc_proxy_logger.go b/server/etcdmain/grpc_proxy_logger.go
new file mode 100644
index 000000000..285cf6ed2
--- /dev/null
+++ b/server/etcdmain/grpc_proxy_logger.go
@@ -0,0 +1,100 @@
+// 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 etcdmain
+
+import (
+	"context"
+	"fmt"
+	"path"
+	"reflect"
+	"slices"
+	"time"
+
+	"github.com/golang/protobuf/jsonpb"
+	"github.com/golang/protobuf/proto"
+	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors"
+	"go.uber.org/zap"
+	"google.golang.org/grpc/peer"
+)
+
+// grpcProxyLogger implements the go-grpc-middleware v2 Reporter interface
+type grpcProxyLogger struct {
+	logger *zap.Logger
+	fields []zap.Field
+}
+
+var _ interceptors.Reporter = (*grpcProxyLogger)(nil)
+
+const (
+	responseCallType = "response"
+	requestCallType  = "request"
+)
+
+func (r *grpcProxyLogger) PostCall(_ error, _ time.Duration) {
+	// no op - no post-call payload logging
+}
+
+func (r *grpcProxyLogger) PostMsgReceive(payload any, err error, _ time.Duration) {
+	callType := requestCallType
+	f := logFieldsFromPayload(payload, err, callType)
+	r.logger.Info(fmt.Sprintf("received payload logged as grpc.%s.content field", callType), slices.Concat(f, r.fields)...)
+}
+
+func (r *grpcProxyLogger) PostMsgSend(payload any, err error, _ time.Duration) {
+	callType := responseCallType
+	f := logFieldsFromPayload(payload, err, callType)
+	r.logger.Info(fmt.Sprintf("returned response payload logged as grpc.%s.content field", callType), slices.Concat(f, r.fields)...)
+}
+
+func logFieldsFromPayload(payload any, err error, callType string) []zap.Field {
+	fields := []zap.Field{}
+	if err != nil {
+		fields = append(fields, zap.NamedError(fmt.Sprintf("grpc.%s.error", callType), err))
+	}
+	p, ok := payload.(proto.Message)
+	if !ok {
+		fields = append(fields, zap.NamedError("msg.type.error", fmt.Errorf("payload is not a github.com/golang/protobuf/proto message")))
+	}
+	msg, pErr := protoToJSON(p)
+	if pErr != nil {
+		fields = append(fields, zap.NamedError("msg.proto.error", fmt.Errorf("error when converting payload to logging json: %w", pErr)))
+	}
+	fields = append(fields, zap.String(fmt.Sprintf("grpc.%s.content", callType), msg))
+	return fields
+}
+
+func protoToJSON(msg proto.Message) (string, error) {
+	if reflect.ValueOf(msg).IsNil() {
+		return "", nil
+	}
+	marshaler := jsonpb.Marshaler{}
+	return marshaler.MarshalToString(msg)
+}
+
+func reportable(lg *zap.Logger) interceptors.CommonReportableFunc {
+	return func(ctx context.Context, c interceptors.CallMeta) (interceptors.Reporter, context.Context) {
+		fields := []zap.Field{
+			zap.String("grpc.service", path.Dir(c.FullMethod())[1:]),
+			zap.String("grpc.method", path.Base(c.FullMethod())),
+		}
+		if peer, ok := peer.FromContext(ctx); ok {
+			fields = append(fields, zap.String("peer.address", peer.Addr.String()))
+		}
+		return &grpcProxyLogger{
+			logger: lg,
+			fields: fields,
+		}, ctx
+	}
+}
diff --git a/server/go.mod b/server/go.mod
index 2755c6baf..57ccf0b37 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -9,16 +9,16 @@ require (
 	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.2
+	github.com/golang-jwt/jwt/v5 v5.3.0
 	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
-	github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
 	github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
+	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
 	github.com/jonboulle/clockwork v0.5.0
-	github.com/prometheus/client_golang v1.22.0
+	github.com/prometheus/client_golang v1.23.0
 	github.com/prometheus/client_model v0.6.2
 	github.com/soheilhy/cmux v0.1.5
 	github.com/spf13/cobra v1.9.1
@@ -40,11 +40,11 @@ require (
 	golang.org/x/crypto v0.40.0
 	golang.org/x/net v0.42.0
 	golang.org/x/time v0.12.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822
-	google.golang.org/grpc v1.73.0
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0
+	google.golang.org/grpc v1.74.2
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
-	sigs.k8s.io/yaml v1.5.0
+	sigs.k8s.io/yaml v1.6.0
 )
 
 require (
@@ -56,24 +56,23 @@ require (
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/google/uuid v1.6.0 // 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/kylelemons/godebug v1.1.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/common v0.65.0 // indirect
-	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
-	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/spf13/pflag v1.0.7 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
 	go.opentelemetry.io/otel/metric v1.37.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.7.1 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 
diff --git a/server/go.sum b/server/go.sum
index 52303f22d..da43f7b28 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -1,14 +1,9 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 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/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
 github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
 github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
 github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
@@ -22,43 +17,28 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 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=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
 github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 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.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-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 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=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 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=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 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.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.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
@@ -73,8 +53,6 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
 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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 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=
@@ -83,36 +61,30 @@ 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/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/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/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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
-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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 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=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
 github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@@ -144,15 +116,12 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
 go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
 go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
 go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
-go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
-go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
 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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 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.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
@@ -164,16 +133,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
 golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
-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=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -181,18 +142,13 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
 golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
-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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
 golang.org/x/sync v0.16.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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
@@ -204,10 +160,6 @@ golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
 golang.org/x/time v0.12.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=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -215,22 +167,12 @@ 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/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.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
@@ -238,11 +180,8 @@ 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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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=
-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/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
-sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/server/storage/mvcc/kv_view.go b/server/storage/mvcc/kv_view.go
index 56260e759..b5e8bf909 100644
--- a/server/storage/mvcc/kv_view.go
+++ b/server/storage/mvcc/kv_view.go
@@ -24,13 +24,13 @@ import (
 type readView struct{ kv KV }
 
 func (rv *readView) FirstRev() int64 {
-	tr := rv.kv.Read(ConcurrentReadTxMode, traceutil.TODO())
+	tr := rv.kv.Read(SharedBufReadTxMode, traceutil.TODO())
 	defer tr.End()
 	return tr.FirstRev()
 }
 
 func (rv *readView) Rev() int64 {
-	tr := rv.kv.Read(ConcurrentReadTxMode, traceutil.TODO())
+	tr := rv.kv.Read(SharedBufReadTxMode, traceutil.TODO())
 	defer tr.End()
 	return tr.Rev()
 }
diff --git a/tools/mod/go.mod b/tools/mod/go.mod
index dde1f0459..92213118f 100644
--- a/tools/mod/go.mod
+++ b/tools/mod/go.mod
@@ -145,17 +145,17 @@ require (
 	github.com/nishanths/exhaustive v0.12.0 // indirect
 	github.com/nishanths/predeclared v0.2.2 // indirect
 	github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
-	github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
-	github.com/olekukonko/ll v0.0.8 // indirect
-	github.com/olekukonko/tablewriter v1.0.8 // indirect
+	github.com/olekukonko/errors v1.1.0 // indirect
+	github.com/olekukonko/ll v0.0.9 // indirect
+	github.com/olekukonko/tablewriter v1.0.9 // 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.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/polyfloyd/go-errorlint v1.7.1 // indirect
-	github.com/prometheus/client_golang v1.22.0 // indirect
+	github.com/prometheus/client_golang v1.23.0 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/common v0.65.0 // indirect
-	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
 	github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
 	github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
 	github.com/quasilyte/gogrep v0.5.0 // indirect
@@ -182,7 +182,7 @@ require (
 	github.com/spf13/afero v1.12.0 // indirect
 	github.com/spf13/cast v1.7.1 // indirect
 	github.com/spf13/cobra v1.9.1 // indirect
-	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/spf13/pflag v1.0.7 // 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
@@ -223,9 +223,9 @@ require (
 	golang.org/x/term v0.33.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
 	golang.org/x/tools v0.34.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/grpc v1.73.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/grpc v1.74.2 // 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
diff --git a/tools/mod/go.sum b/tools/mod/go.sum
index bfbb66f18..ca5623e11 100644
--- a/tools/mod/go.sum
+++ b/tools/mod/go.sum
@@ -324,12 +324,12 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm
 github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
 github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4=
 github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s=
-github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
-github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
-github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
-github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
-github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
+github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
+github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
+github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
 github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
 github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
 github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
@@ -353,14 +353,14 @@ github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L
 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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
-github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
+github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
-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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo=
 github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI=
 github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
@@ -421,8 +421,9 @@ github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cA
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 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=
@@ -685,12 +686,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 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/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
diff --git a/tools/rw-heatmaps/go.mod b/tools/rw-heatmaps/go.mod
index 76bea1069..a6d1a139c 100644
--- a/tools/rw-heatmaps/go.mod
+++ b/tools/rw-heatmaps/go.mod
@@ -6,7 +6,7 @@ toolchain go1.24.5
 
 require (
 	github.com/spf13/cobra v1.9.1
-	github.com/spf13/pflag v1.0.6
+	github.com/spf13/pflag v1.0.7
 	gonum.org/v1/plot v0.14.0
 )
 
diff --git a/tools/rw-heatmaps/go.sum b/tools/rw-heatmaps/go.sum
index 1813c62e0..f2f146b46 100644
--- a/tools/rw-heatmaps/go.sum
+++ b/tools/rw-heatmaps/go.sum
@@ -31,8 +31,9 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
 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=
-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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 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=
diff --git a/tools/testgrid-analysis/go.mod b/tools/testgrid-analysis/go.mod
index c299b7b94..cfdf1ef1c 100644
--- a/tools/testgrid-analysis/go.mod
+++ b/tools/testgrid-analysis/go.mod
@@ -14,12 +14,12 @@ require (
 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
+	github.com/spf13/pflag v1.0.7 // indirect
 	go.opentelemetry.io/otel v1.37.0 // indirect
 	go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/grpc v1.73.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/grpc v1.74.2 // indirect
 )
diff --git a/tools/testgrid-analysis/go.sum b/tools/testgrid-analysis/go.sum
index 246e9763f..1df068c4c 100644
--- a/tools/testgrid-analysis/go.sum
+++ b/tools/testgrid-analysis/go.sum
@@ -1143,8 +1143,9 @@ github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcD
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -1841,8 +1842,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-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/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 +1889,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.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=



Test patch:
diff --git a/cache/cache_test.go b/cache/cache_test.go
index 6321a1356..3093d49fb 100644
--- a/cache/cache_test.go
+++ b/cache/cache_test.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 
+	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
 	mvccpb "go.etcd.io/etcd/api/v3/mvccpb"
 	clientv3 "go.etcd.io/etcd/client/v3"
 )
@@ -188,7 +189,14 @@ func TestCacheWatchAtomicOrderedDelivery(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			mw := newMockWatcher(16)
-			cache, err := New(mw, "")
+			fakeClient := &clientv3.Client{
+				Watcher: mw,
+				KV:      &kvStub{},
+			}
+			cache, err := New(fakeClient, "")
+			if err != nil {
+				t.Fatalf("New cache: %v", err)
+			}
 			if err != nil {
 				t.Fatalf("New cache: %v", err)
 			}
@@ -395,6 +403,32 @@ type mockWatcher struct {
 	registered chan struct{}
 }
 
+type kvStub struct{}
+
+func (kvStub) Get(ctx context.Context, key string, _ ...clientv3.OpOption) (*clientv3.GetResponse, error) {
+	return &clientv3.GetResponse{Header: &pb.ResponseHeader{Revision: 0}}, nil
+}
+
+func (kvStub) Put(ctx context.Context, key, val string, _ ...clientv3.OpOption) (*clientv3.PutResponse, error) {
+	return nil, nil
+}
+
+func (kvStub) Delete(ctx context.Context, key string, _ ...clientv3.OpOption) (*clientv3.DeleteResponse, error) {
+	return nil, nil
+}
+
+func (kvStub) Compact(ctx context.Context, rev int64, _ ...clientv3.CompactOption) (*clientv3.CompactResponse, error) {
+	return nil, nil
+}
+
+func (kvStub) Do(ctx context.Context, op clientv3.Op) (clientv3.OpResponse, error) {
+	return clientv3.OpResponse{}, nil
+}
+
+func (kvStub) Txn(ctx context.Context) clientv3.Txn {
+	return nil
+}
+
 func newMockWatcher(buf int) *mockWatcher {
 	return &mockWatcher{
 		responses:  make(chan clientv3.WatchResponse, buf),
@@ -403,7 +437,11 @@ func newMockWatcher(buf int) *mockWatcher {
 }
 
 func (m *mockWatcher) Watch(_ context.Context, _ string, _ ...clientv3.OpOption) clientv3.WatchChan {
-	close(m.registered)
+	select { // prevent “close of closed channel” on re-watch
+	case <-m.registered:
+	default:
+		close(m.registered)
+	}
 	return m.responses
 }
 
diff --git a/etcdctl/ctlv3/command/printer_json_test.go b/etcdctl/ctlv3/command/printer_json_test.go
index d0ae96221..8f65be698 100644
--- a/etcdctl/ctlv3/command/printer_json_test.go
+++ b/etcdctl/ctlv3/command/printer_json_test.go
@@ -207,6 +207,86 @@ func TestMemberRemove(t *testing.T) {
 	}
 }
 
+func TestMemberUpdate(t *testing.T) {
+	tests := []testScenario{
+		{name: "decimal", isHex: false, cases: testCases},
+		{name: "hex", isHex: true, cases: testCases},
+	}
+
+	for _, testGroup := range tests {
+		t.Run(testGroup.name, func(t *testing.T) {
+			var buffer bytes.Buffer
+			p := &jsonPrinter{writer: &buffer, isHex: testGroup.isHex}
+
+			for _, tt := range testGroup.cases {
+				t.Run(fmt.Sprintf("number=%d", tt.number), func(t *testing.T) {
+					buffer.Reset()
+					decoder := json.NewDecoder(&buffer)
+					decoder.UseNumber()
+
+					response := clientv3.MemberUpdateResponse{
+						Header: &pb.ResponseHeader{
+							ClusterId: tt.number,
+							MemberId:  tt.number,
+							Revision:  int64(tt.number),
+							RaftTerm:  tt.number,
+						},
+						Members: []*pb.Member{{ID: tt.number}},
+					}
+					p.MemberUpdate(0, response)
+
+					var got map[string]any
+					err := decoder.Decode(&got)
+					require.NoErrorf(t, err, "failed to decode JSON")
+
+					assertHeader(t, &testGroup, &tt, got)
+					assertMembers(t, &testGroup, &tt, got)
+				})
+			}
+		})
+	}
+}
+
+func TestMemberPromote(t *testing.T) {
+	tests := []testScenario{
+		{name: "decimal", isHex: false, cases: testCases},
+		{name: "hex", isHex: true, cases: testCases},
+	}
+
+	for _, testGroup := range tests {
+		t.Run(testGroup.name, func(t *testing.T) {
+			var buffer bytes.Buffer
+			p := &jsonPrinter{writer: &buffer, isHex: testGroup.isHex}
+
+			for _, tt := range testGroup.cases {
+				t.Run(fmt.Sprintf("number=%d", tt.number), func(t *testing.T) {
+					buffer.Reset()
+					decoder := json.NewDecoder(&buffer)
+					decoder.UseNumber()
+
+					response := clientv3.MemberPromoteResponse{
+						Header: &pb.ResponseHeader{
+							ClusterId: tt.number,
+							MemberId:  tt.number,
+							Revision:  int64(tt.number),
+							RaftTerm:  tt.number,
+						},
+						Members: []*pb.Member{{ID: tt.number}},
+					}
+					p.MemberPromote(0, response)
+
+					var got map[string]any
+					err := decoder.Decode(&got)
+					require.NoErrorf(t, err, "failed to decode JSON")
+
+					assertHeader(t, &testGroup, &tt, got)
+					assertMembers(t, &testGroup, &tt, got)
+				})
+			}
+		})
+	}
+}
+
 func TestMemberList(t *testing.T) {
 	tests := []testScenario{
 		{name: "decimal", isHex: false, cases: testCases},
diff --git a/pkg/netutil/host_normalize_test.go b/pkg/netutil/host_normalize_test.go
new file mode 100644
index 000000000..dbb0d951a
--- /dev/null
+++ b/pkg/netutil/host_normalize_test.go
@@ -0,0 +1,139 @@
+// 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 netutil
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"go.uber.org/zap/zaptest"
+)
+
+func TestIPv6AddressNormalization(t *testing.T) {
+	testCases := []struct {
+		name     string
+		urlsA    []string
+		urlsB    []string
+		expected bool
+	}{
+		{
+			name:     "IPv6 with leading zeros vs without should match",
+			urlsA:    []string{"https://[c262:266f:fa53:0ee6:966e:e3f0:d68f:b046]:2380"},
+			urlsB:    []string{"https://[c262:266f:fa53:ee6:966e:e3f0:d68f:b046]:2380"},
+			expected: true,
+		},
+		{
+			name:     "IPv6 different (lower/upper) case should match",
+			urlsA:    []string{"https://[2001:DB8::1]:2380"},
+			urlsB:    []string{"https://[2001:db8::1]:2380"},
+			expected: true,
+		},
+		{
+			name:     "IPv4 address normalization should still work",
+			urlsA:    []string{"http://192.168.1.1:2380"},
+			urlsB:    []string{"http://192.168.1.1:2380"},
+			expected: true,
+		},
+		{
+			name:     "Different IPv6 addresses should not match",
+			urlsA:    []string{"https://[2001:db8::1]:2380"},
+			urlsB:    []string{"https://[2001:db8::2]:2380"},
+			expected: false,
+		},
+		{
+			name:     "IPv6 without port should match",
+			urlsA:    []string{"https://[2001:db8::1]"},
+			urlsB:    []string{"https://[2001:0db8:0:00:000:0000:0:01]"},
+			expected: true,
+		},
+		{
+			name:     "IPv4 without port should match",
+			urlsA:    []string{"http://192.168.1.1"},
+			urlsB:    []string{"http://192.168.1.1"},
+			expected: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			result, err := URLStringsEqual(t.Context(), zaptest.NewLogger(t), tc.urlsA, tc.urlsB)
+			if tc.expected {
+				assert.True(t, result)
+			} else {
+				if err != nil {
+					t.Logf("Got expected error for non-matching URLs: %v", err)
+				} else {
+					assert.False(t, result)
+				}
+			}
+		})
+	}
+}
+
+func TestNormalizeHostFunction(t *testing.T) {
+	testCases := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "IPv6 with leading zeros should be normalized",
+			input:    "[c262:266f:fa53:0ee6:966e:e3f0:d68f:b046]:2380",
+			expected: "[c262:266f:fa53:ee6:966e:e3f0:d68f:b046]:2380",
+		},
+		{
+			name:     "Compressed IPv6 should remain compressed",
+			input:    "[2001:db8::1]:2380",
+			expected: "[2001:db8::1]:2380",
+		},
+		{
+			name:     "IPv6 case should be normalized to lowercase",
+			input:    "[2001:DB8::1]:2380",
+			expected: "[2001:db8::1]:2380",
+		},
+		{
+			name:     "IPv4 should remain unchanged",
+			input:    "192.168.1.1:2380",
+			expected: "192.168.1.1:2380",
+		},
+		{
+			name:     "Hostname should remain unchanged",
+			input:    "example.com:2380",
+			expected: "example.com:2380",
+		},
+		{
+			name:     "IPv6 without port",
+			input:    "[2025:db8::1]",
+			expected: "[2025:db8::1]",
+		},
+		{
+			name:     "IPv4 without port",
+			input:    "192.168.1.1",
+			expected: "192.168.1.1",
+		},
+		{
+			name:     "Hostname without port",
+			input:    "example.com",
+			expected: "example.com",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			result := normalizeHost(tc.input)
+			assert.Equal(t, tc.expected, result)
+		})
+	}
+}
diff --git a/server/etcdmain/grpc_proxy_logger_test.go b/server/etcdmain/grpc_proxy_logger_test.go
new file mode 100644
index 000000000..9faed2829
--- /dev/null
+++ b/server/etcdmain/grpc_proxy_logger_test.go
@@ -0,0 +1,129 @@
+// 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 etcdmain
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"testing"
+	"time"
+
+	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors"
+	"github.com/grpc-ecosystem/go-grpc-middleware/v2/testing/testpb"
+	"github.com/stretchr/testify/suite"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zaptest/observer"
+	"google.golang.org/grpc"
+)
+
+type loggingPayloadSuite struct {
+	*testpb.InterceptorTestSuite
+	logger *zap.Logger
+	logs   *observer.ObservedLogs
+}
+
+func TestLoggingPayloadSuite(t *testing.T) {
+	observer, logs := observer.New(zap.InfoLevel)
+	logger := zap.New(observer)
+	s := &loggingPayloadSuite{
+		InterceptorTestSuite: &testpb.InterceptorTestSuite{
+			TestService: &testpb.TestPingService{},
+			ServerOpts: []grpc.ServerOption{
+				grpc.UnaryInterceptor(interceptors.UnaryServerInterceptor(reportable(logger))),
+				grpc.StreamInterceptor(interceptors.StreamServerInterceptor(reportable(logger))),
+			},
+		},
+		logs:   logs,
+		logger: zap.New(observer),
+	}
+	suite.Run(t, s)
+}
+
+func (s *loggingPayloadSuite) SetupTest() {
+	s.logs.TakeAll() // clear logs
+	s.Require().Empty(s.logs.TakeAll())
+}
+
+func (s *loggingPayloadSuite) TestPing_LogsBothRequestAndResponse() {
+	_, err := s.Client.Ping(s.SimpleCtx(), testpb.GoodPing)
+	s.Require().NoError(err)
+	s.Require().Len(s.logs.All(), 2) // request and response
+	s.assertField("grpc.request.content", `{"value":"something","sleepTimeMs":9999}`, 1)
+	s.assertField("grpc.response.content", `{"value":"something"}`, 1)
+}
+
+func (s *loggingPayloadSuite) TestPingError_LogsError() {
+	_, err := s.Client.PingError(s.SimpleCtx(), &testpb.PingErrorRequest{Value: "something", ErrorCodeReturned: uint32(4)})
+	s.Require().Error(err)
+	s.Require().Len(s.logs.All(), 2) // request and response
+	s.assertField("grpc.request.content", `{"value":"something","errorCodeReturned":4}`, 1)
+	s.assertField("grpc.response.content", ``, 1)
+	s.assertField("grpc.response.error", "rpc error: code = DeadlineExceeded desc = Userspace error", 1)
+}
+
+func (s *loggingPayloadSuite) TestPingStream_LogsAllRequestsAndResponses() {
+	messagesExpected := 10
+	stream, err := s.Client.PingStream(s.SimpleCtx())
+	s.Require().NoError(err)
+
+	for range messagesExpected {
+		s.Require().NoError(stream.Send(testpb.GoodPingStream))
+		pong := &testpb.PingResponse{}
+		err := stream.RecvMsg(pong)
+		s.Require().NoError(err)
+	}
+	s.Require().NoError(stream.CloseSend())
+	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+	defer cancel()
+	s.Require().NoError(waitUntil(200*time.Millisecond, ctx.Done(), func() error {
+		if len(s.logs.FilterFieldKey("grpc.request.error").All()) > 0 {
+			return nil
+		}
+		return fmt.Errorf("no EOF log yet")
+	}))
+	eof := s.logs.FilterFieldKey("grpc.request.error").All()
+	s.Len(eof, 1)
+	s.Equal(io.EOF.Error(), eof[0].ContextMap()["grpc.request.error"])
+	s.assertField("grpc.request.content", `{"value":"something","sleepTimeMs":9999}`, messagesExpected+1)
+	s.assertField("grpc.response.content", `{"value":"something"}`, messagesExpected)
+}
+
+func (s *loggingPayloadSuite) assertField(key, expectedValue string, expectedLineCount int) {
+	s.T().Helper()
+	filtered := s.logs.FilterFieldKey(key).All()
+	s.Require().Len(filtered, expectedLineCount)
+	actualValue, ok := filtered[0].ContextMap()[key].(string)
+	s.Require().True(ok)
+	s.Equal(expectedValue, actualValue)
+}
+
+// waitUntil executes f every interval seconds until timeout or no error is returned from f.
+func waitUntil(interval time.Duration, stopc <-chan struct{}, f func() error) error {
+	tick := time.NewTicker(interval)
+	defer tick.Stop()
+
+	var err error
+	for {
+		if err = f(); err == nil {
+			return nil
+		}
+		select {
+		case <-stopc:
+			return err
+		case <-tick.C:
+		}
+	}
+}
diff --git a/tests/antithesis/config/docker-compose-1-node.yml b/tests/antithesis/config/docker-compose-1-node.yml
index 646c04a3d..fcf2d97db 100644
--- a/tests/antithesis/config/docker-compose-1-node.yml
+++ b/tests/antithesis/config/docker-compose-1-node.yml
@@ -35,6 +35,7 @@ services:
       ETCD_SNAPSHOT_CATCHUP_ENTRIES: 100
       ETCD_SNAPSHOT_COUNT: 50
       ETCD_COMPACTION_BATCH_LIMIT: 10
+      ETCD_VERIFY: "all"
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
     depends_on:
       init:
diff --git a/tests/antithesis/config/docker-compose-3-node.yml b/tests/antithesis/config/docker-compose-3-node.yml
index 002f58737..4a0bf7253 100644
--- a/tests/antithesis/config/docker-compose-3-node.yml
+++ b/tests/antithesis/config/docker-compose-3-node.yml
@@ -37,6 +37,7 @@ services:
       ETCD_SNAPSHOT_CATCHUP_ENTRIES: 100
       ETCD_SNAPSHOT_COUNT: 50
       ETCD_COMPACTION_BATCH_LIMIT: 10
+      ETCD_VERIFY: "all"
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
     depends_on:
       init:
@@ -63,6 +64,7 @@ services:
       ETCD_SNAPSHOT_CATCHUP_ENTRIES: 100
       ETCD_SNAPSHOT_COUNT: 50
       ETCD_COMPACTION_BATCH_LIMIT: 10
+      ETCD_VERIFY: "all"
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
     depends_on:
       init:
@@ -89,6 +91,7 @@ services:
       ETCD_SNAPSHOT_CATCHUP_ENTRIES: 100
       ETCD_SNAPSHOT_COUNT: 50
       ETCD_COMPACTION_BATCH_LIMIT: 10
+      ETCD_VERIFY: "all"
     user: "${USER_ID:-root}:${GROUP_ID:-root}"
     depends_on:
       init:
diff --git a/tests/antithesis/server/Dockerfile b/tests/antithesis/server/Dockerfile
index f332e598c..ed1f96bba 100644
--- a/tests/antithesis/server/Dockerfile
+++ b/tests/antithesis/server/Dockerfile
@@ -7,13 +7,17 @@ FROM golang:$GO_VERSION
 ARG REF=main
 RUN git clone --depth=1 https://github.com/etcd-io/etcd.git --branch=${REF} /etcd
 
+# inject assertions in place of gofail
+WORKDIR /etcd/server
+RUN go install golang.org/x/tools/cmd/goimports@latest
+RUN go get github.com/antithesishq/antithesis-sdk-go@v0.4.4
+RUN for file in $(grep -rl '// gofail'); do sed -i 's|\/\/ gofail.*var \([[:alnum:]]*\) .*|assert\.Reachable("\1", nil)|' $file; goimports -w $file; done
+RUN go mod tidy
+
 # replace verify with antithesis
 WORKDIR /etcd/client/pkg/verify
 COPY inject/verify.patch verify.patch
-RUN if [ "${REF}" == "main" ]; then \
-      git apply verify.patch; \
-    fi
-RUN rm verify.patch
+RUN if [ "${REF}" = "main" ]; then git apply verify.patch; fi
 WORKDIR /etcd/client/pkg
 RUN go mod tidy
 WORKDIR /etcd
@@ -22,7 +26,7 @@ WORKDIR /etcd
 RUN go mod download
 
 # install instrumentor
-RUN go get github.com/antithesishq/antithesis-sdk-go@a802e8810442e01d16b3e9df77d7ce3875e36e55 # v0.4.3
+RUN go get github.com/antithesishq/antithesis-sdk-go@v0.4.4
 RUN go install github.com/antithesishq/antithesis-sdk-go/tools/antithesis-go-instrumentor@a802e8810442e01d16b3e9df77d7ce3875e36e55 # v0.4.3
 RUN go mod tidy
 
diff --git a/tests/e2e/reproduce_18667_test.go b/tests/e2e/reproduce_18667_test.go
new file mode 100644
index 000000000..e7ac651cf
--- /dev/null
+++ b/tests/e2e/reproduce_18667_test.go
@@ -0,0 +1,85 @@
+// 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 (
+	"fmt"
+	"math/rand"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
+	clientv3 "go.etcd.io/etcd/client/v3"
+	"go.etcd.io/etcd/tests/v3/framework/e2e"
+)
+
+// TestReproduce18667 reproduces the issue: https://github.com/etcd-io/etcd/issues/18667.
+func TestReproduce18667(t *testing.T) {
+	e2e.BeforeTest(t)
+
+	ctx := t.Context()
+	clus, cerr := e2e.NewEtcdProcessCluster(ctx, t,
+		e2e.WithClusterSize(3),
+		e2e.WithSnapshotCount(1000),
+	)
+	require.NoError(t, cerr)
+
+	t.Cleanup(func() { clus.Stop() })
+
+	targetIdx := rand.Intn(3)
+	cli := newClient(t, clus.Procs[targetIdx].EndpointsGRPC(), e2e.ClientConfig{})
+
+	t.Log("Put key-value [k1...k20]")
+	var revision int64
+	for i := 1; i <= 20; i++ {
+		resp, err := cli.Put(ctx, fmt.Sprintf("k%d", i), fmt.Sprintf("v%d", i))
+		require.NoError(t, err)
+		revision = resp.Header.Revision
+	}
+
+	t.Logf("Compact on the latest revision %d", revision)
+	_, err := cli.Compact(ctx, revision)
+	require.NoError(t, err)
+
+	t.Log("Send txn to make data inconsistent among etcdservers")
+	cmp := clientv3.Compare(clientv3.Value("k1"), "=", "v1")
+	then := []clientv3.Op{
+		clientv3.OpPut("k2", "foo"),
+		clientv3.OpGet("k1", clientv3.WithRev(revision-1)),
+	}
+	_, err = cli.Txn(ctx).If(cmp).Then(then...).Commit()
+	require.Error(t, err)
+	require.Contains(t, err.Error(), rpctypes.ErrCompacted.Error())
+
+	t.Log("Verify all members have consistent data on key 'k2'")
+	for i := 0; i < clus.Cfg.ClusterSize; i++ {
+		idx := (targetIdx + i) % clus.Cfg.ClusterSize
+		cli = newClient(t, clus.Procs[idx].EndpointsGRPC(), e2e.ClientConfig{})
+		resp, err := cli.Get(ctx, "k2")
+		require.NoError(t, err)
+		// TODO: All members are supposed to have the same key/value pair k2:v2.
+		// However, the issue https://github.com/etcd-io/etcd/issues/18667
+		// hasn't been resolved yet, so some members hold the wrong data
+		// "k2:foo". Once the issue is fixed, we should remove the if-else
+		// branch below, and all member should have consistent data k2:v2.
+		if i == 0 {
+			assert.Equal(t, "v2", string(resp.Kvs[0].Value))
+		} else {
+			assert.Equal(t, "foo", string(resp.Kvs[0].Value))
+		}
+	}
+}
diff --git a/tests/framework/e2e/etcdctl.go b/tests/framework/e2e/etcdctl.go
index e212e447c..c86fdb871 100644
--- a/tests/framework/e2e/etcdctl.go
+++ b/tests/framework/e2e/etcdctl.go
@@ -250,13 +250,13 @@ func (ctl *EtcdctlV3) Txn(ctx context.Context, compares, ifSucess, ifFail []stri
 		return nil, err
 	}
 	var resp clientv3.TxnResponse
-	AddTxnResponse(&resp, line)
+	addTxnResponse(&resp, line)
 	err = json.Unmarshal([]byte(line), &resp)
 	return &resp, err
 }
 
-// AddTxnResponse looks for ResponseOp json tags and adds the objects for json decoding
-func AddTxnResponse(resp *clientv3.TxnResponse, jsonData string) {
+// addTxnResponse looks for ResponseOp json tags and adds the objects for json decoding
+func addTxnResponse(resp *clientv3.TxnResponse, jsonData string) {
 	if resp == nil {
 		return
 	}
diff --git a/tests/framework/e2e/e2e_test.go b/tests/framework/e2e/etcdctl_test.go
similarity index 95%
rename from tests/framework/e2e/e2e_test.go
rename to tests/framework/e2e/etcdctl_test.go
index 00059df81..3bc16e981 100644
--- a/tests/framework/e2e/e2e_test.go
+++ b/tests/framework/e2e/etcdctl_test.go
@@ -21,10 +21,10 @@ import (
 	clientv3 "go.etcd.io/etcd/client/v3"
 )
 
-func Test_AddTxnResponse(t *testing.T) {
+func Test_addTxnResponse(t *testing.T) {
 	jsonData := `{"header":{"cluster_id":238453183653593855,"member_id":14578408409545168728,"revision":3,"raft_term":2},"succeeded":true,"responses":[{"Response":{"response_range":{"header":{"revision":3},"kvs":[{"key":"a2V5MQ==","create_revision":2,"mod_revision":2,"version":1,"value":"dmFsdWUx"}],"count":1}}},{"Response":{"response_range":{"header":{"revision":3},"kvs":[{"key":"a2V5Mg==","create_revision":3,"mod_revision":3,"version":1,"value":"dmFsdWUy"}],"count":1}}}]}`
 	var resp clientv3.TxnResponse
-	AddTxnResponse(&resp, jsonData)
+	addTxnResponse(&resp, jsonData)
 	err := json.Unmarshal([]byte(jsonData), &resp)
 	if err != nil {
 		t.Errorf("json Unmarshal failed. err: %s", err)
diff --git a/tests/go.mod b/tests/go.mod
index 09646e7eb..79b583963 100644
--- a/tests/go.mod
+++ b/tests/go.mod
@@ -19,14 +19,14 @@ require (
 	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-jwt/jwt/v5 v5.3.0
 	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.1.0
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1
-	github.com/olekukonko/tablewriter v1.0.8
-	github.com/prometheus/client_golang v1.22.0
+	github.com/olekukonko/tablewriter v1.0.9
+	github.com/prometheus/client_golang v1.23.0
 	github.com/prometheus/client_model v0.6.2
 	github.com/prometheus/common v0.65.0
 	github.com/soheilhy/cmux v0.1.5
@@ -45,12 +45,12 @@ require (
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0
 	go.opentelemetry.io/otel v1.37.0
 	go.opentelemetry.io/otel/sdk v1.37.0
-	go.opentelemetry.io/proto/otlp v1.7.0
+	go.opentelemetry.io/proto/otlp v1.7.1
 	go.uber.org/zap v1.27.0
 	golang.org/x/crypto v0.40.0
 	golang.org/x/sync v0.16.0
 	golang.org/x/time v0.12.0
-	google.golang.org/grpc v1.73.0
+	google.golang.org/grpc v1.74.2
 	google.golang.org/protobuf v1.36.6
 )
 
@@ -80,14 +80,14 @@ require (
 	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/errors v0.0.0-20250405072817-4e6d85265da6 // indirect
-	github.com/olekukonko/ll v0.0.8 // indirect
+	github.com/olekukonko/errors v1.1.0 // indirect
+	github.com/olekukonko/ll v0.0.9 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/prometheus/procfs v0.16.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/spf13/cobra v1.9.1 // indirect
-	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/spf13/pflag v1.0.7 // indirect
 	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
 	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@@ -100,9 +100,9 @@ require (
 	golang.org/x/net v0.42.0 // indirect
 	golang.org/x/sys v0.34.0 // indirect
 	golang.org/x/text v0.27.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	sigs.k8s.io/yaml v1.5.0 // indirect
+	sigs.k8s.io/yaml v1.6.0 // indirect
 )
diff --git a/tests/go.sum b/tests/go.sum
index b813aac20..27b4d631b 100644
--- a/tests/go.sum
+++ b/tests/go.sum
@@ -51,8 +51,8 @@ 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.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
+github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 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=
@@ -103,26 +103,26 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 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/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo=
-github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
-github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
-github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
-github.com/olekukonko/tablewriter v1.0.8 h1:f6wJzHg4QUtJdvrVPKco4QTrAylgaU0+b9br/lJxEiQ=
-github.com/olekukonko/tablewriter v1.0.8/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
+github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
+github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
+github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
+github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
 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/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/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_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
+github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
-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/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
+github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
 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=
@@ -136,8 +136,9 @@ github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
 github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
-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/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
+github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -175,8 +176,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh
 go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
 go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
 go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
-go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
-go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
+go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
+go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
 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=
@@ -252,17 +253,17 @@ 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-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
-google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8=
+google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/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.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
-google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
+google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
 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=
@@ -276,5 +277,5 @@ 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/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
-sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
+sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/tests/integration/cache_test.go b/tests/integration/cache_test.go
index 0b244b7e1..0a9de0323 100644
--- a/tests/integration/cache_test.go
+++ b/tests/integration/cache_test.go
@@ -374,7 +374,7 @@ func testWatch(t *testing.T, kv clientv3.KV, watcher Watcher) {
 	}
 }
 
-func TestCacheWithPrefix(t *testing.T) {
+func TestCacheWithPrefixWatch(t *testing.T) {
 	integration.BeforeTest(t)
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
 	t.Cleanup(func() { clus.Terminate(t) })
@@ -486,6 +486,312 @@ func TestCacheWithPrefix(t *testing.T) {
 	}
 }
 
+func TestCacheWithoutPrefixGet(t *testing.T) {
+	tcs := []struct {
+		name                          string
+		initialEvents, followupEvents []*clientv3.Event
+	}{
+		{"empty cache", nil, TestGetEvents},
+		{"partial load", filterEvents(TestGetEvents, revLessThan(4)), filterEvents(TestGetEvents, revGreaterEqual(4))},
+		{"complete load", TestGetEvents, nil},
+	}
+
+	for _, tc := range tcs {
+		t.Run(tc.name, func(t *testing.T) {
+			integration.BeforeTest(t)
+			clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+			t.Cleanup(func() { clus.Terminate(t) })
+			client, kv := clus.Client(0), clus.Client(0).KV
+
+			testGet(t, kv, func() Getter {
+				c, err := cache.New(client, "")
+				if err != nil {
+					t.Fatalf("cache.New: %v", err)
+				}
+				t.Cleanup(c.Close)
+				if err := c.WaitReady(t.Context()); err != nil {
+					t.Fatalf("cache not ready: %v", err)
+				}
+				return c
+			}, tc.initialEvents, tc.followupEvents)
+		})
+	}
+}
+
+func TestGet(t *testing.T) {
+	integration.BeforeTest(t)
+	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+	t.Cleanup(func() { clus.Terminate(t) })
+
+	client := clus.Client(0)
+	kv := client.KV
+
+	testGet(t, kv, func() Getter { return kv }, TestGetEvents, nil)
+}
+
+func testGet(t *testing.T, kv clientv3.KV, getReader func() Getter, initialEvents, followupEvents []*clientv3.Event) {
+	ctx := t.Context()
+	t.Log("Setup")
+	lastRev := applyEvents(ctx, t, kv, initialEvents)
+
+	reader := getReader()
+	if c, ok := reader.(*cache.Cache); ok {
+		if err := c.WaitForRevision(ctx, lastRev); err != nil {
+			t.Fatalf("cache never caught up to rev %d: %v", lastRev, err)
+		}
+	}
+
+	lastRev = applyEvents(ctx, t, kv, followupEvents)
+	if c, ok := reader.(*cache.Cache); ok {
+		if err := c.WaitForRevision(ctx, lastRev); err != nil {
+			t.Fatalf("cache never caught up to rev %d: %v", lastRev, err)
+		}
+	}
+
+	t.Log("Validate")
+	for _, tc := range getTestCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			resp, err := reader.Get(ctx, tc.key, tc.opts...)
+			if err != nil {
+				t.Fatalf("Get %q failed: %v", tc.key, err)
+			}
+			if diff := cmp.Diff(tc.wantKVs, resp.Kvs); diff != "" {
+				t.Fatalf("unexpected KVs (-want +got):\n%s", diff)
+			}
+			if resp.Header.Revision != tc.wantRevision {
+				t.Fatalf("revision: got %d, want %d", resp.Header.Revision, tc.wantRevision)
+			}
+		})
+	}
+}
+
+var TestGetEvents = []*clientv3.Event{
+	Rev2PutFooA, Rev3PutFooB, Rev4PutFooC, Rev5PutFooD, Rev6DeleteFooD, Rev7TxnPutFooA, Rev7TxnPutFooB, Rev8PutFooA,
+}
+
+var (
+	Rev2PutFooA = &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/foo/a"),
+			Value:          []byte("a1"),
+			CreateRevision: 2,
+			ModRevision:    2,
+			Version:        1,
+		},
+	}
+	Rev3PutFooB = &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/foo/b"),
+			Value:          []byte("b1"),
+			CreateRevision: 3,
+			ModRevision:    3,
+			Version:        1,
+		},
+	}
+	Rev4PutFooC = &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/foo/c"),
+			Value:          []byte("c1"),
+			CreateRevision: 4,
+			ModRevision:    4,
+			Version:        1,
+		},
+	}
+	Rev5PutFooD = &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/foo/d"),
+			Value:          []byte("d1"),
+			CreateRevision: 5,
+			ModRevision:    5,
+			Version:        1,
+		},
+	}
+	Rev6DeleteFooD = &clientv3.Event{
+		Type: clientv3.EventTypeDelete,
+		Kv: &mvccpb.KeyValue{
+			Key:         []byte("/foo/d"),
+			ModRevision: 6,
+		},
+	}
+	Rev7TxnPutFooA = &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/foo/a"),
+			Value:          []byte("a2"),
+			CreateRevision: 2,
+			ModRevision:    7,
+			Version:        2,
+		},
+	}
+	Rev7TxnPutFooB = &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/foo/b"),
+			Value:          []byte("b2"),
+			CreateRevision: 3,
+			ModRevision:    7,
+			Version:        2,
+		},
+	}
+	Rev8PutFooA = &clientv3.Event{
+		Type: clientv3.EventTypePut,
+		Kv: &mvccpb.KeyValue{
+			Key:            []byte("/foo/a"),
+			Value:          []byte("a3"),
+			CreateRevision: 2,
+			ModRevision:    8,
+			Version:        3,
+		},
+	}
+)
+
+type getTestCase struct {
+	name         string
+	key          string
+	opts         []clientv3.OpOption
+	wantKVs      []*mvccpb.KeyValue
+	wantRevision int64
+}
+
+var getTestCases = []getTestCase{
+	{
+		name:         "single key /foo/a",
+		key:          "/foo/a",
+		opts:         []clientv3.OpOption{clientv3.WithSerializable()},
+		wantKVs:      []*mvccpb.KeyValue{Rev8PutFooA.Kv},
+		wantRevision: 8,
+	},
+	{
+		name:         "non-existing key",
+		key:          "/doesnotexist",
+		opts:         []clientv3.OpOption{clientv3.WithSerializable()},
+		wantKVs:      nil,
+		wantRevision: 8,
+	},
+	{
+		name:         "prefix /foo",
+		key:          "/foo",
+		opts:         []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithPrefix()},
+		wantKVs:      []*mvccpb.KeyValue{Rev8PutFooA.Kv, Rev7TxnPutFooB.Kv, Rev4PutFooC.Kv},
+		wantRevision: 8,
+	},
+	{
+		name:         "range [/foo/a, /foo/c)",
+		key:          "/foo/a",
+		opts:         []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithRange("/foo/c")},
+		wantKVs:      []*mvccpb.KeyValue{Rev8PutFooA.Kv, Rev7TxnPutFooB.Kv},
+		wantRevision: 8,
+	},
+	{
+		name:         "fromKey /foo/b",
+		key:          "/foo/b",
+		opts:         []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithFromKey()},
+		wantKVs:      []*mvccpb.KeyValue{Rev7TxnPutFooB.Kv, Rev4PutFooC.Kv},
+		wantRevision: 8,
+	},
+}
+
+func TestCacheWithPrefixGet(t *testing.T) {
+	integration.BeforeTest(t)
+	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+	t.Cleanup(func() { clus.Terminate(t) })
+	client := clus.Client(0)
+
+	ctx := t.Context()
+
+	tests := []struct {
+		name        string
+		key         string
+		opts        []clientv3.OpOption
+		expectError bool
+	}{
+		{
+			name:        "single key within prefix",
+			key:         "/foo/a",
+			opts:        []clientv3.OpOption{clientv3.WithSerializable()},
+			expectError: false,
+		},
+		{
+			name:        "single key outside prefix returns error",
+			key:         "/bar/a",
+			opts:        []clientv3.OpOption{clientv3.WithSerializable()},
+			expectError: true,
+		},
+		{
+			name:        "prefix() within cache prefix",
+			key:         "/foo",
+			opts:        []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithPrefix()},
+			expectError: false,
+		},
+		{
+			name:        "prefix() outside cache prefix returns error",
+			key:         "/bar",
+			opts:        []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithPrefix()},
+			expectError: true,
+		},
+		{
+			name:        "range within prefix",
+			key:         "/foo/a",
+			opts:        []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithRange("/foo/b")},
+			expectError: false,
+		},
+		{
+			name:        "range crosses cache prefix boundary returns error /foo/a",
+			key:         "/foo/a",
+			opts:        []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithRange("/zzz")},
+			expectError: true,
+		},
+		{
+			name:        "fromKey not allowed when cache has prefix returns error /foo/a",
+			key:         "/foo/a",
+			opts:        []clientv3.OpOption{clientv3.WithSerializable(), clientv3.WithFromKey()},
+			expectError: true,
+		},
+	}
+
+	c, err := cache.New(client, "/foo")
+	if err != nil {
+		t.Fatalf("cache.New: %v", err)
+	}
+	defer c.Close()
+	if err = c.WaitReady(ctx); err != nil {
+		t.Fatalf("cache.WaitReady: %v", err)
+	}
+
+	const testKey = "/foo/a"
+	putResp, err := client.Put(ctx, testKey, "val")
+	if err != nil {
+		t.Fatalf("client.Put(%q, \"val\") failed: %v", testKey, err)
+	}
+	if err := c.WaitForRevision(ctx, putResp.Header.Revision); err != nil {
+		t.Fatalf("cache never caught up to rev %d: %v", putResp.Header.Revision, err)
+	}
+
+	for _, tc := range tests {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			resp, err := c.Get(ctx, tc.key, tc.opts...)
+			if tc.expectError {
+				if !errors.Is(err, cache.ErrKeyRangeInvalid) {
+					t.Fatalf("expected ErrKeyRangeInvalid for Get %q, got: %v", tc.key, err)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("Get %q failed: %v", tc.key, err)
+			}
+			if len(resp.Kvs) == 0 {
+				t.Fatalf("Get %q returned no KVs, expected at least one", tc.key)
+			}
+		})
+	}
+}
+
 func TestCacheLaggingWatcher(t *testing.T) {
 	const prefix = "/test/"
 	integration.BeforeTest(t)
@@ -613,6 +919,48 @@ func TestCacheUnsupportedWatchOptions(t *testing.T) {
 	}
 }
 
+func TestCacheUnsupportedGetOptions(t *testing.T) {
+	integration.BeforeTest(t)
+	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
+	t.Cleanup(func() { clus.Terminate(t) })
+	client := clus.Client(0)
+
+	c, err := cache.New(client, "", cache.WithHistoryWindowSize(1))
+	if err != nil {
+		t.Fatalf("cache.New: %v", err)
+	}
+	defer c.Close()
+	if err := c.WaitReady(t.Context()); err != nil {
+		t.Fatalf("cache not ready: %v", err)
+	}
+
+	unsupported := []struct {
+		name string
+		opts []clientv3.OpOption
+	}{
+		{"WithCountOnly", []clientv3.OpOption{clientv3.WithCountOnly()}},
+		{"WithLimit", []clientv3.OpOption{clientv3.WithLimit(1)}},
+		{"WithSort", []clientv3.OpOption{clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend)}},
+		{"WithPrevKV", []clientv3.OpOption{clientv3.WithPrevKV()}},
+		{"WithMinModRevision", []clientv3.OpOption{clientv3.WithMinModRev(2)}},
+		{"WithMaxModRevision", []clientv3.OpOption{clientv3.WithMaxModRev(10)}},
+		{"WithMinCreateRevision", []clientv3.OpOption{clientv3.WithMinCreateRev(3)}},
+		{"WithMaxCreateRevision", []clientv3.OpOption{clientv3.WithMaxCreateRev(5)}},
+		{"WithRev", []clientv3.OpOption{clientv3.WithRev(123)}},
+		{"NoSerializable", nil},
+	}
+
+	for _, tc := range unsupported {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := c.Get(t.Context(), "foo", tc.opts...)
+			if !errors.Is(err, cache.ErrUnsupportedRequest) {
+				t.Errorf("Get with %s: expected ErrUnsupportedRequest, got %v", tc.name, err)
+			}
+		})
+	}
+}
+
 func TestCacheWatchOldRevisionCompacted(t *testing.T) {
 	integration.BeforeTest(t)
 	clus := integration.NewCluster(t, &integration.ClusterConfig{Size: 1})
@@ -677,6 +1025,10 @@ type Watcher interface {
 	Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan
 }
 
+type Getter interface {
+	Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error)
+}
+
 func collectAndAssertAtomicEvents(t *testing.T, watch clientv3.WatchChan) (events []*clientv3.Event, ok bool) {
 	deadline := time.After(time.Second)
 	var lastRevision int64
@@ -704,3 +1056,59 @@ func collectAndAssertAtomicEvents(t *testing.T, watch clientv3.WatchChan) (event
 		}
 	}
 }
+
+func applyEvents(ctx context.Context, t *testing.T, kv clientv3.KV, evs []*clientv3.Event) int64 {
+	var lastRev int64
+	for _, batches := range batchEventsByRevision(evs) {
+		lastRev = applyEventBatch(ctx, t, kv, batches)
+	}
+	return lastRev
+}
+
+func batchEventsByRevision(events []*clientv3.Event) [][]*clientv3.Event {
+	var batches [][]*clientv3.Event
+	if len(events) == 0 {
+		return batches
+	}
+	start := 0
+	for end := 1; end < len(events); end++ {
+		if events[end].Kv.ModRevision != events[start].Kv.ModRevision {
+			batches = append(batches, events[start:end])
+			start = end
+		}
+	}
+	batches = append(batches, events[start:])
+	return batches
+}
+
+func applyEventBatch(ctx context.Context, t *testing.T, kv clientv3.KV, batch []*clientv3.Event) int64 {
+	ops := make([]clientv3.Op, 0, len(batch))
+	for _, event := range batch {
+		switch event.Type {
+		case clientv3.EventTypePut:
+			ops = append(ops, clientv3.OpPut(string(event.Kv.Key), string(event.Kv.Value)))
+		case clientv3.EventTypeDelete:
+			ops = append(ops, clientv3.OpDelete(string(event.Kv.Key)))
+		default:
+			t.Fatalf("unsupported event type: %v", event.Type)
+		}
+	}
+	resp, err := kv.Txn(ctx).Then(ops...).Commit()
+	if err != nil {
+		t.Fatalf("Txn failed: %v", err)
+	}
+	return resp.Header.Revision
+}
+
+func filterEvents(evs []*clientv3.Event, pred func(int64) bool) []*clientv3.Event {
+	var out []*clientv3.Event
+	for _, ev := range evs {
+		if pred(ev.Kv.ModRevision) {
+			out = append(out, ev)
+		}
+	}
+	return out
+}
+
+func revLessThan(n int64) func(int64) bool     { return func(r int64) bool { return r < n } }
+func revGreaterEqual(n int64) func(int64) bool { return func(r int64) bool { return r >= n } }
diff --git a/tests/robustness/Makefile b/tests/robustness/Makefile
index 0c6068803..68d77d188 100644
--- a/tests/robustness/Makefile
+++ b/tests/robustness/Makefile
@@ -45,6 +45,11 @@ 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' $(TOPLEVEL_MAKE) test-robustness && \
 	 echo "Failed to reproduce" || echo "Successful reproduction"
 
+.PHONY: test-robustness-issue15220
+test-robustness-issue15220: /tmp/etcd-v3.5.7-failpoints/bin
+	GO_TEST_FLAGS='-v --run=TestRobustnessRegression/Issue15220 --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-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' $(TOPLEVEL_MAKE) test-robustness && \
diff --git a/tests/robustness/README.md b/tests/robustness/README.md
index 809c03548..2f17d178f 100644
--- a/tests/robustness/README.md
+++ b/tests/robustness/README.md
@@ -14,7 +14,7 @@ The purpose of these tests is to rigorously validate that etcd maintains its [KV
 | Single node cluster can loose a write on crash [#14370]                      | Aug 2022   | v3.4 or earlier   | User            | Yes, report preceded robustness tests             | `make test-robustness-issue14370`   |
 | Enabling auth can lead to inconsistency [#14571]                             | Oct 2022   | v3.4 or earlier   | User            | No, authorization is not covered.                 |                                     |
 | Inconsistent revision caused by crash during defrag [#14685]                 | Nov 2022   | v3.5              | Robustness      | Yes, after covering defragmentation.              | `make test-robustness-issue14685`   |
-| Watch progress notification not synced with steam [#15220]                   | Jan 2023   | v3.4 or earlier   | User            | Yes, after covering watch progress notification   |                                     |
+| Watch progress notification not synced with steam [#15220]                   | Jan 2023   | v3.4 or earlier   | User            | Yes, after covering watch progress notification   | `make test-robustness-issue15220`   |
 | Watch traveling back in time after network partition [#15271]                | Feb 2023   | v3.4 or earlier   | Robustness      | Yes, after covering network partitions            | `make test-robustness-issue15271`   |
 | Duplicated watch event due to bug in TXN caching [#17247]                    | Jan 2024   | main branch       | Robustness      | Yes, prevented regression in v3.6                 |                                     |
 | Watch events lost during stream starvation [#17529]                          | Mar 2024   | v3.4 or earlier   | User            | Yes, after covering of slow watch                 | `make test-robustness-issue17529`   |
@@ -60,49 +60,52 @@ These tests cover various scenarios, including:
 
 ### Distributed System Terminology
 
-*   **Consensus:** A process where nodes in a distributed system agree on a single data value. Etcd uses the Raft algorithm to achieve consensus.
-*   **Strict vs Eventual consistency:**
-    *   **Strict Consistency:** All components see the same data at the same time after an update.
-    *   **Eventual Consistency:** Components may temporarily see different data after an update but converge to the same view eventually.
-*   **Consistency Models (https://jepsen.io/consistency)**
-    *   **Single-Object Consistency Models:**
-        *   **Sequential Consistency:** A strong single-object model. Operations appear to take place in some total order, consistent with the order of operations on each individual process.
-        *   **Linearizable Consistency:** The strongest single-object model. Operations appear to happen instantly and in order, consistent with real-time ordering.
-    *   **Transactional Consistency Models**
-        *   **Serializable Consistency:** A transactional model guaranteeing that transactions appear to occur in some total order. Operations within a transaction are atomic and do not interleave with other transactions. It's a multi-object property, applying to the entire system, not just individual objects.
-        *   **Strict Serializable Consistency:** The strongest transactional model. Combines the total order of serializability with the real-time ordering constraints of linearizability.
+* **Consensus:** A process where nodes in a distributed system agree on a single data value. Etcd uses the Raft algorithm to achieve consensus.
+* **Strict vs Eventual consistency:**
+  * **Strict Consistency:** All components see the same data at the same time after an update.
+  * **Eventual Consistency:** Components may temporarily see different data after an update but converge to the same view eventually.
+* **Consistency Models (<https://jepsen.io/consistency>)**
+  * **Single-Object Consistency Models:**
+    * **Sequential Consistency:** A strong single-object model. Operations appear to take place in some total order, consistent with the order of operations on each individual process.
+    * **Linearizable Consistency:** The strongest single-object model. Operations appear to happen instantly and in order, consistent with real-time ordering.
+  * **Transactional Consistency Models**
+    * **Serializable Consistency:** A transactional model guaranteeing that transactions appear to occur in some total order. Operations within a transaction are atomic and do not interleave with other transactions. It's a multi-object property, applying to the entire system, not just individual objects.
+    * **Strict Serializable Consistency:** The strongest transactional model. Combines the total order of serializability with the real-time ordering constraints of linearizability.
 
 Etcd provides strict serializability for KV operations and eventual consistency for Watch.
 
 **Etcd Guarantees**
 
-*   **Key-value API operations** https://etcd.io/docs/latest/learning/api_guarantees/#kv-apis
-*   **Watch API guarantees** https://etcd.io/docs/latest/learning/api_guarantees/#watch-apis
+* **Key-value API operations** <https://etcd.io/docs/latest/learning/api_guarantees/#kv-apis>
+* **Watch API guarantees** <https://etcd.io/docs/latest/learning/api_guarantees/#watch-apis>
 
 ### Kubernetes Integration
 
-*   **[Implicit Kubernetes-ETCD Contract]:**  Defines how Kubernetes uses etcd to store cluster state.
-*   **ResourceVersion:**  A string used by Kubernetes to track resource versions, corresponding to etcd revisions.
-*   **Sharding resource types:** Kubernetes treats each resource type as a totally independent entity.
+* **[Implicit Kubernetes-ETCD Contract]:**  Defines how Kubernetes uses etcd to store cluster state.
+* **ResourceVersion:**  A string used by Kubernetes to track resource versions, corresponding to etcd revisions.
+* **Sharding resource types:** Kubernetes treats each resource type as a totally independent entity.
     It allows sharding each resource type into a separate etcd cluster.
 
 [Implicit Kubernetes-ETCD Contract]: https://docs.google.com/document/d/1NUZDiJeiIH5vo_FMaTWf0JtrQKCx0kpEaIIuPoj9P6A/edit?usp=sharing
 
-## Running locally 
+## Running locally
 
 1. Build etcd with failpoints
+
     ```bash
     make gofail-enable
     make build
     make gofail-disable
     ```
+
 2. Run the tests
+
     ```bash
     make test-robustness
     ```
-   
+
     Optionally, you can pass environment variables:
-    * `GO_TEST_FLAGS` - to pass additional arguments to `go test`. 
+    * `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 the location where the results report will be saved.
@@ -120,6 +123,7 @@ Errors in the etcd model could be causing false positives, which makes the abili
    > Note: By default robustness test report is only generated for failed test.
 
    * **For local runs:** this would be by identifying log line, in the following example that would be `/tmp/TestRobustnessExploratory_Etcd_HighTraffic_ClusterOfSize1`:
+
       ```
       logger.go:146: 2024-04-08T09:45:27.734+0200 INFO    Saving robustness test report   {"path": "/tmp/TestRobustnessExploratory_Etcd_HighTraffic_ClusterOfSize1"}
       ```
@@ -152,6 +156,7 @@ Errors in the etcd model could be causing false positives, which makes the abili
 
 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"}
@@ -179,6 +184,7 @@ is mentioned in the `Saving robustness test report` log. Logs from report genera
 ```
 
 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.
@@ -192,31 +198,35 @@ The report follows the hierarchy:
 
 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 the report locally.
+After a couple of tries robustness tests should fail with a log `Linearization illegal` and save the report locally.
 
 Example:
+
 ```
-    logger.go:146: 2024-05-08T10:42:53.379+0200 INFO    Validating linearizable operations      {"timeout": "5m0s"}
-    logger.go:146: 2024-05-08T10:42:54.429+0200 ERROR   Linearization failed    {"duration": "1.050105973s"}
-    validate.go:39: Failed linearization, skipping further validation
-    logger.go:146: 2024-05-08T10:42:54.429+0200 INFO    Saving robustness test report   {"path": "/tmp/TestRobustnessRegression_Issue14370/1715157774429416550"}
+    logger.go:146: 2025-08-01T22:54:26.550+0900 INFO Validating linearizable operations {"timeout": "5m0s"}
+    logger.go:146: 2025-08-01T22:54:26.755+0900 ERROR Linearization illegal {"duration": "205.05225ms"}
+    logger.go:146: 2025-08-01T22:54:26.755+0900 INFO Skipping other validations as linearization failed
+    main_test.go:122: linearization: illegal
+    logger.go:146: 2025-08-01T22:54:26.756+0900 INFO Saving robustness test report {"path": "/tmp/TestRobustnessRegression_Issue14370/1754056466755991000"}
     ...
-    logger.go:146: 2024-05-08T10:42:54.441+0200 INFO    Saving visualization    {"path": "/tmp/TestRobustnessRegression_Issue14370/1715157774429416550/history.html"}
+    logger.go:146: 2025-08-01T22:54:26.850+0900 INFO Saving visualization {"path": "/tmp/TestRobustnessRegression_Issue14370/1754056466755991000/history.html"}
+    logger.go:146: 2025-08-01T22:54:26.878+0900 INFO killing server... {"name": "TestRobustnessRegressionIssue14370-test-0"}
+    logger.go:146: 2025-08-01T22:54:26.878+0900 INFO stopping server... {"name": "TestRobustnessRegressionIssue14370-test-0"}
+    logger.go:146: 2025-08-01T22:54:26.886+0900 INFO stopped server. {"name": "TestRobustnessRegressionIssue14370-test-0"}
 ```
 
 Linearization issues are easiest to analyse via history visualization.
-Open `/tmp/TestRobustnessRegression_Issue14370/1715157774429416550/history.html` file in your browser.
+Open `/tmp/TestRobustnessRegression_Issue14370/1754056466755991000/history.html` file in your browser.
 Jump to the error in linearization by clicking `[ jump to first error ]` on the top of the page.
 
 You should see a graph similar to the one on the image below.
 ![issue14370](readme-images/issue14370.png)
 
 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`. 
+All following requests are invalid (connected with red line) as they have revision `167`.
 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 a watch issue
 
@@ -225,6 +235,7 @@ To reproduce the issue by yourself run `make test-robustness-issue15271`.
 After a couple of tries robustness tests should fail with a logs `Broke watch guarantee` and save report locally.
 
 Example:
+
 ```
     logger.go:146: 2024-05-08T10:50:11.301+0200 INFO    Validating linearizable operations      {"timeout": "5m0s"}
     logger.go:146: 2024-05-08T10:50:15.754+0200 INFO    Linearization success   {"duration": "4.453346487s"}
@@ -245,6 +256,7 @@ For client `4` that broke the watch guarantee open `/tmp/TestRobustnessRegressio
 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}, ...
@@ -256,4 +268,3 @@ If you follow the `revision` throughout the file you should notice that watch re
 This is incorrect and breaks `Ordered` [watch API guarantees].
 This is consistent with the root cause of [#15271] where the member reconnecting to cluster will resend revisions.
 
-[#15271]: https://github.com/etcd-io/etcd/issues/15271
diff --git a/tests/robustness/coverage/coverage_test.go b/tests/robustness/coverage/coverage_test.go
index 9faf97eda..d4ca35a8e 100644
--- a/tests/robustness/coverage/coverage_test.go
+++ b/tests/robustness/coverage/coverage_test.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -30,6 +31,121 @@ import (
 	"google.golang.org/protobuf/encoding/protojson"
 )
 
+type column struct {
+	name string
+
+	// matcher encodes column with boolean values.
+	matcher Matcher
+}
+
+type method struct {
+	name string
+
+	// matcher should be a tight filter that returns true only if the method was
+	// used to make a call to Etcd (that produced the matching trace).
+	matcher Matcher
+}
+
+type refOp struct {
+	// args splits traces into buckets based on Matcher or Modeler results.
+	args []column
+	// methods tries to associate Matched with method name.
+	methods []method
+
+	keyAttrName string
+}
+
+type row struct {
+	method, pattern, args string
+}
+
+const notMatched byte = ' '
+
+var referenceUsageOfEtcdAPI = map[string]refOp{
+	"etcdserverpb.KV/Range": {
+		// All calls should go through etcd-k8s interface
+		args: []column{
+			{name: "limit", matcher: isLimitSet},
+			{name: "rangeEnd", matcher: isRangeEndSet},
+			{name: "rev", matcher: isRevisionSet},
+			{name: "countOnly", matcher: isCountOnly},
+			{name: "keysOnly", matcher: isKeysOnly},
+		},
+		keyAttrName: "range_begin",
+		methods: []method{
+			{
+				name:    "Healthcheck",
+				matcher: keyIsEqualStr("range_begin", "/registry/health"),
+			},
+			{
+				name:    "Compaction",
+				matcher: keyIsEqualStr("range_begin", "compact_rev_key"),
+			},
+			{
+				name: "Get",
+				matcher: andMatcher(
+					keyIsEqualInt("limit", 1),
+					notMatcher(isRangeEndSet),
+				),
+			},
+			{
+				name: "Count",
+				matcher: andMatcher(
+					isCountOnly,
+					isRangeEndSet,
+					notMatcher(orMatcher(isKeysOnly, isLimitSet, isRevisionSet)),
+				),
+			},
+			{
+				name: "List",
+				matcher: andMatcher(
+					isRangeEndSet,
+					notMatcher(orMatcher(isKeysOnly, isCountOnly)),
+				),
+			},
+			{
+				name: "GetCurrentRevision",
+				matcher: andMatcher(
+					keyIsEqualInt("limit", 1),
+					isRangeEndSet,
+					notMatcher(orMatcher(isKeysOnly, isCountOnly, isRevisionSet)),
+				),
+			},
+		},
+	},
+	"etcdserverpb.KV/Txn": {
+		// All calls should go through etcd-k8s interface
+		args: []column{
+			{name: "getOnFailure", matcher: keyIsEqualInt("failure_len", 1)},
+			{name: "readOnly", matcher: isReadOnly},
+		},
+		keyAttrName: "compare_first_key",
+		methods: []method{
+			{
+				name:    "Compaction",
+				matcher: keyIsEqualStr("compare_first_key", "compact_rev_key"),
+			},
+			{
+				name:    "OptimisticPutOrDelete",
+				matcher: andMatcher(keyIsEqualInt("compare_len", 1), keyIsEqualInt("success_len", 1), notMatcher(isReadOnly)),
+			},
+		},
+	},
+	"etcdserverpb.KV/Compact": {
+		// Compaction should move to using internal Etcd mechanism
+		// Discussed in https://github.com/kubernetes/kubernetes/issues/80513
+	},
+	"etcdserverpb.Watch/Watch": {
+		// Not part of the contract interface (yet)
+	},
+	"etcdserverpb.Lease/LeaseGrant": {
+		// Used to manage masterleases and events
+	},
+	"etcdserverpb.Maintenance/Status": {
+		// Used to expose database size on apiserver's metrics endpoint
+	},
+}
+
 func TestInterfaceUse(t *testing.T) {
 	files, err := os.ReadDir("testdata")
 	if err != nil {
@@ -60,39 +176,128 @@ func testInterfaceUse(t *testing.T, filename string) {
 	}
 	traces := dump.Result
 
-	callsByOperationName := make(map[string]int)
+	callsByOperationName := make(map[string][]*tracev1.ResourceSpans)
 	for _, trace := range traces.GetResourceSpans() {
-		serviceName := getServiceName(trace)
-		if serviceName != "etcd" {
+		if getServiceName(trace) != "etcd" {
 			continue
 		}
 		opName := getOperationName(trace)
-		callsByOperationName[opName]++
+		// Skip put key=_test after healthcheck in command tests.
+		// https://github.com/kubernetes/kubernetes/blob/release-1.33/hack/lib/etcd.sh#L93
+		if opName == "etcdserverpb.KV/Put" && isCommandTestHC(trace) {
+			continue
+		}
+		callsByOperationName[opName] = append(callsByOperationName[opName], trace)
+	}
+	if c := len(callsByOperationName[""]); c > 0 {
+		t.Logf("Found traces that did not go through gRPC: %d", c)
+		delete(callsByOperationName, "") // Ignoring them.
 	}
 	t.Logf("\n%s", printableCallTable(callsByOperationName))
 
-	knownMethodsUsedByKubernetes := map[string]bool{
-		"etcdserverpb.KV/Range":           true, // All calls should go through etcd-k8s interface
-		"etcdserverpb.KV/Txn":             true, // All calls should go through etcd-k8s interface
-		"etcdserverpb.KV/Compact":         true, // Compaction should move to using internal Etcd mechanism
-		"etcdserverpb.Watch/Watch":        true, // Not part of the contract interface (yet)
-		"etcdserverpb.Lease/LeaseGrant":   true, // Used to manage masterleases and events
-		"etcdserverpb.Maintenance/Status": true, // Used to expose database size on apiserver's metrics endpoint
-	}
-	for method := range knownMethodsUsedByKubernetes {
-		t.Run(method, func(t *testing.T) {
-			if _, ok := callsByOperationName[method]; !ok {
-				t.Errorf("expected %q method to be called at least once", method)
-			}
-		})
-	}
 	t.Run("only_expected_methods_were_called", func(t *testing.T) {
-		for opName := range callsByOperationName {
-			if !knownMethodsUsedByKubernetes[opName] {
-				t.Errorf("method called outside the list: %s", opName)
+		for opName, traces := range callsByOperationName {
+			if _, ok := referenceUsageOfEtcdAPI[opName]; !ok {
+				t.Logf("Example trace to the unknown method: %+v", traces[0])
+				t.Errorf("unexpected %d calls to method: %s", len(traces), opName)
 			}
 		}
 	})
+
+	for op, td := range referenceUsageOfEtcdAPI {
+		t.Run(op, func(t *testing.T) {
+			if _, ok := callsByOperationName[op]; !ok {
+				t.Fatalf("expected %q method to be called at least once", op)
+			}
+
+			if len(td.args) == 0 {
+				return
+			}
+
+			// tracesWithNoMethod ensures that we print error only once when a
+			// new call pattern is found.
+			tracesWithNoMethod := make(map[string]bool)
+
+			callCounts := make(map[row]int)
+			for _, trace := range callsByOperationName[op] {
+				args := columnsToArgs(trace, td.args)
+
+				pattern, pFound := extractPattern(trace, td.keyAttrName)
+				if !pFound && !tracesWithNoMethod[args] {
+					t.Errorf("New key pattern detected: %s", pattern)
+				}
+
+				method, mFound := extractMethod(td.methods, trace)
+				if !mFound && !tracesWithNoMethod[args] {
+					t.Errorf("New call pattern detected: %s(key=%s,%s)", op, pattern, argsToDescription(args, td.args))
+					tracesWithNoMethod[args] = true
+				}
+
+				callCounts[row{method, pattern, args}]++
+			}
+
+			t.Logf("\n%s", printableMatcherTable(td.args, callCounts))
+		})
+	}
+}
+
+func extractPattern(trace *tracev1.ResourceSpans, key string) (string, bool) {
+	k, found := strAttr(trace, key)
+	if !found {
+		return "", false
+	}
+	if k == "/registry/health" || k == "compact_rev_key" {
+		return k, true
+	}
+	if !strings.HasPrefix(k, "/registry") {
+		return k, false
+	}
+	suffix := ""
+	if strings.HasSuffix(k, "/") {
+		suffix = "/"
+	}
+	switch strings.Count(strings.TrimRight(k, "/"), "/") {
+	case 1:
+		return "/registry" + suffix, true
+	case 2:
+		return "/registry/{resource}" + suffix, true
+	case 3:
+		return "/registry/{resource}/{namespace}" + suffix, true
+	case 4:
+		return "/registry/{resource}/{namespace}/{name}" + suffix, true
+	case 5:
+		return "/registry/{api-group}/{resource}/{namespace}/{name}" + suffix, true
+	}
+	return k, false
+}
+
+func columnsToArgs(trace *tracev1.ResourceSpans, cols []column) string {
+	acc := make([]byte, len(cols))
+	for i, col := range cols {
+		if col.matcher(trace) {
+			acc[i] = 'X'
+		} else {
+			acc[i] = notMatched
+		}
+	}
+	return string(acc)
+}
+
+func argsToDescription(matched string, cols []column) string {
+	ret := make([]string, len(cols))
+	for i, col := range cols {
+		ret[i] = fmt.Sprintf("%s=%v", col.name, matched[i] != notMatched)
+	}
+	return strings.Join(ret, ",")
+}
+
+func extractMethod(methodToMatched []method, trace *tracev1.ResourceSpans) (string, bool) {
+	for _, mm := range methodToMatched {
+		if mm.matcher(trace) {
+			return mm.name, true
+		}
+	}
+	return "", false
 }
 
 type Traces struct {
@@ -128,7 +333,7 @@ func getOperationName(trace *tracev1.ResourceSpans) string {
 	return ""
 }
 
-func printableCallTable(callsByOperationName map[string]int) string {
+func printableCallTable(callsByOperationName map[string][]*tracev1.ResourceSpans) string {
 	buf := new(bytes.Buffer)
 	cfgBuilder := tablewriter.NewConfigBuilder().WithRowAlignment(tw.AlignRight)
 	table := tablewriter.NewTable(buf, tablewriter.WithConfig(cfgBuilder.Build()))
@@ -136,10 +341,11 @@ func printableCallTable(callsByOperationName map[string]int) string {
 
 	totalCalls := 0
 	for _, c := range callsByOperationName {
-		totalCalls += c
+		totalCalls += len(c)
 	}
 
-	for opName, callCount := range callsByOperationName {
+	for opName, calls := range callsByOperationName {
+		callCount := len(calls)
 		table.Append(opName, callCount, fmt.Sprintf("%.2f%%", float64(callCount*100)/float64(totalCalls)))
 	}
 	table.Footer("total", totalCalls, "100.00%")
@@ -147,3 +353,61 @@ func printableCallTable(callsByOperationName map[string]int) string {
 	table.Render()
 	return buf.String()
 }
+
+func printableMatcherTable(cols []column, res map[row]int) string {
+	buf := new(bytes.Buffer)
+	width := 2 + len(cols) + 2
+	alignment := make([]tw.Align, width)
+	alignment[1] = tw.AlignLeft
+	cfgBuilder := tablewriter.NewConfigBuilder().
+		WithRowAlignment(tw.AlignRight).
+		Row().Alignment().WithPerColumn(alignment).Build()
+	table := tablewriter.NewTable(buf, tablewriter.WithConfig(cfgBuilder.Build()))
+
+	hdr := make([]string, width)
+	hdr[0] = "method"
+	hdr[1] = "pattern"
+	for i, col := range cols {
+		hdr[i+2] = col.name
+	}
+	hdr[len(hdr)-2] = "calls"
+	hdr[len(hdr)-1] = "percent"
+	table.Header(hdr)
+
+	totalCalls := 0
+	for _, c := range res {
+		totalCalls += c
+	}
+
+	footer := make([]int, len(cols))
+	for r, callCount := range res {
+		rowPrefix := make([]string, len(cols))
+		for i := range cols {
+			rowPrefix[i] = string(r.args[i])
+			if r.args[i] != notMatched {
+				footer[i] += callCount
+			}
+		}
+
+		table.Append(append(
+			[]string{r.method, r.pattern},
+			append(rowPrefix,
+				strconv.Itoa(callCount),
+				fmt.Sprintf("%.2f%%", float64(callCount*100)/float64(totalCalls)),
+			)...))
+	}
+
+	footerStr := make([]string, len(cols))
+	for i := range footer {
+		footerStr[i] = strconv.Itoa(footer[i])
+	}
+	table.Footer(append([]string{"", ""},
+		append(
+			footerStr,
+			strconv.Itoa(totalCalls),
+			"100.00%",
+		)...))
+
+	table.Render()
+	return buf.String()
+}
diff --git a/tests/robustness/coverage/matchers_test.go b/tests/robustness/coverage/matchers_test.go
new file mode 100644
index 000000000..62935edde
--- /dev/null
+++ b/tests/robustness/coverage/matchers_test.go
@@ -0,0 +1,152 @@
+// 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 coverage_test
+
+import (
+	tracev1 "go.opentelemetry.io/proto/otlp/trace/v1"
+)
+
+// Matcher returns true if trace passes the filter.
+type Matcher func(trace *tracev1.ResourceSpans) bool
+
+// isCommandTestHC matches trace produced by curl call to verify Etcd instance
+// is working in command tests.
+func isCommandTestHC(trace *tracev1.ResourceSpans) bool {
+	for _, scopeSpan := range trace.GetScopeSpans() {
+		for _, span := range scopeSpan.GetSpans() {
+			if span.GetName() != "put" {
+				continue
+			}
+			for _, attr := range span.GetAttributes() {
+				if attr.GetKey() == "key" {
+					return attr.GetValue().GetStringValue() == "_test"
+				}
+			}
+		}
+	}
+	return false
+}
+
+//nolint:unparam
+func keyIsEqualInt(key string, want int) Matcher {
+	return func(trace *tracev1.ResourceSpans) bool {
+		got, found := intAttr(trace, key)
+		return found && got == want
+	}
+}
+
+func isLimitSet(trace *tracev1.ResourceSpans) bool {
+	limit, found := intAttr(trace, "limit")
+	return found && limit > 0
+}
+
+func isRevisionSet(trace *tracev1.ResourceSpans) bool {
+	rev, found := intAttr(trace, "rev")
+	return found && rev > 0
+}
+
+func intAttr(trace *tracev1.ResourceSpans, key string) (int, bool) {
+	for _, scopeSpan := range trace.GetScopeSpans() {
+		for _, span := range scopeSpan.GetSpans() {
+			for _, attr := range span.GetAttributes() {
+				if attr.GetKey() == key {
+					return int(attr.GetValue().GetIntValue()), true
+				}
+			}
+		}
+	}
+	return 0, false
+}
+
+func keyIsEqualStr(key string, want string) Matcher {
+	return func(trace *tracev1.ResourceSpans) bool {
+		got, found := strAttr(trace, key)
+		return found && got == want
+	}
+}
+
+func isRangeEndSet(trace *tracev1.ResourceSpans) bool {
+	rangeEnd, found := strAttr(trace, "range_end")
+	return found && len(rangeEnd) > 0
+}
+
+func strAttr(trace *tracev1.ResourceSpans, key string) (string, bool) {
+	for _, scopeSpan := range trace.GetScopeSpans() {
+		for _, span := range scopeSpan.GetSpans() {
+			for _, attr := range span.GetAttributes() {
+				if attr.GetKey() == key {
+					return attr.Value.GetStringValue(), true
+				}
+			}
+		}
+	}
+	return "", false
+}
+
+func isReadOnly(trace *tracev1.ResourceSpans) bool {
+	isRO, found := boolAttr(trace, "read_only")
+	return found && isRO
+}
+
+func isKeysOnly(trace *tracev1.ResourceSpans) bool {
+	isKeys, found := boolAttr(trace, "keys_only")
+	return found && isKeys
+}
+
+func isCountOnly(trace *tracev1.ResourceSpans) bool {
+	isCount, found := boolAttr(trace, "count_only")
+	return found && isCount
+}
+
+func boolAttr(trace *tracev1.ResourceSpans, key string) (bool, bool) {
+	for _, scopeSpan := range trace.GetScopeSpans() {
+		for _, span := range scopeSpan.GetSpans() {
+			for _, attr := range span.GetAttributes() {
+				if attr.GetKey() == key {
+					return attr.Value.GetBoolValue(), true
+				}
+			}
+		}
+	}
+	return false, false
+}
+
+func orMatcher(l ...Matcher) Matcher {
+	return func(trace *tracev1.ResourceSpans) bool {
+		for _, m := range l {
+			if m(trace) {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+func andMatcher(l ...Matcher) Matcher {
+	return func(trace *tracev1.ResourceSpans) bool {
+		for _, m := range l {
+			if !m(trace) {
+				return false
+			}
+		}
+		return true
+	}
+}
+
+func notMatcher(m Matcher) Matcher {
+	return func(trace *tracev1.ResourceSpans) bool {
+		return !m(trace)
+	}
+}
diff --git a/tests/robustness/report/wal.go b/tests/robustness/report/wal.go
index 83ad793f7..08663711c 100644
--- a/tests/robustness/report/wal.go
+++ b/tests/robustness/report/wal.go
@@ -20,9 +20,9 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"reflect"
 	"strings"
 
-	"github.com/google/go-cmp/cmp"
 	"go.uber.org/zap"
 
 	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
@@ -58,106 +58,121 @@ func PersistedRequestsCluster(lg *zap.Logger, cluster *e2e.EtcdProcessCluster) (
 }
 
 func PersistedRequests(lg *zap.Logger, dataDirs []string) ([]model.EtcdRequest, error) {
-	return persistedRequests(lg, dataDirs, requestsPersistedInWAL)
-}
-
-func persistedRequests(lg *zap.Logger, dataDirs []string, reader persistedRequestReaderFunc) ([]model.EtcdRequest, error) {
 	if len(dataDirs) == 0 {
 		return nil, errors.New("no data dirs")
 	}
-	// Allow failure in minority of etcd cluster.
-	allowedFailures := len(dataDirs) / 2
-	memberRequestHistories := make([][]model.EtcdRequest, 0, len(dataDirs))
-	for _, dir := range dataDirs {
-		requests, err := reader(lg, dir)
+	entriesPersistedInWAL := make([][]raftpb.Entry, len(dataDirs))
+	for i, dir := range dataDirs {
+		_, entries, err := ReadWAL(lg, dir)
 		if err != nil {
-			if allowedFailures < 1 {
-				return nil, err
-			}
-			allowedFailures--
-			continue
+			lg.Error("Failed to read WAL", zap.Error(err), zap.String("data-dir", dir))
 		}
-		memberRequestHistories = append(memberRequestHistories, requests)
-	}
-	// Return empty history if all histories were empty/failed to read.
-	if len(memberRequestHistories) == 0 {
-		return []model.EtcdRequest{}, nil
+		entriesPersistedInWAL[i] = entries
 	}
-	// Each history collects votes from each history that it matches.
-	votes := make([]int, len(memberRequestHistories))
-	lastDiff := ""
-	for i := 0; i < len(memberRequestHistories); i++ {
-		for j := 0; j < len(memberRequestHistories); j++ {
-			if i == j {
-				// history votes for itself
-				votes[i]++
-				continue
-			}
-			if i > j {
-				// avoid comparing things twice
-				continue
-			}
-			first := memberRequestHistories[i]
-			second := memberRequestHistories[j]
-			minLength := min(len(first), len(second))
-			if diff := cmp.Diff(first[:minLength], second[:minLength]); diff == "" {
-				votes[i]++
-				votes[j]++
-			} else {
-				lastDiff = diff
-			}
-		}
+	entries, err := mergeMembersEntries(entriesPersistedInWAL)
+	if err != nil {
+		return nil, err
 	}
-	// Select longest history that has votes from quorum.
-	longestHistory := []model.EtcdRequest{}
-	quorum := len(dataDirs)/2 + 1
-	foundQuorum := false
-	for i := 0; i < len(memberRequestHistories); i++ {
-		if votes[i] < quorum {
+	persistedRequests := make([]model.EtcdRequest, 0, len(entries))
+	for _, e := range entries {
+		if e.Type != raftpb.EntryNormal {
 			continue
 		}
-		// There cannot be incompabible histories supported by quorum
-		minLength := min(len(memberRequestHistories[i]), len(longestHistory))
-		if diff := cmp.Diff(memberRequestHistories[i][:minLength], longestHistory[:minLength]); diff != "" {
-			lastDiff = diff
-			foundQuorum = false
-			break
-		}
-		foundQuorum = true
-		if len(memberRequestHistories[i]) > len(longestHistory) {
-			longestHistory = memberRequestHistories[i]
+		request, err := parseEntryNormal(e)
+		if err != nil {
+			return nil, err
 		}
-	}
-	if !foundQuorum {
-		if lastDiff != "" {
-			fmt.Printf("Difference between WAL:\n%s", lastDiff) // zap doesn't nicely writes multiline strings like diff
+		if request != nil {
+			persistedRequests = append(persistedRequests, *request)
 		}
-		return nil, errors.New("unexpected differences between wal entries")
 	}
-	return longestHistory, nil
+	return persistedRequests, nil
 }
 
-type persistedRequestReaderFunc = func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error)
-
-func requestsPersistedInWAL(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-	_, ents, err := ReadWAL(lg, dataDir)
-	if err != nil {
-		return nil, err
+func mergeMembersEntries(memberEntries [][]raftpb.Entry) ([]raftpb.Entry, error) {
+	for _, entries := range memberEntries {
+		var lastIndex uint64
+		for _, e := range entries {
+			if e.Index <= lastIndex {
+				return nil, fmt.Errorf("raft index should increase, got: %d, previous: %d", e.Index, lastIndex)
+			}
+			lastIndex = e.Index
+		}
 	}
-	requests := make([]model.EtcdRequest, 0, len(ents))
-	for _, ent := range ents {
-		if ent.Type != raftpb.EntryNormal || len(ent.Data) == 0 {
-			continue
+	memberIndices := make([]int, len(memberEntries))
+	mergedHistory := []raftpb.Entry{}
+	var raftIndex uint64
+	for {
+		// Find entry with raftIndex.
+		raftIndex++
+		entriesLeft := false
+		for i, entries := range memberEntries {
+			memberIndex := memberIndices[i]
+			for memberIndex < len(entries) && entries[memberIndex].Index < raftIndex {
+				memberIndex++
+			}
+			if memberIndex < len(entries) {
+				entriesLeft = true
+			}
+			memberIndices[i] = memberIndex
 		}
-		request, err := parseEntryNormal(ent)
-		if err != nil {
-			return nil, err
+		if !entriesLeft {
+			break
 		}
-		if request != nil {
-			requests = append(requests, *request)
+		// Entries collects votes from matching entries.
+		votes := make([]int, len(memberEntries))
+		for i := 0; i < len(memberEntries); i++ {
+			if len(memberEntries[i]) <= memberIndices[i] {
+				continue
+			}
+			entry1 := memberEntries[i][memberIndices[i]]
+			if entry1.Index != raftIndex {
+				continue
+			}
+			for j := i; j < len(memberEntries); j++ {
+				if i == j {
+					votes[i]++
+					continue
+				}
+				if len(memberEntries[j]) <= memberIndices[j] {
+					continue
+				}
+				entry2 := memberEntries[j][memberIndices[j]]
+				if entry2.Index != raftIndex {
+					continue
+				}
+				if reflect.DeepEqual(entry1, entry2) {
+					votes[i]++
+					votes[j]++
+				}
+			}
+		}
+		// Select entry with most votes
+		topVotes := 0
+		for _, vote := range votes {
+			if vote > topVotes {
+				topVotes = vote
+			}
 		}
+		if topVotes == 0 {
+			return nil, fmt.Errorf("no entry for raft index %d", raftIndex)
+		}
+		var entryWithMostVotes *raftpb.Entry
+		for i, vote := range votes {
+			if vote != topVotes {
+				continue
+			}
+			if entryWithMostVotes != nil && !reflect.DeepEqual(*entryWithMostVotes, memberEntries[i][memberIndices[i]]) {
+				return nil, fmt.Errorf("mismatching entries on raft index %d", raftIndex)
+			}
+			entryWithMostVotes = &memberEntries[i][memberIndices[i]]
+		}
+		mergedHistory = append(mergedHistory, *entryWithMostVotes)
+	}
+	if len(mergedHistory) == 0 {
+		return nil, errors.New("no WAL entries matched")
 	}
-	return requests, nil
+	return mergedHistory, nil
 }
 
 func ReadWAL(lg *zap.Logger, dataDir string) (state raftpb.HardState, ents []raftpb.Entry, err error) {
@@ -195,6 +210,9 @@ func ReadWAL(lg *zap.Logger, dataDir string) (state raftpb.HardState, ents []raf
 
 func parseEntryNormal(ent raftpb.Entry) (*model.EtcdRequest, error) {
 	var raftReq pb.InternalRaftRequest
+	if len(ent.Data) == 0 {
+		return nil, nil
+	}
 	if err := raftReq.Unmarshal(ent.Data); err != nil {
 		var r pb.Request
 		isV2Entry := pbutil.MaybeUnmarshal(&r, ent.Data)
diff --git a/tests/robustness/report/wal_test.go b/tests/robustness/report/wal_test.go
index e047f9076..5ff9f34e6 100644
--- a/tests/robustness/report/wal_test.go
+++ b/tests/robustness/report/wal_test.go
@@ -15,349 +15,315 @@
 package report
 
 import (
-	"errors"
 	"testing"
 
 	"github.com/stretchr/testify/require"
-	"go.uber.org/zap"
-	"go.uber.org/zap/zaptest"
 
-	"go.etcd.io/etcd/tests/v3/robustness/model"
+	"go.etcd.io/raft/v3/raftpb"
 )
 
-func TestPersistedRequests(t *testing.T) {
-	lg := zaptest.NewLogger(t)
-
+func TestMergeMemberEntries(t *testing.T) {
 	tcs := []struct {
-		name           string
-		dataDirs       []string
-		readerFunc     func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error)
-		expectErr      string
-		expectRequests []model.EtcdRequest
+		name          string
+		memberEntries [][]raftpb.Entry
+		expectErr     string
+		expectEntries []raftpb.Entry
 	}{
 		{
-			name:      "Error when empty data dir",
-			dataDirs:  []string{},
-			expectErr: "no data dirs",
+			name:          "Error when empty data dir",
+			memberEntries: [][]raftpb.Entry{},
+			expectErr:     "no WAL entries matched",
 		},
 		{
-			name:     "Success when no entries",
-			dataDirs: []string{"etcd0"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				return []model.EtcdRequest{}, nil
+			name: "Success when no entries",
+			memberEntries: [][]raftpb.Entry{
+				{},
 			},
-			expectRequests: []model.EtcdRequest{},
+			expectErr: "no WAL entries matched",
 		},
 		{
-			name:     "Error when error on single node cluster",
-			dataDirs: []string{"etcd0"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				return []model.EtcdRequest{}, errors.New("error reading")
+			name: "Error when one member cluster didn't observed index",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
 			},
-			expectErr: "error reading",
+			expectErr: "no entry for raft index 2",
 		},
 		{
-			name:     "Success when one member cluster",
-			dataDirs: []string{"etcd0"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				return []model.EtcdRequest{
-					{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-					{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-					{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-				}, nil
+			name: "Error when entries index unordered",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+				},
 			},
-			expectRequests: []model.EtcdRequest{
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
+			expectErr: "raft index should increase, got: 1, previous: 3",
+		},
+		{
+			name: "Error when entries index duplicated",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+				},
 			},
+			expectErr: "raft index should increase, got: 1, previous: 1",
 		},
 		{
-			name:     "Success when three members agree on entries",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				return []model.EtcdRequest{
-					{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-					{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-					{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-				}, nil
+			name: "Success when one member cluster",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
 			},
-			expectRequests: []model.EtcdRequest{
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
+			expectEntries: []raftpb.Entry{
+				{Index: 1, Data: []byte("a")},
+				{Index: 2, Data: []byte("b")},
+				{Index: 3, Data: []byte("c")},
 			},
 		},
 		{
-			name:     "Success when three member have no entries",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				return []model.EtcdRequest{}, nil
+			name: "Success when three members agree on entries",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+			},
+			expectEntries: []raftpb.Entry{
+				{Index: 1, Data: []byte("a")},
+				{Index: 2, Data: []byte("b")},
+				{Index: 3, Data: []byte("c")},
 			},
-			expectRequests: []model.EtcdRequest{},
 		},
 		{
-			name:     "Success when one member returned error in three node cluster",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd1":
-					return []model.EtcdRequest{}, errors.New("error reading")
-				default:
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				}
-			},
-			expectRequests: []model.EtcdRequest{
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
+			name: "Success when three members have no entries",
+			memberEntries: [][]raftpb.Entry{
+				{}, {}, {},
 			},
+			expectErr: "no WAL entries matched",
 		},
 		{
-			name:     "Success when one member returned empty in three node cluster",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd1":
-					return []model.EtcdRequest{}, nil
-				default:
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				}
+			name: "Success when one member has no entries in three node cluster",
+			memberEntries: [][]raftpb.Entry{
+				{},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
 			},
-			expectRequests: []model.EtcdRequest{
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
+			expectEntries: []raftpb.Entry{
+				{Index: 1, Data: []byte("a")},
+				{Index: 2, Data: []byte("b")},
+				{Index: 3, Data: []byte("c")},
 			},
 		},
 		{
-			name:     "Error when two members returned error in three node cluster",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd1", "etcd2":
-					return []model.EtcdRequest{}, errors.New("error reading")
-				default:
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				}
+			name: "Success if two members have no entries in three node cluster",
+			memberEntries: [][]raftpb.Entry{
+				{},
+				{},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+			},
+			expectEntries: []raftpb.Entry{
+				{Index: 1, Data: []byte("a")},
+				{Index: 2, Data: []byte("b")},
+				{Index: 3, Data: []byte("c")},
 			},
-			expectErr: "error reading",
 		},
 		{
-			name:     "Success if members didn't observe whole history",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd0":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				case "etcd1":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-					}, nil
-				case "etcd2":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-					}, nil
-				default:
-					panic("unexpected")
-				}
+			name: "Success if members didn't observe whole history",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+				},
+				{
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
 			},
-			expectRequests: []model.EtcdRequest{
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
+			expectEntries: []raftpb.Entry{
+				{Index: 1, Data: []byte("a")},
+				{Index: 2, Data: []byte("b")},
+				{Index: 3, Data: []byte("c")},
 			},
 		},
 		{
-			name:     "Success if only one member observed history",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd0":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				case "etcd1", "etcd2":
-					return []model.EtcdRequest{}, nil
-				default:
-					panic("unexpected")
-				}
+			name: "Success if members observed only one part of history",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+				},
+				{
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+				},
+				{
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
 			},
-			expectRequests: []model.EtcdRequest{
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
+			expectEntries: []raftpb.Entry{
+				{Index: 1, Data: []byte("a")},
+				{Index: 2, Data: []byte("b")},
+				{Index: 3, Data: []byte("c")},
 			},
 		},
 		{
-			name:     "Success when one member observed different last entry",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd0":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				case "etcd1":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				case "etcd2":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 4}},
-					}, nil
-				default:
-					panic("unexpected")
-				}
+			name: "Error when in three member cluster if no members observed index",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
 			},
-			expectRequests: []model.EtcdRequest{
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-				{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
+			expectErr: "no entry for raft index 2",
+		},
+		{
+			name: "Success if only one member observed history",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{},
+				{},
+			},
+			expectEntries: []raftpb.Entry{
+				{Index: 1, Data: []byte("a")},
+				{Index: 2, Data: []byte("b")},
+				{Index: 3, Data: []byte("c")},
 			},
 		},
 		{
-			name:     "Error when one member didn't observe whole history and others observed different last entry",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd0":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-					}, nil
-				case "etcd1":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				case "etcd2":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 4}},
-					}, nil
-				default:
-					panic("unexpected")
-				}
+			name: "Success when one member observed different last entry",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("x")},
+				},
+			},
+			expectEntries: []raftpb.Entry{
+				{Index: 1, Data: []byte("a")},
+				{Index: 2, Data: []byte("b")},
+				{Index: 3, Data: []byte("c")},
 			},
-			expectErr: "unexpected differences between wal entries",
 		},
 		{
-			name:     "Error when three members observed different last entry",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd0":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				case "etcd1":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 4}},
-					}, nil
-				case "etcd2":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 5}},
-					}, nil
-				default:
-					panic("unexpected")
-				}
+			name: "Error when one member didn't observe whole history and others observed different last entry",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("x")},
+				},
 			},
-			expectErr: "unexpected differences between wal entries",
+			expectErr: "mismatching entries on raft index 3",
 		},
 		{
-			name:     "Error when one member returned error and others differ on last entry",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd0":
-					return []model.EtcdRequest{}, errors.New("error reading")
-				case "etcd1":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				case "etcd2":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 4}},
-					}, nil
-				default:
-					panic("unexpected")
-				}
+			name: "Error when three members observed different last entry",
+			memberEntries: [][]raftpb.Entry{
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("x")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("y")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("a")},
+					raftpb.Entry{Index: 2, Data: []byte("z")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
 			},
-			expectErr: "unexpected differences between wal entries",
+			expectErr: "mismatching entries on raft index 2",
 		},
 		{
-			name:     "Error when one member observed empty history and others differ on last entry",
-			dataDirs: []string{"etcd0", "etcd1", "etcd2"},
-			readerFunc: func(lg *zap.Logger, dataDir string) ([]model.EtcdRequest, error) {
-				switch dataDir {
-				case "etcd0":
-					return []model.EtcdRequest{}, nil
-				case "etcd1":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 3}},
-					}, nil
-				case "etcd2":
-					return []model.EtcdRequest{
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 1}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 2}},
-						{Type: model.Compact, Compact: &model.CompactRequest{Revision: 4}},
-					}, nil
-				default:
-					panic("unexpected")
-				}
+			name: "Error when one member observed empty history and others differ on last entry",
+			memberEntries: [][]raftpb.Entry{
+				{},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("x")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
+				{
+					raftpb.Entry{Index: 1, Data: []byte("y")},
+					raftpb.Entry{Index: 2, Data: []byte("b")},
+					raftpb.Entry{Index: 3, Data: []byte("c")},
+				},
 			},
-			expectErr: "unexpected differences between wal entries",
+			expectErr: "mismatching entries on raft index 1",
 		},
 	}
 	for _, tc := range tcs {
 		t.Run(tc.name, func(t *testing.T) {
-			requests, err := persistedRequests(lg, tc.dataDirs, tc.readerFunc)
+			entries, err := mergeMembersEntries(tc.memberEntries)
 			if tc.expectErr == "" {
 				require.NoError(t, err)
 			} else {
 				require.ErrorContains(t, err, tc.expectErr)
 			}
-			require.Equal(t, tc.expectRequests, requests)
+			require.Equal(t, tc.expectEntries, entries)
 		})
 	}
 }