diff --git a/go.mod b/go.mod
index 2ce031f..4342b05 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,5 @@
 module golang.org/x/mod
 
-go 1.25.0
+go 1.26.0
 
-require golang.org/x/tools v0.45.0 // tagx:ignore
+require golang.org/x/tools v0.49.0 // tagx:ignore
diff --git a/go.sum b/go.sum
index 46bf538..b261e95 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,2 @@
-golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
-golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
+golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
+golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
diff --git a/modfile/rule.go b/modfile/rule.go
index 20ba825..5b2466f 100644
--- a/modfile/rule.go
+++ b/modfile/rule.go
@@ -1678,7 +1678,7 @@
 	return nil
 }
 
-// RemoveTool removes a tool directive with the given path.
+// DropTool removes a tool directive with the given path.
 // It does nothing if no such tool directive exists.
 func (f *File) DropTool(path string) error {
 	for _, t := range f.Tool {
diff --git a/modfile/work.go b/modfile/work.go
index 09df5ea..92db618 100644
--- a/modfile/work.go
+++ b/modfile/work.go
@@ -84,7 +84,7 @@
 }
 
 // Cleanup cleans up the file f after any edit operations.
-// To avoid quadratic behavior, modifications like [WorkFile.DropRequire]
+// To avoid quadratic behavior, modifications like [WorkFile.DropUse]
 // clear the entry but do not remove it from the slice.
 // Cleanup cleans out all the cleared entries.
 func (f *WorkFile) Cleanup() {
diff --git a/sumdb/client.go b/sumdb/client.go
index 47533a8..25df2cd 100644
--- a/sumdb/client.go
+++ b/sumdb/client.go
@@ -274,7 +274,7 @@
 			c.ops.WriteCache(file, data)
 		}
 
-		return cached{data, nil}
+		return cached{text, nil}
 	}).(cached)
 	if result.err != nil {
 		return nil, result.err
diff --git a/sumdb/client_test.go b/sumdb/client_test.go
index f085e7d..b884d60 100644
--- a/sumdb/client_test.go
+++ b/sumdb/client_test.go
@@ -189,6 +189,55 @@
 	}
 }
 
