From f1dcee2749fe3b003f58671b08dbde3049927cd2 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 28 May 2020 18:34:59 +0200 Subject: [PATCH] wip: cache * cache lists and arrays of complex objects (5x block processing speed on ncli_db) trivial baseline cache that stores tree in flat memory structure --- AllTests-mainnet.md | 11 +- AllTests-minimal.md | 11 +- FixtureSSZGeneric-minimal.md | 11 +- beacon_chain/spec/datatypes.nim | 14 +- beacon_chain/spec/state_transition_block.nim | 9 +- beacon_chain/ssz.nim | 99 +++++++++++- beacon_chain/ssz/bytes_reader.nim | 15 +- beacon_chain/ssz/types.nim | 153 +++++++++++++++++- ncli/ncli_query.nim | 7 +- tests/helpers/debug_state.nim | 6 +- .../test_fixture_ssz_generic_types.nim | 12 ++ tests/test_ssz.nim | 47 +++++- 12 files changed, 346 insertions(+), 49 deletions(-) diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 10e3406fc..9acf31b8e 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -62,12 +62,6 @@ OK: 2/2 Fail: 0/2 Skip: 0/2 + parent sanity [Preset: mainnet] OK ``` OK: 2/2 Fail: 0/2 Skip: 0/2 -## Discovery v5 utilities -```diff -+ ENR to ENode OK -+ Multiaddress to ENode OK -``` -OK: 2/2 Fail: 0/2 Skip: 0/2 ## Fork Choice + Finality [Preset: mainnet] ```diff + fork_choice - testing finality #01 OK @@ -257,6 +251,11 @@ OK: 4/4 Fail: 0/4 Skip: 0/4 + Rule IV - 12 finalization without support OK ``` OK: 8/8 Fail: 0/8 Skip: 0/8 +## hash +```diff ++ HashArray OK +``` +OK: 1/1 Fail: 0/1 Skip: 0/1 ---TOTAL--- OK: 158/161 Fail: 0/161 Skip: 3/161 diff --git a/AllTests-minimal.md b/AllTests-minimal.md index dc48ad771..6d3963b0d 100644 --- a/AllTests-minimal.md +++ b/AllTests-minimal.md @@ -68,12 +68,6 @@ OK: 2/2 Fail: 0/2 Skip: 0/2 + parent sanity [Preset: minimal] OK ``` OK: 2/2 Fail: 0/2 Skip: 0/2 -## Discovery v5 utilities -```diff -+ ENR to ENode OK -+ Multiaddress to ENode OK -``` -OK: 2/2 Fail: 0/2 Skip: 0/2 ## Fork Choice + Finality [Preset: minimal] ```diff + fork_choice - testing finality #01 OK @@ -263,6 +257,11 @@ OK: 4/4 Fail: 0/4 Skip: 0/4 + Rule IV - 12 finalization without support OK ``` OK: 8/8 Fail: 0/8 Skip: 0/8 +## hash +```diff ++ HashArray OK +``` +OK: 1/1 Fail: 0/1 Skip: 0/1 ---TOTAL--- OK: 160/163 Fail: 0/163 Skip: 3/163 diff --git a/FixtureSSZGeneric-minimal.md b/FixtureSSZGeneric-minimal.md index 536735a27..7ab89707e 100644 --- a/FixtureSSZGeneric-minimal.md +++ b/FixtureSSZGeneric-minimal.md @@ -2,19 +2,20 @@ FixtureSSZGeneric-minimal === ## Official - SSZ generic types ```diff -+ **Skipping** bitlist inputs - valid - skipped altogether OK Testing basic_vector inputs - invalid - skipping Vector[uint128, N] and Vector[uint256, N] Skip + Testing basic_vector inputs - valid - skipping Vector[uint128, N] and Vector[uint256, N] OK ++ Testing bitlist inputs - invalid OK ++ Testing bitlist inputs - valid OK Testing bitvector inputs - invalid Skip + Testing bitvector inputs - valid OK + Testing boolean inputs - invalid OK + Testing boolean inputs - valid OK -+ Testing containers inputs - invalid - skipping VarTestStruct, ComplexTestStruct, BitsStr OK -+ Testing containers inputs - valid - skipping VarTestStruct, ComplexTestStruct, BitsStruc OK ++ Testing containers inputs - invalid - skipping BitsStruct OK ++ Testing containers inputs - valid - skipping BitsStruct OK + Testing uints inputs - invalid - skipping uint128 and uint256 OK + Testing uints inputs - valid - skipping uint128 and uint256 OK ``` -OK: 9/11 Fail: 0/11 Skip: 2/11 +OK: 10/12 Fail: 0/12 Skip: 2/12 ---TOTAL--- -OK: 9/11 Fail: 0/11 Skip: 2/11 +OK: 10/12 Fail: 0/12 Skip: 2/12 diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index 6e5ad3a8d..c714218b4 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -260,24 +260,24 @@ type latest_block_header*: BeaconBlockHeader ##\ ## `latest_block_header.state_root == ZERO_HASH` temporarily - block_roots*: array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] ##\ + block_roots*: HashArray[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] ##\ ## Needed to process attestations, older to newer - state_roots*: array[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] + state_roots*: HashArray[SLOTS_PER_HISTORICAL_ROOT, Eth2Digest] historical_roots*: List[Eth2Digest, HISTORICAL_ROOTS_LIMIT] # Eth1 eth1_data*: Eth1Data eth1_data_votes*: - List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + HashList[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index*: uint64 # Registry - validators*: List[Validator, VALIDATOR_REGISTRY_LIMIT] + validators*: HashList[Validator, VALIDATOR_REGISTRY_LIMIT] balances*: List[uint64, VALIDATOR_REGISTRY_LIMIT] # Randomness - randao_mixes*: array[EPOCHS_PER_HISTORICAL_VECTOR, Eth2Digest] + randao_mixes*: HashArray[EPOCHS_PER_HISTORICAL_VECTOR, Eth2Digest] # Slashings slashings*: array[EPOCHS_PER_SLASHINGS_VECTOR, uint64] ##\ @@ -285,9 +285,9 @@ type # Attestations previous_epoch_attestations*: - List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] + HashList[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] current_epoch_attestations*: - List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] + HashList[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] # Finality justification_bits*: uint8 ##\ diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index dec2a1caf..a7e9b26db 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -95,6 +95,10 @@ proc process_block_header*( true +proc `xor`[T: array](a, b: T): T = + for i in 0.. SLOTS_PER_ETH1_VOTING_PERIOD.int: state.eth1_data = body.eth1_data diff --git a/beacon_chain/ssz.nim b/beacon_chain/ssz.nim index 2759b4ac3..8adcd0cde 100644 --- a/beacon_chain/ssz.nim +++ b/beacon_chain/ssz.nim @@ -136,7 +136,7 @@ func init*(T: type SszWriter, stream: OutputStream): T {.raises: [Defect].} = result.stream = stream template enumerateSubFields(holder, fieldVar, body: untyped) = - when holder is array: + when holder is array|HashArray: for fieldVar in holder: body else: enumInstanceSerializedFields(holder, _{.used.}, fieldVar): body @@ -148,7 +148,7 @@ proc beginRecord*(w: var SszWriter, TT: type): auto {.raises: [Defect].} = when isFixedSize(T): FixedSizedWriterCtx() else: - const offset = when T is array: len(T) * offsetSize + const offset = when T is array|HashArray: len(T) * offsetSize else: fixedPortionSize(T) VarSizedWriterCtx(offset: offset, fixedParts: w.stream.delayFixedSizeWrite(offset)) @@ -205,13 +205,19 @@ proc writeVarSizeType(w: var SszWriter, value: auto) {.raises: [Defect, IOError] mixin toSszType type T = type toSszType(value) - when T is List: + when T is List|HashList: # We reduce code bloat by forwarding all `List` types to a general `seq[T]` proc. writeSeq(w, asSeq value) elif T is BitList: # ATTENTION! We can reuse `writeSeq` only as long as our BitList type is implemented # to internally match the binary representation of SSZ BitLists in memory. writeSeq(w, bytes value) + elif T is HashArray: + trs "WRITING HASHARRAY" + var ctx = beginRecord(w, T.T) + enumerateSubFields(value, field): + writeField w, ctx, astToStr(field), field.data + endRecord w, ctx elif T is object|tuple|array: trs "WRITING OBJECT OR ARRAY" var ctx = beginRecord(w, T) @@ -244,10 +250,12 @@ func sszSize*(value: auto): int {.gcsafe, raises: [Defect].} = when isFixedSize(T): anonConst fixedPortionSize(T) - elif T is array|List: + elif T is array|List|HashList|HashArray: type E = ElemType(T) when isFixedSize(E): len(value) * anonConst(fixedPortionSize(E)) + elif T is HashArray: + sszSizeForVarSizeList(value.data) elif T is array: sszSizeForVarSizeList(value) else: @@ -577,10 +585,91 @@ func hashTreeRootAux[T](x: T): Eth2Digest = else: unsupported T +func mergedDataHash(x: HashList|HashArray, dataIdx: int64): Eth2Digest = + trs "DATA HASH ", dataIdx, " ", x.data.len + + if dataIdx + 1 > x.data.len(): + zeroHashes[x.maxDepth] + elif dataIdx + 1 == x.data.len(): + mergeBranches( + hash_tree_root(x.data[dataIdx]), + Eth2Digest()) + else: + mergeBranches( + hash_tree_root(x.data[dataIdx]), + hash_tree_root(x.data[dataIdx + 1])) + +func cachedHash*(x: HashList, vIdx: int64): Eth2Digest = + doAssert vIdx >= 1 + + let + layer = layer(vIdx) + idxInLayer = vIdx - (1 shl layer) + layerIdx = idxInlayer + x.indices[layer] + + doAssert layer < x.maxDepth + trs "GETTING ", vIdx, " ", layerIdx, " ", layer, " ", x.indices.len + if layerIdx >= x.indices[layer + 1]: + trs "ZERO ", x.indices[layer], " ", x.indices[layer + 1] + zeroHashes[x.maxDepth - layer] + else: + if true or not isCached(x.hashes[layerIdx]): + # TODO oops. so much for maintaining non-mutability. + let px = unsafeAddr x + + trs "REFRESHING ", vIdx, " ", layerIdx, " ", layer + + px[].hashes[layerIdx] = + if layer == x.maxDepth - 1: + let dataIdx = vIdx * 2 - 1 shl (x.maxDepth) + mergedDataHash(x, dataIdx) + else: + mergeBranches( + cachedHash(x, vIdx * 2), + cachedHash(x, vIdx * 2 + 1)) + else: + trs "CACHED ", layerIdx + + x.hashes[layerIdx] + +func cachedHash*(x: HashArray, i: int): Eth2Digest = + doAssert i > 0, "Only valid for flat merkle tree indices" + + if not isCached(x.hashes[i]): + # TODO oops. so much for maintaining non-mutability. + let px = unsafeAddr x + + px[].hashes[i] = + if i * 2 >= x.hashes.len(): + let dataIdx = i * 2 - x.hashes.len() + mergedDataHash(x, dataIdx) + else: + mergeBranches( + cachedHash(x, i * 2), + cachedHash(x, i * 2 + 1)) + + return x.hashes[i] + func hash_tree_root*(x: auto): Eth2Digest {.raises: [Defect], nbench.} = trs "STARTING HASH TREE ROOT FOR TYPE ", name(type(x)) mixin toSszType - result = when x is List|BitList: + + result = when x is HashArray: + if x.hashes.len < 2: + zeroHashes[log2trunc(uint64(x.data.len() + 1))] + else: + cachedHash(x, 1) + elif x is HashList: + if x.hashes.len < 2: + mixInLength(zeroHashes[x.maxDepth], x.data.len()) + else: + if not isCached(x.hashes[0]): + # TODO oops. so much for maintaining non-mutability. + let px = unsafeAddr x + + px[].hashes[0] = mixInLength(cachedHash(x, 1), x.data.len) + x.hashes[0] + elif x is List|BitList: const maxLen = static(x.maxLen) type T = type(x) const limit = maxChunksCount(T, maxLen) diff --git a/beacon_chain/ssz/bytes_reader.nim b/beacon_chain/ssz/bytes_reader.nim index bd799a3ca..be5743c45 100644 --- a/beacon_chain/ssz/bytes_reader.nim +++ b/beacon_chain/ssz/bytes_reader.nim @@ -3,7 +3,7 @@ import typetraits, options, - stew/[bitseqs, endians2, objects, bitseqs], serialization/testing/tracing, + stew/[bitseqs, bitops2, endians2, objects, bitseqs], serialization/testing/tracing, ../spec/[digest, datatypes], ./types template raiseIncorrectSize(T: type) = @@ -66,8 +66,6 @@ template fromSszBytes*(T: type BitSeq, bytes: openarray[byte]): auto = proc `[]`[T, U, V](s: openArray[T], x: HSlice[U, V]) {.error: "Please don't use openarray's [] as it allocates a result sequence".} -# func readOpenArray[T](result: var openarray[T], input: openarray[byte]) = - template checkForForbiddenBits(ResulType: type, input: openarray[byte], expectedBits: static int) = @@ -131,6 +129,16 @@ func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = if resultBytesCount == maxExpectedSize: checkForForbiddenBits(T, input, val.maxLen + 1) + elif val is HashList: + readSszValue(input, val.data) + val.hashes.setLen(0) + val.growHashes() + + elif val is HashArray: + readSszValue(input, val.data) + for h in val.hashes.mitems(): + clearCache(h) + elif val is List|array: type E = type val[0] when E is byte: @@ -248,7 +256,6 @@ func readSszValue*[T](input: openarray[byte], val: var T) {.raisesssz.} = input.toOpenArray(startOffset, endOffset - 1), field) trs "READING COMPLETE ", fieldName - else: trs "READING FOREIGN ", fieldName, ": ", name(SszType) field = fromSszBytes( diff --git a/beacon_chain/ssz/types.nim b/beacon_chain/ssz/types.nim index 624ba5528..57d04a43c 100644 --- a/beacon_chain/ssz/types.nim +++ b/beacon_chain/ssz/types.nim @@ -2,8 +2,9 @@ import tables, options, typetraits, - stew/shims/macros, stew/[objects, bitseqs], - serialization/[object_serialization, errors] + stew/shims/macros, stew/[byteutils, bitops2, objects, bitseqs], + serialization/[object_serialization, errors], + ../spec/digest const offsetSize* = 4 @@ -30,6 +31,15 @@ type actualSszSize*: int elementSize*: int + HashArray*[maxLen: static int; T] = object + data*: array[maxLen, T] + hashes* {.dontSerialize.}: array[maxLen, Eth2Digest] + + HashList*[T; maxLen: static int64] = object + data*: List[T, maxLen] + hashes* {.dontSerialize.}: seq[Eth2Digest] + indices* {.dontSerialize.}: array[log2trunc(maxLen.uint64) + 1, int] + template asSeq*(x: List): auto = distinctBase(x) template init*[T](L: type List, x: seq[T], N: static Limit): auto = @@ -46,7 +56,7 @@ template low*(x: List): auto = low(distinctBase x) template high*(x: List): auto = high(distinctBase x) template `[]`*(x: List, idx: auto): untyped = distinctBase(x)[idx] template `[]=`*(x: var List, idx: auto, val: auto) = distinctBase(x)[idx] = val -template `==`*(a, b: List): bool = asSeq(a) == distinctBase(b) +template `==`*(a, b: List): bool = distinctBase(a) == distinctBase(b) template `&`*(a, b: List): auto = (type(a)(distinctBase(a) & distinctBase(b))) @@ -78,6 +88,133 @@ iterator items*(x: BitList): bool = for i in 0 ..< x.len: yield x[i] +template isCached*(v: Eth2Digest): bool = + ## An entry is "in the cache" if the first 8 bytes are zero - conveniently, + ## Nim initializes values this way, and while there may be false positives, + ## that's fine. + v.data.toOpenArray(0, 7) != [byte 0, 0, 0, 0, 0, 0, 0, 0] +template clearCache*(v: var Eth2Digest) = + v.data[0..<8] = [byte 0, 0, 0, 0, 0, 0, 0, 0] + +proc clearTree*(a: var HashArray, dataIdx: auto) = + ## Clear all cache entries after data at dataIdx has been modified + var idx = 1 shl (a.maxDepth - 1) + int(dataIdx div 2) + while idx != 0: + clearCache(a.hashes[idx]) + idx = idx div 2 + +func nodesAtLayer*(layer, depth, leaves: int): int = + ## Given a number of leaves, how many nodes do you need at a given layer + ## in a binary tree structure? + let leavesPerNode = 1 shl (depth - layer) + (leaves + leavesPerNode - 1) div leavesPerNode + +func cacheNodes*(depth, leaves: int): int = + ## Total number of nodes needed to cache a tree of a given depth with + ## `leaves` items in it (the rest zero-filled) + var res = 0 + for i in 0.. 0: + let + idxInLayer = idx - (1 shl layer) + layerIdx = idxInlayer + a.indices[layer] + if layerIdx < a.indices[layer + 1]: + clearCache(a.hashes[layerIdx]) + + idx = idx div 2 + layer = layer - 1 + + clearCache(a.hashes[0]) + +proc growHashes*(a: var HashList) = + # Ensure that the hash cache is big enough for the data in the list + let + leaves = a.data.len() + newSize = 1 + cacheNodes(a.maxDepth, leaves) + + if a.hashes.len >= newSize: + return + + var + newHashes = newSeq[Eth2Digest](newSize) + newIndices = default(type a.indices) + + if a.hashes.len != newSize: + newIndices[0] = nodesAtLayer(0, a.maxDepth, leaves) + for i in 1..a.maxDepth: + newIndices[i] = newIndices[i - 1] + nodesAtLayer(i - 1, a.maxDepth, leaves) + + for i in 1..