|
| 1 | +package torchwood_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "slices" |
| 6 | + "testing" |
| 7 | + |
| 8 | + "filippo.io/torchwood" |
| 9 | + "golang.org/x/mod/sumdb/tlog" |
| 10 | +) |
| 11 | + |
| 12 | +// TestHashReaderOverlayRightEdge checks that appending records, computing tree |
| 13 | +// and subtree hashes, and proving new records only need the right edge of the |
| 14 | +// original tree. |
| 15 | +func TestHashReaderOverlayRightEdge(t *testing.T) { |
| 16 | + const N = 130 |
| 17 | + leaves, full := buildTree(t, N) |
| 18 | + for n0 := int64(0); n0 <= N; n0++ { |
| 19 | + edge := make(map[int64]tlog.Hash) |
| 20 | + if n0 > 0 { |
| 21 | + idx := torchwood.RightEdge(n0) |
| 22 | + hashes, err := full.ReadHashes(idx) |
| 23 | + fatalIfErr(t, err) |
| 24 | + for i, id := range idx { |
| 25 | + edge[id] = hashes[i] |
| 26 | + } |
| 27 | + } |
| 28 | + r := tlog.HashReaderFunc(func(indexes []int64) ([]tlog.Hash, error) { |
| 29 | + list := make([]tlog.Hash, 0, len(indexes)) |
| 30 | + for _, id := range indexes { |
| 31 | + h, ok := edge[id] |
| 32 | + if !ok { |
| 33 | + return nil, fmt.Errorf("index %d not on the right edge of tree of size %d", id, n0) |
| 34 | + } |
| 35 | + list = append(list, h) |
| 36 | + } |
| 37 | + return list, nil |
| 38 | + }) |
| 39 | + |
| 40 | + overlay := torchwood.NewHashReaderOverlay(n0, r) |
| 41 | + for n := n0; n < N; n++ { |
| 42 | + if overlay.Size() != n { |
| 43 | + t.Fatalf("n0=%d: Size() = %d, want %d", n0, overlay.Size(), n) |
| 44 | + } |
| 45 | + fatalIfErr(t, overlay.AppendRecordHash(leaves[n])) |
| 46 | + |
| 47 | + got, err := tlog.TreeHash(n+1, overlay) |
| 48 | + fatalIfErr(t, err) |
| 49 | + want, err := tlog.TreeHash(n+1, full) |
| 50 | + fatalIfErr(t, err) |
| 51 | + if got != want { |
| 52 | + t.Fatalf("n0=%d: TreeHash(%d) = %v, want %v", n0, n+1, got, want) |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + th, err := tlog.TreeHash(N, overlay) |
| 57 | + fatalIfErr(t, err) |
| 58 | + for i := n0; i < N; i++ { |
| 59 | + proof, err := tlog.ProveRecord(N, i, overlay) |
| 60 | + fatalIfErr(t, err) |
| 61 | + if err := tlog.CheckRecord(proof, N, th, i, leaves[i]); err != nil { |
| 62 | + t.Fatalf("n0=%d: CheckRecord(%d): %v", n0, i, err) |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + for start := int64(0); start < N; start++ { |
| 67 | + // Subtrees wholly within the original tree may not be on its |
| 68 | + // right edge, so only check those extending past it. |
| 69 | + for end := max(start+1, n0+1); end <= N; end++ { |
| 70 | + if !torchwood.ValidSubtree(start, end) { |
| 71 | + continue |
| 72 | + } |
| 73 | + got, err := torchwood.SubtreeHash(start, end, overlay) |
| 74 | + fatalIfErr(t, err) |
| 75 | + want, err := torchwood.SubtreeHash(start, end, full) |
| 76 | + fatalIfErr(t, err) |
| 77 | + if got != want { |
| 78 | + t.Fatalf("n0=%d: SubtreeHash(%d, %d) = %v, want %v", n0, start, end, got, want) |
| 79 | + } |
| 80 | + } |
| 81 | + } |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +// TestHashReaderOverlayFullTree checks subtree hashes and tile contents |
| 86 | +// against a reader for the whole tree, with the underlying reader restricted |
| 87 | +// to the original tree. |
| 88 | +func TestHashReaderOverlayFullTree(t *testing.T) { |
| 89 | + const N = 130 |
| 90 | + leaves, full := buildTree(t, N) |
| 91 | + for n0 := int64(0); n0 <= N; n0++ { |
| 92 | + base := tlog.StoredHashCount(n0) |
| 93 | + r := tlog.HashReaderFunc(func(indexes []int64) ([]tlog.Hash, error) { |
| 94 | + for _, id := range indexes { |
| 95 | + if id >= base { |
| 96 | + return nil, fmt.Errorf("index %d beyond the original tree of size %d", id, n0) |
| 97 | + } |
| 98 | + } |
| 99 | + return full.ReadHashes(indexes) |
| 100 | + }) |
| 101 | + |
| 102 | + overlay := torchwood.NewHashReaderOverlay(n0, r) |
| 103 | + for n := n0; n < N; n++ { |
| 104 | + fatalIfErr(t, overlay.AppendRecordHash(leaves[n])) |
| 105 | + } |
| 106 | + |
| 107 | + for start := int64(0); start < N; start++ { |
| 108 | + for end := start + 1; end <= N; end++ { |
| 109 | + if !torchwood.ValidSubtree(start, end) { |
| 110 | + continue |
| 111 | + } |
| 112 | + got, err := torchwood.SubtreeHash(start, end, overlay) |
| 113 | + fatalIfErr(t, err) |
| 114 | + want, err := torchwood.SubtreeHash(start, end, full) |
| 115 | + fatalIfErr(t, err) |
| 116 | + if got != want { |
| 117 | + t.Fatalf("n0=%d: SubtreeHash(%d, %d) = %v, want %v", n0, start, end, got, want) |
| 118 | + } |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + for _, tile := range tlog.NewTiles(2, n0, N) { |
| 123 | + got, err := tlog.ReadTileData(tile, overlay) |
| 124 | + fatalIfErr(t, err) |
| 125 | + want, err := tlog.ReadTileData(tile, full) |
| 126 | + fatalIfErr(t, err) |
| 127 | + if !slices.Equal(got, want) { |
| 128 | + t.Fatalf("n0=%d: ReadTileData(%v) = %x, want %x", n0, tile, got, want) |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +func TestHashReaderOverlayBatchedReads(t *testing.T) { |
| 135 | + const n0 = 100 |
| 136 | + leaves, full := buildTree(t, 130) |
| 137 | + var reads int |
| 138 | + r := tlog.HashReaderFunc(func(indexes []int64) ([]tlog.Hash, error) { |
| 139 | + reads++ |
| 140 | + return full.ReadHashes(indexes) |
| 141 | + }) |
| 142 | + overlay := torchwood.NewHashReaderOverlay(n0, r) |
| 143 | + for n := int64(n0); n < 130; n++ { |
| 144 | + fatalIfErr(t, overlay.AppendRecordHash(leaves[n])) |
| 145 | + } |
| 146 | + |
| 147 | + // Interleave original tree and overlay indexes. |
| 148 | + base := tlog.StoredHashCount(n0) |
| 149 | + indexes := []int64{0, base, 1, base + 1, 2, base + 2, 3} |
| 150 | + reads = 0 |
| 151 | + got, err := overlay.ReadHashes(indexes) |
| 152 | + fatalIfErr(t, err) |
| 153 | + if reads != 1 { |
| 154 | + t.Errorf("ReadHashes made %d underlying reads, want 1", reads) |
| 155 | + } |
| 156 | + want, err := full.ReadHashes(indexes) |
| 157 | + fatalIfErr(t, err) |
| 158 | + if !slices.Equal(got, want) { |
| 159 | + t.Errorf("ReadHashes(%v) = %v, want %v", indexes, got, want) |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +func TestHashReaderOverlayErrors(t *testing.T) { |
| 164 | + leaves, full := buildTree(t, 10) |
| 165 | + |
| 166 | + // Requests beyond the extended tree are rejected. |
| 167 | + overlay := torchwood.NewHashReaderOverlay(10, full) |
| 168 | + if _, err := overlay.ReadHashes([]int64{tlog.StoredHashCount(10)}); err == nil { |
| 169 | + t.Errorf("expected error for index beyond the tree size") |
| 170 | + } |
| 171 | + if _, err := overlay.ReadHashes([]int64{-1}); err == nil { |
| 172 | + t.Errorf("expected error for negative index") |
| 173 | + } |
| 174 | + |
| 175 | + // A nil underlying reader works from size 0... |
| 176 | + overlay = torchwood.NewHashReaderOverlay(0, nil) |
| 177 | + for n := int64(0); n < 10; n++ { |
| 178 | + fatalIfErr(t, overlay.AppendRecordHash(leaves[n])) |
| 179 | + } |
| 180 | + got, err := tlog.TreeHash(10, overlay) |
| 181 | + fatalIfErr(t, err) |
| 182 | + want, err := tlog.TreeHash(10, full) |
| 183 | + fatalIfErr(t, err) |
| 184 | + if got != want { |
| 185 | + t.Errorf("TreeHash(10) = %v, want %v", got, want) |
| 186 | + } |
| 187 | + |
| 188 | + // ...but fails requests for a non-empty original tree. |
| 189 | + overlay = torchwood.NewHashReaderOverlay(10, nil) |
| 190 | + if _, err := tlog.TreeHash(10, overlay); err == nil { |
| 191 | + t.Errorf("expected error for nil underlying reader") |
| 192 | + } |
| 193 | + // Record 10 completes no subtrees, so it doesn't need the original tree. |
| 194 | + fatalIfErr(t, overlay.AppendRecordHash(leaves[0])) |
| 195 | + // Record 11 completes the subtree [10, 12) and needs the hash of [8, 10). |
| 196 | + if err := overlay.AppendRecordHash(leaves[1]); err == nil { |
| 197 | + t.Errorf("expected error appending a record that completes a subtree") |
| 198 | + } |
| 199 | + if overlay.Size() != 11 { |
| 200 | + t.Errorf("Size() = %d, want 11 after the failed append", overlay.Size()) |
| 201 | + } |
| 202 | +} |
0 commit comments