+func TestRejectUnauthenticatedLines(t *testing.T) {
+	tc := newTestClient(t)
+
+	data := "golang.org/x/good v1.0.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=\n"
+	id := tc.treeSize
+	tc.treeSize++
+	rec, err := tlog.FormatRecord(id, []byte(data))
+	if err != nil {
+		t.Fatal(err)
+	}
+	hashes, err := tlog.StoredHashesForRecordHash(id, tlog.RecordHash([]byte(data)), tc)
+	if err != nil {
+		t.Fatal(err)
+	}
+	tc.hashes = append(tc.hashes, hashes...)
+
+	// Create lookup result.
+	h, err := tlog.TreeHash(tc.treeSize, tc)
+	if err != nil {
+		t.Fatal(err)
+	}
+	text := append(tlog.FormatTree(tlog.Tree{N: tc.treeSize, Hash: h}), []byte("golang.org/x/bad v1.0.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=\n")...)
+	signed, err := note.Sign(&note.Note{Text: string(text)}, tc.signer)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	lookupRes := append(rec, signed...)
+	tc.remote["/lookup/golang.org/x/bad@v1.0.0"] = lookupRes
+
+	// Create new tiles.
+	tiles := tlog.NewTiles(tc.tileHeight, id, tc.treeSize)
+	for _, tile := range tiles {
+		data, err := tlog.ReadTileData(tile, tc)
+		if err != nil {
+			t.Fatal(err)
+		}
+		tc.remote["/"+tile.Path()] = data
+	}
+
+	lines, err := tc.client.Lookup("golang.org/x/bad", "v1.0.0")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(lines) != 0 {
+		t.Errorf("Lookup(%q): expected no hashes, got %q", "golang.org/x/bad", strings.Join(lines, "\n"))
+	}
+}
+
 // A testClient is a self-contained client-side testing environment.
 type testClient struct {
 	t          *testing.T // active test
diff --git a/sumdb/tlog/tile.go b/sumdb/tlog/tile.go
index 153f41c..fb0c8d1 100644
--- a/sumdb/tlog/tile.go
+++ b/sumdb/tlog/tile.go
@@ -300,13 +300,13 @@
 func (r *tileHashReader) ReadHashes(indexes []int64) ([]Hash, error) {
 	h := r.tr.Height()
 
-	tileOrder := make(map[Tile]int) // tileOrder[tileKey(tiles[i])] = i
+	tileOrder := make(map[Tile]int) // tileOrder[tiles[i]] = i
 	var tiles []Tile
 
 	// Plan to fetch tiles necessary to recompute tree hash.
 	// If it matches, those tiles are authenticated.
 	stx := subTreeIndex(0, r.tree.N, nil)
-	stxTileOrder := make([]int, len(stx))
+	stxTileOrder := make([]int, len(stx)) // stx[i] is in tiles[stxTileOrder[i]]
 	for i, x := range stx {
 		tile, _, _ := tileForIndex(h, x)
 		tile = tileParent(tile, 0, r.tree.N)
@@ -323,7 +323,7 @@
 	// along with any parent tiles needed
 	// for authentication. For most calls,
 	// the parents are being fetched anyway.
-	indexTileOrder := make([]int, len(indexes))
+	indexTileOrder := make([]int, len(indexes)) // indexes[i] is in tiles[indexTileOrder[i]]
 	for i, x := range indexes {
 		if x >= StoredHashIndex(0, r.tree.N) {
 			return nil, fmt.Errorf("indexes not in tree")
@@ -377,19 +377,38 @@
 		}
 	}
 
+	// At this point, for example if h = 2, N = 15, indexes = [(0, 01)]:
+	//
+	//                 s3
+	//           ┌───────┴───────┐
+	//           ∘               ∘              s2        <- 1/000.p/3
+	//       ┌───┴───┐       ┌───┴───┐       ┌───┴───┐
+	//       ∘       ∘       ∘       ∘       ∘       ∘      s1     s0
+	//     ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐   ┌─┴─┐    |
+	//     00  01  02  03  04  05  06  07  08  09  10  11  12  13  14
+	//
+	//     └── 0/000 ───┘  └── 0/001 ───┘  └── 0/002 ───┘ └ 0/003.p/3 ┘
+	//
+	// stx = [s3, s2, s1, s0]
+	//
+	// tiles = [1/000.p/3, 0/003.p/3, 0/000]
+	//                                  ┬
+	//          └──── for stx ─────┘ for idx
+
 	// Authenticate the initial tiles against the tree hash.
 	// They are arranged so that parents are authenticated before children.
 	// First the tiles needed for the tree hash.
-	th, err := HashFromTile(tiles[stxTileOrder[len(stx)-1]], data[stxTileOrder[len(stx)-1]], stx[len(stx)-1])
-	if err != nil {
-		return nil, err
-	}
-	for i := len(stx) - 2; i >= 0; i-- {
+	var th Hash
+	for i := len(stx) - 1; i >= 0; i-- {
 		h, err := HashFromTile(tiles[stxTileOrder[i]], data[stxTileOrder[i]], stx[i])
 		if err != nil {
 			return nil, err
 		}
-		th = NodeHash(h, th)
+		if i == len(stx)-1 {
+			th = h
+		} else {
+			th = NodeHash(h, th)
+		}
 	}
 	if th != r.tree.Hash {
 		// The tiles do not support the tree hash.
@@ -397,8 +416,8 @@
 		return nil, fmt.Errorf("downloaded inconsistent tile")
 	}
 
-	// Authenticate full tiles against their parents.
-	for i := len(stx); i < len(tiles); i++ {
+	// Authenticate remaining full tiles against their parents.
+	for i := stxTileOrder[len(stx)-1] + 1; i < len(tiles); i++ {
 		tile := tiles[i]
 		p := tileParent(tile, 1, r.tree.N)
 		j, ok := tileOrder[p]
diff --git a/sumdb/tlog/tile_test.go b/sumdb/tlog/tile_test.go
index 62b50b7..b86abb6 100644
--- a/sumdb/tlog/tile_test.go
+++ b/sumdb/tlog/tile_test.go
@@ -5,10 +5,157 @@
 package tlog
 
 import (
+	"bytes"
 	"fmt"
 	"testing"
 )
 
+type testTree struct {
+	t      *testing.T
+	h      int
+	n      int64
+	hashes testHashStorage
+}
+
+func newTestTree(t *testing.T, height int) *testTree {
+	return &testTree{t: t, h: height}
+}
+
+// Add appends a record with the given data to the tree.
+func (tt *testTree) Add(data []byte) {
+	tt.t.Helper()
+	hashes, err := StoredHashes(tt.n, data, tt.hashes)
+	if err != nil {
+		tt.t.Fatal(err)
+	}
+	tt.hashes = append(tt.hashes, hashes...)
+	tt.n++
+}
+
+func (tt *testTree) Tree() Tree {
+	tt.t.Helper()
+	th, err := TreeHash(tt.n, tt.hashes)
+	if err != nil {
+		tt.t.Fatal(err)
+	}
+	return Tree{N: tt.n, Hash: th}
+}
+
+func (tt *testTree) Height() int { return tt.h }
+
+func (tt *testTree) ReadTiles(tiles []Tile) ([][]byte, error) {
+	out := make([][]byte, len(tiles))
+	for i, tile := range tiles {
+		data, err := ReadTileData(tile, tt.hashes)
+		if err != nil {
+			return nil, err
+		}
+		out[i] = data
+	}
+	return out, nil
+}
+
+func (tt *testTree) SaveTiles(tiles []Tile, data [][]byte) {
+	tt.t.Helper()
+	if len(data) != len(tiles) {
+		tt.t.Errorf("SaveTiles: got %d data for %d tiles", len(data), len(tiles))
+		return
+	}
+	for i, tile := range tiles {
+		want, err := ReadTileData(tile, tt.hashes)
+		if err != nil {
+			tt.t.Errorf("SaveTiles(%v): %v", tile.Path(), err)
+			continue
+		}
+		if !bytes.Equal(data[i], want) {
+			tt.t.Errorf("SaveTiles(%v): saved data does not match tree", tile.Path())
+		}
+	}
+}
+
+// zeroIndex returns a TileReader that serves the same tiles as tr,
+// except that the hash at the given stored index is replaced with an
+// all-zeroes hash. If the index is not in the bottom row of its tile,
+// the bottom-row hashes it is computed from are zeroed instead.
+func zeroIndex(tr TileReader, index int64) TileReader {
+	return &zeroIndexReader{tr, index}
+}
+
+type zeroIndexReader struct {
+	TileReader
+	index int64
+}
+
+func (r *zeroIndexReader) ReadTiles(tiles []Tile) ([][]byte, error) {
+	data, err := r.TileReader.ReadTiles(tiles)
+	if err != nil {
+		return nil, err
+	}
+	t, start, end := tileForIndex(r.Height(), r.index)
+	for i, tile := range tiles {
+		if tile.H == t.H && tile.L == t.L && tile.N == t.N && end <= len(data[i]) {
+			data[i] = bytes.Clone(data[i])
+			clear(data[i][start:end])
+		}
+	}
+	return data, nil
+}
+
+func TestTileHashReader(t *testing.T) {
+	tt := newTestTree(t, 2)
+	for range int64(100) {
+		tt.Add(fmt.Appendf(nil, "leaf %d", tt.n))
+		t.Run(fmt.Sprintf("N=%d", tt.n), func(t *testing.T) {
+			th := TileHashReader(tt.Tree(), tt)
+			for i := range StoredHashIndex(0, tt.n) {
+				hashes, err := th.ReadHashes([]int64{i})
+				if err != nil {
+					t.Fatal(err)
+				}
+				if len(hashes) != 1 {
+					t.Fatalf("ReadHashes(%d) = %d hashes, want 1", i, len(hashes))
+				}
+				if hashes[0] != tt.hashes[i] {
+					t.Errorf("ReadHashes(%d) = %x, want %x", i, hashes[0], tt.hashes[i])
+				}
+			}
+
+			var indexes []int64
+			for j := range StoredHashIndex(0, tt.n) {
+				indexes = append(indexes, j)
+			}
+			all, err := th.ReadHashes(indexes)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if len(all) != len(tt.hashes) {
+				t.Fatalf("ReadHashes(%d) = %d hashes, want %d", tt.n, len(all), len(tt.hashes))
+			}
+			for j, h := range all {
+				if h != tt.hashes[j] {
+					t.Errorf("ReadHashes(%d)[%d] = %v, want %v", tt.n, j, h, tt.hashes[j])
+				}
+			}
+
+			for i := range StoredHashIndex(0, tt.n) {
+				t.Run(fmt.Sprintf("tampered=%d", i), func(t *testing.T) {
+					thz := TileHashReader(tt.Tree(), zeroIndex(tt, i))
+
+					hashes, err := thz.ReadHashes([]int64{i})
+					if err == nil {
+						t.Errorf("ReadHashes(%d) = %v, want error", i, hashes[0])
+					}
+
+					all, err := thz.ReadHashes(indexes)
+					if err == nil {
+						t.Errorf("ReadHashes(%d) = %d hashes, want error", tt.n, len(all))
+					}
+				})
+			}
+		})
+	}
+}
+
 // FuzzParseTilePath tests that ParseTilePath never crashes
 func FuzzParseTilePath(f *testing.F) {
 	f.Add("tile/4/0/001")