Skip to content

Commit 79ed8a3

Browse files
authored
Invalidate boundary when converting HashList to HashArray in HashSeq (#150)
* Invalidate boundary when converting HashList to HashArray in HashSeq Cache layout is equivalent, but when using readSszBytes the boundary has to be cleared first to cover the case where the resize does not incrementally fill up the list to the full capacity before converting. Also move the boundary cache invalidation from types.nim to codec.nim. * i386 * We actually only need to clear the boundary between retained and new
1 parent e0adf1e commit 79ed8a3

File tree

3 files changed

+98
-33
lines changed

3 files changed

+98
-33
lines changed

ssz_serialization/codec.nim

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ proc readSszValue*[T](
185185
type E = typeof toSszType(declval ElemType(typeof val))
186186
when val is HashList|HashSeq:
187187
template v: untyped = val.data
188+
when not supportsBulkCopy(type v[0]):
189+
let oldDataLen = v.len
188190
else:
189191
template v: untyped = val
190192

@@ -225,9 +227,8 @@ proc readSszValue*[T](
225227

226228
when val is HashList|HashSeq:
227229
# Unconditionally trigger small, O(1) updates to handle when the list
228-
# shrinks without otherwise changing.
229-
val.clearCaches(0)
230-
val.clearCaches(max(val.len - 1, 0))
230+
# resizes without otherwise changing.
231+
val.clearCaches(max(min(val.len, oldDataLen) - 1, 0))
231232

232233
else:
233234
if input.len == 0:
@@ -275,9 +276,8 @@ proc readSszValue*[T](
275276

276277
when val is HashList|HashSeq:
277278
# Unconditionally trigger small, O(1) updates to handle when the list
278-
# shrinks without otherwise changing.
279-
val.clearCaches(0)
280-
val.clearCaches(max(val.len - 1, 0))
279+
# resizes without otherwise changing.
280+
val.clearCaches(max(min(val.len, oldDataLen) - 1, 0))
281281

282282
elif val is BitArray:
283283
if sizeof(val) != input.len:

ssz_serialization/types.nim

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,7 @@ func resizeHashes[T, I](
533533
## Ensure the hash cache is the correct size for the data in the list - must
534534
## be called whenever `data` shrinks or grows.
535535
let
536-
leaves = int(
537-
chunkIdx(T, dataLen + dataPerChunk(T) - 1))
536+
leaves = int(chunkIdx(T, dataLen + dataPerChunk(T) - 1))
538537
newSize = 1 + max(cacheNodes(depth, leaves), 1)
539538

540539
# Growing might be because of add(), addDefault(), or in-place reading of a
@@ -557,22 +556,14 @@ func resizeHashes[T, I](
557556

558557
newIndices[0] = nodesAtLayer(0, depth, leaves)
559558
for i in 1 .. max(depth, 1):
560-
newIndices[i] =
561-
newIndices[i - 1] + nodesAtLayer(i - 1, depth, leaves)
559+
newIndices[i] = newIndices[i - 1] + nodesAtLayer(i - 1, depth, leaves)
562560

563-
# When resizing, copy each layer (truncating when shrinking)
561+
# When resizing, copy each layer (truncating when shrinking).
564562
for i in 1 ..< max(depth, 1):
565563
let copyLen = min(
566564
indices[i] - indices[i-1], newIndices[i] - newIndices[i - 1])
567565
for j in 0 ..< copyLen:
568-
newHashes[newIndices[i - 1] + j] = hashes[indices[i - 1] + j]
569-
570-
# When shrinking or growing, the last entry at each layer may cover a
571-
# subtree that straddles the boundary between retained and changed elements
572-
# (removed when shrinking, newly added when growing), making its cached
573-
# hash stale - reset it to force recomputation
574-
if copyLen > 0:
575-
newHashes[newIndices[i - 1] + copyLen - 1] = uninitSentinel
566+
assign(newHashes[newIndices[i - 1] + j], hashes[indices[i - 1] + j])
576567

577568
swap(hashes, newHashes)
578569
indices = newIndices
@@ -653,7 +644,8 @@ func resizeHashes*(a: var HashSeq) =
653644
let
654645
totalChunkCount = a.data.totalChunkCount
655646
(firstIdx, maxDepth) = totalChunkCount.progressiveBottom
656-
if a.hashes.len != maxDepth:
647+
oldMaxDepth = a.hashes.len
648+
if maxDepth != oldMaxDepth:
657649
clearCache(a.root)
658650
a.hashes.setLen(maxDepth)
659651
a.indices.reset()
@@ -667,6 +659,11 @@ func resizeHashes*(a: var HashSeq) =
667659
a.hashes[depth].setLen(hashesLen)
668660
else:
669661
clearCache(a.hashes[depth][0])
662+
if maxDepth > oldMaxDepth and
663+
oldMaxDepth > 0 and depth == oldMaxDepth - 1:
664+
# Convert bottom `HashList` to intermediate `HashArray`
665+
a.T.clearCachesArray(
666+
a.hashes[depth], max(depth.int shl 1, 1), maxLen - 1)
670667
a.hashes[^1].reset()
671668
let maxLen = a.T.valuesPerChunk.Limit shl ((maxDepth - 1) shl 1)
672669
a.indices.setLen(a.T.hashListIndicesLen(maxLen))
@@ -788,9 +785,8 @@ template swap*(a, b: var HashList) =
788785
template swap*(a, b: var HashSeq) =
789786
swap(a.data, b.data)
790787
swap(a.root, b.root)
791-
swap(a.stemHashes, b.stemHashes)
792-
swap(a.bottomHashes, b.bottomHashes)
793-
swap(a.bottomIndices, b.bottomIndices)
788+
swap(a.hashes, b.hashes)
789+
swap(a.indices, b.indices)
794790

795791
template clear*(a: var HashList) =
796792
if not a.data.setLen(0):

tests/test_hashcache.nim

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
{.used.}
1010

1111
import
12-
std/sequtils,
12+
std/[random, sequtils],
1313
stew/byteutils,
1414
unittest2,
1515
../ssz_serialization,
16-
../ssz_serialization/[merkleization, types]
16+
../ssz_serialization/merkleization
1717

1818
type Foo = object
1919
x: Digest
@@ -24,8 +24,11 @@ let foo = Foo(
2424
"0x4175371111cef0d13cb836c17dba708f026f2ddbf057b91384bb78b1ba42343c")),
2525
y: 42)
2626

27-
proc checkResize(items: var HashList, counts: varargs[int]) =
27+
proc checkResize[T](items: var T, counts: varargs[int]) =
2828
for count in counts:
29+
when T is HashList:
30+
if count + 4 > int(T.maxLen):
31+
continue
2932
for data in [
3033
SSZ.encode((0 ..< count).mapIt(foo)),
3134
SSZ.encode((0 ..< count).mapIt(foo) & (0 ..< 4).mapIt(default(Foo)))]:
@@ -35,9 +38,10 @@ proc checkResize(items: var HashList, counts: varargs[int]) =
3538
raiseAssert "Valid SSZ"
3639
check items.hash_tree_root() == items.data.hash_tree_root()
3740

38-
suite "HashList":
41+
template runHashCacheTests[T](_: typedesc[T]): untyped =
3942
setup:
40-
var items: HashList[Foo, 8192]
43+
randomize(42)
44+
var items: T
4145

4246
test "Shrink to smaller cache depth":
4347
items.checkResize(1074, 1018)
@@ -58,21 +62,86 @@ suite "HashList":
5862
items.checkResize(100, 0)
5963

6064
test "Multiple resizes in sequence":
61-
items.checkResize(100, 500, 1074, 1018, 200, 2000, 50, 0, 300)
65+
items.checkResize(
66+
100, 500, 1074, 1018, 200, 2000, 50, 0, 300, 304, 309, 314)
6267

6368
test "Incremental add":
6469
for i in 0 ..< 100:
65-
check:
70+
when items is HashList:
71+
check items.add(foo)
72+
else:
6673
items.add(foo)
67-
items.hash_tree_root() == items.data.hash_tree_root()
74+
check items.hash_tree_root() == items.data.hash_tree_root()
6875

6976
test "Incremental add across cache depth boundary":
7077
items.checkResize(1020)
7178
for i in 1020 ..< 1080:
72-
check:
79+
when items is HashList:
80+
check items.add(foo)
81+
else:
7382
items.add(foo)
74-
items.hash_tree_root() == items.data.hash_tree_root()
83+
check items.hash_tree_root() == items.data.hash_tree_root()
7584

7685
test "Incremental decrease":
7786
for i in countdown(1050, 0):
7887
items.checkResize(i)
88+
89+
test "Progressive depth boundaries":
90+
items.checkResize(21844, 340, 20, 84, 1, 340)
91+
92+
test "Random resize sequence":
93+
for _ in 0 ..< 50:
94+
let count =
95+
when items is HashList:
96+
rand(int(items.maxLen) - 4)
97+
else:
98+
rand(4000)
99+
items.checkResize(count)
100+
101+
test "Random add/resize mix":
102+
for _ in 0 ..< 100:
103+
let canAdd =
104+
when items is HashList:
105+
items.data.len < int(items.maxLen)
106+
else:
107+
true
108+
if canAdd and rand(1) == 0:
109+
when items is HashList:
110+
check items.add(foo)
111+
else:
112+
items.add(foo)
113+
check items.hash_tree_root() == items.data.hash_tree_root()
114+
else:
115+
let count =
116+
when items is HashList:
117+
rand(int(items.maxLen) - 4)
118+
else:
119+
rand(4000)
120+
items.checkResize(count)
121+
122+
suite "HashList":
123+
runHashCacheTests(HashList[Foo, 8192])
124+
125+
suite "HashSeq":
126+
runHashCacheTests(HashSeq[Foo])
127+
128+
suite "Cache layout equivalence (for HashSeq)":
129+
template checkEquivalence(maxLen: static Limit) =
130+
test $maxLen:
131+
var
132+
ha: HashArray[maxLen, Foo]
133+
hl: HashList[Foo, maxLen]
134+
for i in 0 ..< int(maxLen):
135+
ha.data[i] = foo
136+
check hl.add(foo)
137+
discard ha.hash_tree_root()
138+
discard hl.hash_tree_root()
139+
check hl.hashes.len == ha.hashes.len
140+
for i in 1 ..< hl.hashes.len:
141+
check hl.hashes[i] == ha.hashes[i]
142+
143+
checkEquivalence(1)
144+
checkEquivalence(4)
145+
checkEquivalence(16)
146+
checkEquivalence(64)
147+
checkEquivalence(256)

0 commit comments

Comments
 (0)