diff --git a/nimbus/db/aristo/TODO.md b/nimbus/db/aristo/TODO.md index 8ba6161a7..a35630bf9 100644 --- a/nimbus/db/aristo/TODO.md +++ b/nimbus/db/aristo/TODO.md @@ -14,3 +14,5 @@ of proof nodes is rather small. Also, a right boundary leaf node is typically cleared. This needs to be re-checked when writing the `proof` function mentioned above. + +* `aristo_nearby` also qualifies for a re-write, now diff --git a/nimbus/db/aristo/aristo_debug.nim b/nimbus/db/aristo/aristo_debug.nim index 5608c2d32..7924c6256 100644 --- a/nimbus/db/aristo/aristo_debug.nim +++ b/nimbus/db/aristo/aristo_debug.nim @@ -559,7 +559,7 @@ proc pp*( ): string = sTab.ppXTab(db.orDefault) -proc pp*(root: VertexID, leg: Leg; db = AristoDbRef(nil)): string = +proc pp*(leg: Leg; root: VertexID; db = AristoDbRef(nil)): string = let db = db.orDefault() result = "(" & leg.wp.vid.ppVid & "," block: @@ -583,7 +583,7 @@ proc pp*(hike: Hike; db = AristoDbRef(nil); indent = 4): string = else: if hike.legs[0].wp.vid != hike.root: result &= "(" & hike.root.ppVid & ")" & pfx - result &= hike.legs.mapIt(pp(hike.root, it, db)).join(pfx) + result &= hike.legs.mapIt(it.pp(hike.root, db)).join(pfx) result &= pfx & "(" & hike.tail.ppPathPfx & ")" result &= "]" diff --git a/nimbus/db/aristo/aristo_desc/desc_error.nim b/nimbus/db/aristo/aristo_desc/desc_error.nim index e52fec7eb..85569e182 100644 --- a/nimbus/db/aristo/aristo_desc/desc_error.nim +++ b/nimbus/db/aristo/aristo_desc/desc_error.nim @@ -152,8 +152,10 @@ type # Part/proof node errors + PartArgNotInCore PartArgNotGenericRoot PartArgRootAlreadyUsed + PartArgRootAlreadyOnDatabase PartChkChangedKeyNotInKeyTab PartChkChangedVtxMissing PartChkCoreKeyLookupFailed @@ -188,7 +190,6 @@ type PartRlpPayloadException PartRootKeysDontMatch PartRootVidsDontMatch - PartRootAlreadyOnDatabase PartVtxSlotWasModified PartVtxSlotWasNotModified diff --git a/nimbus/db/aristo/aristo_desc/desc_identifiers.nim b/nimbus/db/aristo/aristo_desc/desc_identifiers.nim index 613c3761f..b5b87a2ae 100644 --- a/nimbus/db/aristo/aristo_desc/desc_identifiers.nim +++ b/nimbus/db/aristo/aristo_desc/desc_identifiers.nim @@ -329,6 +329,11 @@ func to*(n: UInt256; T: type PathID): T = ## Representation of a scalar as `PathID` (preserving full information) T(pfx: n, length: 64) +func to*(a: PathID; T: type UInt256): T = + if not a.pfx.isZero: + assert a.length < 64 # debugging only + result = a.pfx shr (4 * (64 - a.length)) + # ------------------------------------------------------------------------------ # Public helpers: Miscellaneous mappings # ------------------------------------------------------------------------------ diff --git a/nimbus/db/aristo/aristo_nearby.nim b/nimbus/db/aristo/aristo_nearby.nim index 9c35674ae..47bf934f1 100644 --- a/nimbus/db/aristo/aristo_nearby.nim +++ b/nimbus/db/aristo/aristo_nearby.nim @@ -151,11 +151,11 @@ proc zeroAdjust( if 0 < hike.legs.len: return ok(hike) - let root = db.getVtx (hike.root, hike.root) - if root.isValid: + let rootVtx = db.getVtx (hike.root, hike.root) + if rootVtx.isValid: block fail: var pfx: NibblesBuf - case root.vType: + case rootVtx.vType: of Branch: # Find first non-dangling link and assign it let nibbleID = block: @@ -166,18 +166,28 @@ proc zeroAdjust( if hike.tail.len == 0: break fail hike.tail[0].int8 - let n = root.branchBorderNibble nibbleID + let n = rootVtx.branchBorderNibble nibbleID if n < 0: # Before or after the database range return err((hike.root,NearbyBeyondRange)) - pfx = root.ePfx & NibblesBuf.nibble(n.byte) + pfx = rootVtx.ePfx & NibblesBuf.nibble(n.byte) of Leaf: - pfx = root.lPfx + pfx = rootVtx.lPfx if not hike.accept pfx: # Before or after the database range return err((hike.root,NearbyBeyondRange)) + # Pathological case: matching `rootVtx` which is a leaf + if hike.legs.len == 0 and hike.tail.len == 0: + return ok(Hike( + root: hike.root, + legs: @[Leg( + nibble: -1, + wp: VidVtxPair( + vid: hike.root, + vtx: rootVtx))])) + var newHike = pfx.toHike(hike.root, db) if 0 < newHike.legs.len: return ok(newHike) @@ -268,10 +278,6 @@ proc nearbyNext( # Some easy cases let hike = ? hike.zeroAdjust(db, doLeast=moveRight) - # if hike.legs[^1].wp.vtx.vType == Extension: - # let vid = hike.legs[^1].wp.vtx.eVid - # return hike.complete(vid, db, hikeLenMax, doLeast=moveRight) - var uHike = hike start = true diff --git a/nimbus/db/aristo/aristo_part.nim b/nimbus/db/aristo/aristo_part.nim index ec85c5da5..73bac66ff 100644 --- a/nimbus/db/aristo/aristo_part.nim +++ b/nimbus/db/aristo/aristo_part.nim @@ -159,6 +159,16 @@ proc partPut*( ok() +proc partGetSubTree*(ps: PartStateRef; rootHash: Hash256): VertexID = + ## For the argument `roothash` retrieve the root vertex ID of a particular + ## sub tree from the partial state descriptor argument `ps`. The function + ## returns `VertexID(0)` if there is no match. + ## + for vid in ps.core.keys: + if ps[vid].to(Hash256) == rootHash: + return vid + + proc partReRoot*( ps: PartStateRef; frRoot: VertexID; @@ -166,9 +176,11 @@ proc partReRoot*( ): Result[void,AristoError] = ## Realign a generic root vertex (i.e `$2`..`$(LEAST_FREE_VID-1)`) for a ## `proof` state to a new root vertex. - if frRoot notin ps.core or frRoot == toRoot: + if frRoot == toRoot: return ok() # nothing to do + if frRoot notin ps.core: + return err(PartArgNotInCore) if frRoot < VertexID(2) or LEAST_FREE_VID <= frRoot.ord or toRoot < VertexID(2) or LEAST_FREE_VID <= toRoot.ord: return err(PartArgNotGenericRoot) @@ -176,7 +188,7 @@ proc partReRoot*( if toRoot in ps.core: return err(PartArgRootAlreadyUsed) if ps.db.getVtx((toRoot,toRoot)).isValid: - return err(PartRootAlreadyOnDatabase) + return err(PartArgRootAlreadyOnDatabase) # Migrate for key in ps.byKey.keys: diff --git a/nimbus/db/aristo/aristo_part/part_debug.nim b/nimbus/db/aristo/aristo_part/part_debug.nim index a5d6bf6c4..c658e324c 100644 --- a/nimbus/db/aristo/aristo_part/part_debug.nim +++ b/nimbus/db/aristo/aristo_part/part_debug.nim @@ -128,7 +128,7 @@ proc pp*( let pfx0 = indent.toPfx() pfx1 = indent.toPfx(1) - + pfx2 = indent.toPfx(2) var pfx = "" if dbOk: result &= pfx & "" & pfx1 & ps.db.pp( @@ -147,9 +147,10 @@ proc pp*( if 0 < len: var qfx = "" result &= pfx1 & "{" - for (vid,vLst) in ps.core.pairs: + for vid in ps.core.keys.toSeq.sorted: + let vLst = ps.core.getOrDefault vid result &= qfx & "(" & vid.pp & ":" & vLst.pp(ps) & ")" - qfx = pfx1 + qfx = pfx2 result &= "}" pfx = pfx0 if byKeyOk: diff --git a/nimbus/db/core_db/backend/aristo_db.nim b/nimbus/db/core_db/backend/aristo_db.nim index 21139429f..719e879cb 100644 --- a/nimbus/db/core_db/backend/aristo_db.nim +++ b/nimbus/db/core_db/backend/aristo_db.nim @@ -11,14 +11,16 @@ {.push raises: [].} import + eth/common, ../../aristo as use_ari, + ../../aristo/aristo_desc/desc_identifiers, ../../aristo/[aristo_init/memory_only, aristo_walk], ../../kvt as use_kvt, ../../kvt/[kvt_init/memory_only, kvt_walk], ../base/[base_config, base_desc, base_helpers] # ------------------------------------------------------------------------------ -# Public constructor and helper +# Public constructors # ------------------------------------------------------------------------------ proc create*(dbType: CoreDbType; kvt: KvtDbRef; mpt: AristoDbRef): CoreDbRef = @@ -51,6 +53,41 @@ proc newAristoVoidCoreDbRef*(): CoreDbRef = KvtDbRef.init(use_kvt.VoidBackendRef), AristoDbRef.init(use_ari.VoidBackendRef)) +proc newCtxByKey*( + ctx: CoreDbCtxRef; + key: Hash256; + info: static[string]; + ): CoreDbRc[CoreDbCtxRef] = + const + rvid: RootedVertexID = (VertexID(1),VertexID(1)) + let + db = ctx.parent + + # Find `(vid,key)` on transaction stack + inx = block: + let rc = db.ariApi.call(findTx, ctx.mpt, rvid, key.to(HashKey)) + if rc.isErr: + return err(rc.error.toError info) + rc.value + + # Fork MPT descriptor that provides `(vid,key)` + newMpt = block: + let rc = db.ariApi.call(forkTx, ctx.mpt, inx) + if rc.isErr: + return err(rc.error.toError info) + rc.value + + # Fork KVT descriptor parallel to `newMpt` + newKvt = block: + let rc = db.kvtApi.call(forkTx, ctx.kvt, inx) + if rc.isErr: + discard db.ariApi.call(forget, newMpt) + return err(rc.error.toError info) + rc.value + + # Create new context + ok(db.bless CoreDbCtxRef(kvt: newKvt, mpt: newMpt)) + # ------------------------------------------------------------------------------ # End # ------------------------------------------------------------------------------ diff --git a/nimbus/db/core_db/backend/aristo_trace.nim b/nimbus/db/core_db/backend/aristo_trace.nim new file mode 100644 index 000000000..8a9e17f4e --- /dev/null +++ b/nimbus/db/core_db/backend/aristo_trace.nim @@ -0,0 +1,956 @@ +# Nimbus +# Copyright (c) 2023-2024 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or +# http://www.apache.org/licenses/LICENSE-2.0) +# * MIT license ([LICENSE-MIT](LICENSE-MIT) or +# http://opensource.org/licenses/MIT) +# at your option. This file may not be copied, modified, or distributed except +# according to those terms. + +## +## Database Backend Tracer +## ======================= +## + +{.push raises: [].} + +import + std/[sequtils, tables, typetraits], + eth/common, + results, + ../../aristo as use_aristo, + ../../aristo/aristo_desc, + ../../kvt as use_kvt, + ../../kvt/kvt_desc, + ../base/[base_config, base_desc] + +const + LogJournalMax = 1_000_000 + ## Maximal size of a journal (organised as LRU) + +type + TracePfx = enum + TrpOops = 0 + TrpKvt + TrpAccounts + TrpGeneric + TrpStorage + + TraceRequest* = enum + TrqOops = 0 + TrqFind + TrqAdd + TrqModify + TrqDelete + + TraceDataType* = enum + TdtOops = 0 + TdtBlob ## Kvt and Aristo + TdtError ## Kvt and Aristo + TdtVoid ## Kvt and Aristo + TdtAccount ## Aristo only + TdtBigNum ## Aristo only + TdtHash ## Aristo only + + TraceDataItemRef* = ref object + ## Log journal entry + pfx*: TracePfx ## DB storage prefix + info*: int ## `KvtApiProfNames` or `AristoApiProfNames` + req*: TraceRequest ## Logged action request + case kind*: TraceDataType + of TdtBlob: + blob*: Blob + of TdtError: + error*: int ## `KvtError` or `AristoError` + of TdtAccount: + account*: AristoAccount + of TdtBigNum: + bigNum*: UInt256 + of TdtHash: + hash*: Hash256 + of TdtVoid, TdtOops: + discard + + TraceLogInstRef* = ref object + ## Logger instance + base: TraceRecorderRef + level: int + truncated: bool + journal: KeyedQueue[Blob,TraceDataItemRef] + + TraceRecorderRef* = ref object of RootRef + log: seq[TraceLogInstRef] ## Production stack for log database + db: CoreDbRef + kvtSave: KvtApiRef ## Restore `KVT` data + ariSave: AristoApiRef ## Restore `Aristo` data + +doAssert LEAST_FREE_VID <= 256 # needed for journal key byte prefix + +# ------------------------------------------------------------------------------ +# Private helpers +# ------------------------------------------------------------------------------ + +when CoreDbNoisyCaptJournal: + import + std/strutils, + chronicles, + stew/byteutils + + func squeezeHex(s: string; ignLen = false): string = + result = if s.len < 20: s else: s[0 .. 5] & ".." & s[s.len-8 .. ^1] + if not ignLen: + let n = (s.len + 1) div 2 + result &= "[" & (if 0 < n: "#" & $n else: "") & "]" + + func stripZeros(a: string; toExp = false): string = + if 0 < a.len: + result = a.toLowerAscii.strip(leading=true, trailing=false, chars={'0'}) + if result.len == 0: + result = "0" + elif result[^1] == '0' and toExp: + var n = 0 + while result[^1] == '0': + let w = result.len + result.setLen(w-1) + n.inc + if n == 1: + result &= "0" + elif n == 2: + result &= "00" + elif 2 < n: + result &= "↑" & $n + + func `$$`(w: openArray[byte]): string = + w.toHex.squeezeHex + + func `$`(w: Blob): string = + w.toHex.squeezeHex + + func `$`(w: UInt256): string = + "#" & w.toHex.stripZeros.squeezeHex + + func `$`(w: Hash256): string = + "£" & w.data.toHex.squeezeHex + + func `$`(w: VertexID): string = + if 0 < w.uint64: "$" & w.uint64.toHex.stripZeros else: "$ø" + + func `$`(w: AristoAccount): string = + "(" & $w.nonce & "," & $w.balance & "," & $w.codeHash & ")" + + func `$`(ti: TraceDataItemRef): string = + result = "(" & + (if ti.pfx == TrpKvt: $KvtApiProfNames(ti.info) + elif ti.pfx == TrpOops: "" + else: $AristoApiProfNames(ti.info)) + + result &= "," & ( + case ti.req: + of TrqOops: "" + of TrqFind: "" + of TrqModify: "=" + of TrqDelete: "-" + of TrqAdd: "+") + + result &= ( + case ti.kind: + of TdtOops: "" + of TdtBlob: $ti.blob + of TdtBigNum: $ti.bigNum + of TdtHash: $ti.hash + of TdtVoid: "ø" + of TdtError: (if ti.pfx == TrpKvt: $KvtError(ti.error) + elif ti.pfx == TrpOops: "" + else: $AristoError(ti.error)) + of TdtAccount: $ti.account) + + result &= ")" + + func toStr(pfx: TracePfx, key: openArray[byte]): string = + case pfx: + of TrpOops: + "" + of TrpKvt: + $$(key.toOpenArray(0, key.len - 1)) + of TrpAccounts: + "1:" & $$(key.toOpenArray(0, key.len - 1)) + of TrpGeneric: + $key[0] & ":" & $$(key.toOpenArray(1, key.len - 1)) + of TrpStorage: + "1:" & $$(key.toOpenArray(0, min(31, key.len - 1))) & ":" & + (if 32 < key.len: $$(key.toOpenArray(32, key.len - 1)) else: "") + + func `$`(key: openArray[byte]; ti: TraceDataItemRef): string = + "(" & + TracePfx(key[0]).toStr(key.toOpenArray(1, key.len - 1)) & "," & + $ti & ")" + +# ------------------------------- + +template logTxt(info: static[string]): static[string] = + "trace " & info + +func topLevel(tr: TraceRecorderRef): int = + tr.log.len - 1 + +# -------------------- + +proc jLogger( + tr: TraceRecorderRef; + key: openArray[byte]; + ti: TraceDataItemRef; + ) = + ## Add or update journal entry. The `tr.pfx` argument indicates the key type: + ## + ## * `TrpKvt`: followed by KVT key + ## * `TrpAccounts`: followed by + ## * `TrpGeneric`: followed by + + ## * `TrpStorage`: followed by + + ## + doAssert ti.pfx != TrpOops + let + pfx = @[ti.pfx.byte] + lRec = tr.log[^1].journal.lruFetch(pfx & @key).valueOr: + if LogJournalMax <= tr.log[^1].journal.len: + tr.log[^1].truncated = true + discard tr.log[^1].journal.lruAppend(pfx & @key, ti, LogJournalMax) + return + if ti.req != TrqFind: + lRec[] = ti[] + +proc jLogger( + tr: TraceRecorderRef; + accPath: Hash256; + ti: TraceDataItemRef; + ) = + tr.jLogger(accPath.data.toSeq, ti) + +proc jLogger( + tr: TraceRecorderRef; + ti: TraceDataItemRef; + ) = + tr.jLogger(EmptyBlob, ti) + +proc jLogger( + tr: TraceRecorderRef; + root: VertexID; + path: openArray[byte]; + ti: TraceDataItemRef; + ) = + tr.jLogger(@[root.byte] & @path, ti) + +proc jLogger( + tr: TraceRecorderRef; + root: VertexID; + ti: TraceDataItemRef; + ) = + tr.jLogger(@[root.byte], ti) + +proc jLogger( + tr: TraceRecorderRef; + accPath: Hash256; + stoPath: Hash256; + ti: TraceDataItemRef; + ) = + tr.jLogger(accPath.data.toSeq & stoPath.data.toSeq, ti) + +# -------------------- + +func to(w: AristoApiProfNames; T: type TracePfx): T = + case w: + of AristoApiProfFetchAccountRecordFn, + AristoApiProfFetchAccountStateFn, + AristoApiProfDeleteAccountRecordFn, + AristoApiProfMergeAccountRecordFn: + return TrpAccounts + of AristoApiProfFetchGenericDataFn, + AristoApiProfFetchGenericStateFn, + AristoApiProfDeleteGenericDataFn, + AristoApiProfDeleteGenericTreeFn, + AristoApiProfMergeGenericDataFn: + return TrpGeneric + of AristoApiProfFetchStorageDataFn, + AristoApiProfFetchStorageStateFn, + AristoApiProfDeleteStorageDataFn, + AristoApiProfDeleteStorageTreeFn, + AristoApiProfMergeStorageDataFn: + return TrpStorage + else: + discard + raiseAssert "Unsupported AristoApiProfNames: " & $w + +func to(w: KvtApiProfNames; T: type TracePfx): T = + TrpKvt + +# -------------------- + +func logRecord( + info: KvtApiProfNames | AristoApiProfNames; + req: TraceRequest; + data: openArray[byte]; + ): TraceDataItemRef = + TraceDataItemRef( + pfx: info.to(TracePfx), + info: info.ord, + req: req, + kind: TdtBlob, + blob: @data) + +func logRecord( + info: KvtApiProfNames | AristoApiProfNames; + req: TraceRequest; + error: KvtError | AristoError; + ): TraceDataItemRef = + TraceDataItemRef( + pfx: info.to(TracePfx), + info: info.ord, + req: req, + kind: TdtError, + error: error.ord) + +func logRecord( + info: KvtApiProfNames | AristoApiProfNames; + req: TraceRequest; + ): TraceDataItemRef = + TraceDataItemRef( + pfx: info.to(TracePfx), + info: info.ord, + req: req, + kind: TdtVoid) + +# -------------------- + +func logRecord( + info: AristoApiProfNames; + req: TraceRequest; + accRec: AristoAccount; + ): TraceDataItemRef = + TraceDataItemRef( + pfx: info.to(TracePfx), + info: info.ord, + req: req, + kind: TdtAccount, + account: accRec) + +func logRecord( + info: AristoApiProfNames; + req: TraceRequest; + state: Hash256; + ): TraceDataItemRef = + TraceDataItemRef( + pfx: info.to(TracePfx), + info: info.ord, + req: req, + kind: TdtHash, + hash: state) + +func logRecord( + info: AristoApiProfNames; + req: TraceRequest; + sto: Uint256; + ): TraceDataItemRef = + TraceDataItemRef( + pfx: info.to(TracePfx), + info: info.ord, + req: req, + kind: TdtBigNum, + bigNum: sto) + +# ------------------------------------------------------------------------------ +# Private functions +# ------------------------------------------------------------------------------ + +proc kvtTraceRecorder(tr: TraceRecorderRef) = + let + api = tr.db.kvtApi + tracerApi = api.dup + + # Set up new production api `tracerApi` and save the old one + tr.kvtSave = api + tr.db.kvtApi = tracerApi + + # Update production api + tracerApi.get = + proc(kvt: KvtDbRef; key: openArray[byte]): Result[Blob,KvtError] = + const info = KvtApiProfGetFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + let data = api.get(kvt, key).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, key=($$key), error + tr.jLogger(key, logRecord(info, TrqFind, error)) + return err(error) # No way + + tr.jLogger(key, logRecord(info, TrqFind, data)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, key=($$key), data=($$data) + ok(data) + + tracerApi.del = + proc(kvt: KvtDbRef; key: openArray[byte]): Result[void,KvtError] = + const info = KvtApiProfDelFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB (for comprehensive log record) + let tiRec = block: + let rc = api.get(kvt, key) + if rc.isOk: + logRecord(info, TrqDelete, rc.value) + elif rc.error == GetNotFound: + logRecord(info, TrqDelete) + else: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, key=($$key), error=rc.error + tr.jLogger(key, logRecord(info, TrqDelete, rc.error)) + return err(rc.error) + + # Delete from DB + api.del(kvt, key).isOkOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, key=($$key), error + tr.jLogger(key, logRecord(info, TrqDelete, error)) + return err(error) + + # Log on journal + tr.jLogger(key, tiRec) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, key=($$key) + ok() + + tracerApi.put = + proc(kvt: KvtDbRef; key, data: openArray[byte]): Result[void,KvtError] = + const info = KvtApiProfPutFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB + let + hasKey = api.hasKey(kvt, key).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, key=($$key), error + tr.jLogger(key, logRecord(info, TrqAdd, error)) + return err(error) + mode = if hasKey: TrqModify else: TrqAdd + + # Store on DB + api.put(kvt, key, data).isOkOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, key=($$key), data=($$data) + tr.jLogger(key, logRecord(info, mode, error)) + return err(error) + + tr.jLogger(key, logRecord(info, mode, data)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, key=($$key), data=($$data) + ok() + + assert tr.kvtSave != tr.db.kvtApi + assert tr.kvtSave.del != tr.db.kvtApi.del + assert tr.kvtSave.hasKey == tr.db.kvtApi.hasKey + + +proc ariTraceRecorder(tr: TraceRecorderRef) = + let + api = tr.db.ariApi + tracerApi = api.dup + + # Set up new production api `tracerApi` and save the old one + tr.ariSave = api + tr.db.ariApi = tracerApi + + tracerApi.fetchAccountRecord = + proc(mpt: AristoDbRef; + accPath: Hash256; + ): Result[AristoAccount,AristoError] = + const info = AristoApiProfFetchAccountRecordFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB + let accRec = api.fetchAccountRecord(mpt, accPath).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, error + tr.jLogger(accPath, logRecord(info, TrqFind, error)) + return err(error) + + tr.jLogger(accPath, logRecord(info, TrqFind, accRec)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, accRec + ok accRec + + tracerApi.fetchAccountState = + proc(mpt: AristoDbRef; + updateOk: bool; + ): Result[Hash256,AristoError] = + const info = AristoApiProfFetchAccountStateFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB + let state = api.fetchAccountState(mpt, updateOk).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, updateOk, error + tr.jLogger logRecord(info, TrqFind, error) + return err(error) + + tr.jLogger logRecord(info, TrqFind, state) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, updateOk, state + ok state + + tracerApi.fetchGenericData = + proc(mpt: AristoDbRef; + root: VertexID; + path: openArray[byte]; + ): Result[Blob,AristoError] = + const info = AristoApiProfFetchGenericDataFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB + let data = api.fetchGenericData(mpt, root, path).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, path=($$path), error + tr.jLogger(root, path, logRecord(info, TrqFind, error)) + return err(error) + + tr.jLogger(root, path, logRecord(info, TrqFind, data)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, path=($$path), data + ok data + + tracerApi.fetchGenericState = + proc(mpt: AristoDbRef; + root: VertexID; + updateOk: bool; + ): Result[Hash256,AristoError] = + const info = AristoApiProfFetchGenericStateFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB + let state = api.fetchAccountState(mpt, updateOk).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, updateOk, error + tr.jLogger(root, logRecord(info, TrqFind, error)) + return err(error) + + tr.jLogger(root, logRecord(info, TrqFind, state)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, updateOk, state + ok state + + tracerApi.fetchStorageData = + proc(mpt: AristoDbRef; + accPath: Hash256; + stoPath: Hash256; + ): Result[Uint256,AristoError] = + const info = AristoApiProfFetchStorageDataFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB + let stoData = api.fetchStorageData(mpt, accPath, stoPath).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, stoPath, error + tr.jLogger(accPath, stoPath, logRecord(info, TrqFind, error)) + return err(error) + + tr.jLogger(accPath, stoPath, logRecord(info, TrqFind, stoData)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, stoPath, stoData + ok stoData + + tracerApi.fetchStorageState = + proc(mpt: AristoDbRef; + accPath: Hash256; + updateOk: bool; + ): Result[Hash256,AristoError] = + const info = AristoApiProfFetchStorageStateFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB + let state = api.fetchStorageState(mpt, accPath, updateOk).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, updateOk, error + tr.jLogger(accPath, logRecord(info, TrqFind, error)) + return err(error) + + tr.jLogger(accPath, logRecord(info, TrqFind, state)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, updateOk, state + ok state + + tracerApi.deleteAccountRecord = + proc(mpt: AristoDbRef; + accPath: Hash256; + ): Result[void,AristoError] = + const info = AristoApiProfDeleteAccountRecordFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB (for comprehensive log record) + let tiRec = block: + let rc = api.fetchAccountRecord(mpt, accPath) + if rc.isOk: + logRecord(info, TrqDelete, rc.value) + elif rc.error == FetchPathNotFound: + logRecord(info, TrqDelete) + else: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, error=rc.error + tr.jLogger(accPath, logRecord(info, TrqDelete, rc.error)) + return err(rc.error) + + # Delete from DB + api.deleteAccountRecord(mpt, accPath).isOkOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, error + tr.jLogger(accPath, logRecord(info, TrqDelete, error)) + return err(error) + + # Log on journal + tr.jLogger(accPath, tiRec) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath + ok() + + tracerApi.deleteGenericData = + proc(mpt: AristoDbRef; + root: VertexID; + path: openArray[byte]; + ): Result[bool,AristoError] = + const info = AristoApiProfDeleteGenericDataFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB (for comprehensive log record) + let tiRec = block: + let rc = api.fetchGenericData(mpt, root, path) + if rc.isOk: + logRecord(info, TrqDelete, rc.value) + elif rc.error == FetchPathNotFound: + logRecord(info, TrqDelete) + else: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, path=($$path), error=rc.error + tr.jLogger(root, path, logRecord(info, TrqDelete, rc.error)) + return err(rc.error) + + # Delete from DB + let emptyTrie = api.deleteGenericData(mpt, root, path).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, path=($$path), error + tr.jLogger(root, path, logRecord(info, TrqDelete, error)) + return err(error) + + # Log on journal + tr.jLogger(root, path, tiRec) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, path=($$path), emptyTrie + ok emptyTrie + + tracerApi.deleteGenericTree = + proc(mpt: AristoDbRef; + root: VertexID; + ): Result[void,AristoError] = + const info = AristoApiProfDeleteGenericTreeFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Delete from DB + api.deleteGenericTree(mpt, root).isOkOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, error + tr.jLogger(root, logRecord(info, TrqDelete, error)) + return err(error) + + # Log on journal + tr.jLogger(root, logRecord(info, TrqDelete)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root + ok() + + tracerApi.deleteStorageData = + proc(mpt: AristoDbRef; + accPath: Hash256; + stoPath: Hash256; + ): Result[bool,AristoError] = + const info = AristoApiProfDeleteStorageDataFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB (for comprehensive log record) + let tiRec = block: + let rc = api.fetchStorageData(mpt, accPath, stoPath) + if rc.isOk: + logRecord(info, TrqDelete, rc.value) + elif rc.error == FetchPathNotFound: + logRecord(info, TrqDelete) + else: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, stoPath, error=rc.error + tr.jLogger(accPath, stoPath, logRecord(info, TrqDelete, rc.error)) + return err(rc.error) + + let emptyTrie = api.deleteStorageData(mpt, accPath, stoPath).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, stoPath, error + tr.jLogger(accPath, stoPath, logRecord(info, TrqDelete, error)) + return err(error) + + # Log on journal + tr.jLogger(accPath, stoPath, tiRec) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, stoPath, emptyTrie + ok emptyTrie + + tracerApi.deleteStorageTree = + proc(mpt: AristoDbRef; + accPath: Hash256; + ): Result[void,AristoError] = + const info = AristoApiProfDeleteStorageTreeFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Delete from DB + api.deleteStorageTree(mpt, accPath).isOkOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, error + tr.jLogger(accPath, logRecord(info, TrqDelete, error)) + return err(error) + + # Log on journal + tr.jLogger(accPath, logRecord(info, TrqDelete)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath + ok() + + tracerApi.mergeAccountRecord = + proc(mpt: AristoDbRef; + accPath: Hash256; + accRec: AristoAccount; + ): Result[bool,AristoError] = + const info = AristoApiProfMergeAccountRecordFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB (for comprehensive log record) + let + hadPath = api.hasPathAccount(mpt, accPath).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, error + tr.jLogger(accPath, logRecord(info, TrqAdd, error)) + return err(error) + mode = if hadPath: TrqModify else: TrqAdd + + # Do the merge + let updated = api.mergeAccountRecord(mpt, accPath, accRec).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, hadPath, error + tr.jLogger(accPath, logRecord(info, mode, error)) + return err(error) + + # Log on journal + tr.jLogger(accPath, logRecord(info, mode, accRec)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, accRec, hadPath, updated + ok updated + + tracerApi.mergeGenericData = + proc(mpt: AristoDbRef; + root: VertexID; + path: openArray[byte]; + data: openArray[byte]; + ): Result[bool,AristoError] = + const info = AristoApiProfMergeGenericDataFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB (for comprehensive log record) + let + hadPath = api.hasPathGeneric(mpt, root, path).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, path, error + tr.jLogger(root, path, logRecord(info, TrqAdd, error)) + return err(error) + mode = if hadPath: TrqModify else: TrqAdd + + # Do the merge + let updated = api.mergeGenericData(mpt, root, path, data).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, path, error + tr.jLogger(root, path, logRecord(info, mode, error)) + return err(error) + + # Log on journal + tr.jLogger(root, path, logRecord(info, mode, data)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, root, path, data=($$data), hadPath, updated + ok updated + + tracerApi.mergeStorageData = + proc(mpt: AristoDbRef; + accPath: Hash256; + stoPath: Hash256; + stoData: UInt256; + ): Result[void,AristoError] = + const info = AristoApiProfMergeStorageDataFn + + when CoreDbNoisyCaptJournal: + let level = tr.topLevel() + + # Find entry on DB (for comprehensive log record) + let + hadPath = api.hasPathStorage(mpt, accPath, stoPath).valueOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, stoPath, error + tr.jLogger(accPath, stoPath, logRecord(info, TrqAdd, error)) + return err(error) + mode = if hadPath: TrqModify else: TrqAdd + + # Do the merge + api.mergeStorageData(mpt, accPath, stoPath,stoData).isOkOr: + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, stoPath, error + tr.jLogger(accPath, stoPath, logRecord(info, mode, error)) + return err(error) + + # Log on journal + tr.jLogger(accPath, stoPath, logRecord(info, mode, stoData)) + + when CoreDbNoisyCaptJournal: + debug logTxt $info, level, accPath, stoPath, stoData, hadPath + ok() + + assert tr.ariSave != tr.db.ariApi + assert tr.ariSave.deleteAccountRecord != tr.db.ariApi.deleteAccountRecord + assert tr.ariSave.hasPathAccount == tr.db.ariApi.hasPathAccount + +# ------------------------------------------------------------------------------ +# Public functions +# ------------------------------------------------------------------------------ + +proc topInst*(tr: TraceRecorderRef): TraceLogInstRef = + ## Get top level logger + tr.log[^1] + +func truncated*(log: TraceLogInstRef): bool = + ## True if journal was truncated due to collecting too many entries + log.truncated + +func level*(log: TraceLogInstRef): int = + ## Non-negative stack level of this log instance. + log.level + +func journal*(log: TraceLogInstRef): KeyedQueue[Blob,TraceDataItemRef] = + ## Get the journal + log.journal + +func db*(log: TraceLogInstRef): CoreDbRef = + ## Get database + log.base.db + +iterator kvtLog*(log: TraceLogInstRef): (Blob,TraceDataItemRef) = + ## Extract `Kvt` journal + for p in log.journal.nextPairs: + let pfx = TracePfx(p.key[0]) + if pfx == TrpKvt: + yield (p.key[1..^1], p.data) + +proc kvtLogBlobs*(log: TraceLogInstRef): seq[(Blob,Blob)] = + log.kvtLog.toSeq + .filterIt(it[1].kind==TdtBlob) + .mapIt((it[0],it[1].blob)) + +iterator ariLog*(log: TraceLogInstRef): (VertexID,Blob,TraceDataItemRef) = + ## Extract `Aristo` journal + for p in log.journal.nextPairs: + let + pfx = TracePfx(p.key[0]) + (root, key) = block: + case pfx: + of TrpAccounts,TrpStorage: + (VertexID(1), p.key[1..^1]) + of TrpGeneric: + (VertexID(p.key[1]), p.key[2..^1]) + else: + continue + yield (root, key, p.data) + +proc pop*(log: TraceLogInstRef): bool = + ## Reduce logger stack by the argument descriptor `log` which must be the + ## top entry on the stack. The function returns `true` if the descriptor + ## `log` was not the only one on stack and the stack was reduced by the + ## top entry. Otherwise nothing is done and `false` returned. + ## + let tr = log.base + doAssert log.level == tr.topLevel() + if 1 < tr.log.len: # Always leave one instance on stack + tr.log.setLen(tr.log.len - 1) + return true + +proc push*(tr: TraceRecorderRef) = + ## Push overlay logger instance + tr.log.add TraceLogInstRef(base: tr, level: tr.log.len) + +# ------------------------------------------------------------------------------ +# Public constructor/destructor +# ------------------------------------------------------------------------------ + +proc init*( + T: type TraceRecorderRef; # Recorder desc to instantiate + db: CoreDbRef; # Database + ): T = + ## Constructor, create initial/base tracer descriptor + result = T(db: db) + result.push() + result.kvtTraceRecorder() + result.ariTraceRecorder() + +proc restore*(tr: TraceRecorderRef) = + ## Restore production API. + tr.db.kvtApi = tr.kvtSave + tr.db.ariApi = tr.ariSave + tr[].reset + +# ------------------------------------------------------------------------------ +# End +# ------------------------------------------------------------------------------ + diff --git a/nimbus/db/core_db/base.nim b/nimbus/db/core_db/base.nim index 04a40dcd4..7f9dd6d4f 100644 --- a/nimbus/db/core_db/base.nim +++ b/nimbus/db/core_db/base.nim @@ -15,6 +15,7 @@ import eth/common, "../.."/[constants, errors], ".."/[kvt, aristo], + ./backend/aristo_db, ./base/[api_tracking, base_config, base_desc, base_helpers] export @@ -44,7 +45,7 @@ when CoreDbEnableProfiling: CoreDbFnInx, CoreDbProfListRef -when CoreDbEnableCaptJournal and false: +when CoreDbEnableCaptJournal: import ./backend/aristo_trace type @@ -69,33 +70,69 @@ proc ctx*(db: CoreDbRef): CoreDbCtxRef = ## db.defCtx -proc swapCtx*(db: CoreDbRef; ctx: CoreDbCtxRef): CoreDbCtxRef = - ## Activate argument context `ctx` as default and return the previously - ## active context. This function goes typically together with `forget()`. A - ## valid scenario might look like - ## :: - ## proc doSomething(db: CoreDbRef; ctx: CoreDbCtxRef) = - ## let saved = db.swapCtx ctx - ## defer: db.swapCtx(saved).forget() +proc newCtxByKey*(ctx: CoreDbCtxRef; root: Hash256): CoreDbRc[CoreDbCtxRef] = + ## Create new context derived from a matching transaction of the currently + ## active context. If successful, the resulting context has the following + ## properties: + ## + ## * Transaction level is 1 + ## * The state of the accounts column is equal to the argument `root` + ## + ## If successful, the resulting descriptor **must** be manually released + ## with `forget()` when it is not used, anymore. + ## + ## Note: + ## The underlying `Aristo` backend uses lazy hashing so this function + ## might fail simply because there is no computed state when nesting + ## the next transaction. If the previous transaction needs to be found, + ## then it must called like this: + ## :: + ## let db = .. # Instantiate CoreDb handle ## ... + ## discard db.ctx.getAccounts.state() # Compute state hash + ## db.ctx.newTransaction() # Enter new transaction + ## + ## However, remember that unused hash computations are contle relative + ## to processing time. + ## + ctx.setTrackNewApi CtxNewCtxByKeyFn + result = ctx.newCtxByKey(root, $api) + ctx.ifTrackNewApi: debug logTxt, api, elapsed, root=($$root), result + +proc swapCtx*(ctx: CoreDbCtxRef; db: CoreDbRef): CoreDbCtxRef = + ## Activate argument context `ctx` as default and return the previously + ## active context. This function goes typically together with `forget()`. + ## A valid scenario might look like + ## :: + ## let db = .. # Instantiate CoreDb handle + ## ... + ## let ctx = newCtxByKey(..).expect "ctx" # Create new context + ## let saved = db.swapCtx ctx # Swap context dandles + ## defer: db.swapCtx(saved).forget() # Restore + ## ... ## doAssert not ctx.isNil - db.setTrackNewApi BaseSwapCtxFn + assert db.defCtx != ctx # debugging only + db.setTrackNewApi CtxSwapCtxFn + + # Swap default context with argument `ctx` result = db.defCtx + db.defCtx = ctx # Set read-write access and install CoreDbAccRef(ctx).call(reCentre, db.ctx.mpt).isOkOr: raiseAssert $api & " failed: " & $error CoreDbKvtRef(ctx).call(reCentre, db.ctx.kvt).isOkOr: raiseAssert $api & " failed: " & $error - db.defCtx = ctx + doAssert db.defCtx != result db.ifTrackNewApi: debug logTxt, api, elapsed proc forget*(ctx: CoreDbCtxRef) = ## Dispose `ctx` argument context and related columns created with this - ## context. This function fails if `ctx` is the default context. + ## context. This function throws an exception `ctx` is the default context. ## ctx.setTrackNewApi CtxForgetFn + doAssert ctx != ctx.parent.defCtx CoreDbAccRef(ctx).call(forget, ctx.mpt).isOkOr: raiseAssert $api & ": " & $error CoreDbKvtRef(ctx).call(forget, ctx.kvt).isOkOr: @@ -713,66 +750,54 @@ proc dispose*(tx: CoreDbTxRef) = # Public tracer methods # ------------------------------------------------------------------------------ -when CoreDbEnableCaptJournal and false: # currently disabled - proc newCapture*( - db: CoreDbRef; - ): CoreDbRc[CoreDbCaptRef] = - ## Trace constructor providing an overlay on top of the argument database - ## `db`. This overlay provides a replacement database handle that can be - ## retrieved via `db.recorder()` (which can in turn be ovelayed.) While - ## running the overlay stores data in a log-table which can be retrieved - ## via `db.logDb()`. +when CoreDbEnableCaptJournal: + proc pushCapture*(db: CoreDbRef): CoreDbCaptRef = + ## .. ## - ## Caveat: - ## The original database argument `db` should not be used while the tracer - ## is active (i.e. exists as overlay). The behaviour for this situation - ## is undefined and depends on the backend implementation of the tracer. - ## - db.setTrackNewApi BaseNewCaptureFn - result = db.methods.newCaptureFn flags + db.setTrackNewApi BasePushCaptureFn + if db.tracerHook.isNil: + db.tracerHook = TraceRecorderRef.init(db) + else: + TraceRecorderRef(db.tracerHook).push() + result = TraceRecorderRef(db.tracerHook).topInst().CoreDbCaptRef db.ifTrackNewApi: debug logTxt, api, elapsed, result - proc recorder*(cpt: CoreDbCaptRef): CoreDbRef = - ## Getter, returns a tracer replacement handle to be used as new database. - ## It records every action like fetch, store, hasKey, hasPath and delete. - ## This descriptor can be superseded by a new overlay tracer (using - ## `newCapture()`, again.) + proc level*(cpt: CoreDbCaptRef): int = + ## Getter, returns the positive number of stacked instances. ## - ## Caveat: - ## Unless the desriptor `cpt` referes to the top level overlay tracer, the - ## result is undefined and depends on the backend implementation of the - ## tracer. - ## - cpt.setTrackNewApi CptRecorderFn - result = cpt.methods.recorderFn() - cpt.ifTrackNewApi: debug logTxt, api, elapsed + let log = cpt.distinctBase + log.db.setTrackNewApi CptLevelFn + result = log.level() + log.db.ifTrackNewApi: debug logTxt, api, elapsed, result - proc logDb*(cp: CoreDbCaptRef): TableRef[Blob,Blob] = - ## Getter, returns the logger table for the overlay tracer database. + proc kvtLog*(cpt: CoreDbCaptRef): seq[(Blob,Blob)] = + ## Getter, returns the `Kvt` logger list for the argument instance. ## - ## Caveat: - ## Unless the desriptor `cpt` referes to the top level overlay tracer, the - ## result is undefined and depends on the backend implementation of the - ## tracer. - ## - cp.setTrackNewApi CptLogDbFn - result = cp.methods.logDbFn() - cp.ifTrackNewApi: debug logTxt, api, elapsed + let log = cpt.distinctBase + log.db.setTrackNewApi CptKvtLogFn + result = log.kvtLogBlobs() + log.db.ifTrackNewApi: debug logTxt, api, elapsed - proc flags*(cp: CoreDbCaptRef):set[CoreDbCaptFlags] = - ## Getter - ## - cp.setTrackNewApi CptFlagsFn - result = cp.methods.getFlagsFn() - cp.ifTrackNewApi: debug logTxt, api, elapsed, result - - proc forget*(cp: CoreDbCaptRef) = + proc pop*(cpt: CoreDbCaptRef) = ## Explicitely stop recording the current tracer instance and reset to ## previous level. ## - cp.setTrackNewApi CptForgetFn - cp.methods.forgetFn() - cp.ifTrackNewApi: debug logTxt, api, elapsed + let db = cpt.distinctBase.db + db.setTrackNewApi CptPopFn + if not cpt.distinctBase.pop(): + TraceRecorderRef(db.tracerHook).restore() + db.tracerHook = TraceRecorderRef(nil) + db.ifTrackNewApi: debug logTxt, api, elapsed, cpt + + proc stopCapture*(db: CoreDbRef) = + ## Discard capture instances. This function is equivalent to `pop()`-ing + ## all instances. + ## + db.setTrackNewApi CptStopCaptureFn + if not db.tracerHook.isNil: + TraceRecorderRef(db.tracerHook).restore() + db.tracerHook = TraceRecorderRef(nil) + db.ifTrackNewApi: debug logTxt, api, elapsed # ------------------------------------------------------------------------------ # End diff --git a/nimbus/db/core_db/base/api_tracking.nim b/nimbus/db/core_db/base/api_tracking.nim index 9688af529..039f6803b 100644 --- a/nimbus/db/core_db/base/api_tracking.nim +++ b/nimbus/db/core_db/base/api_tracking.nim @@ -50,20 +50,21 @@ type BaseFinishFn = "finish" BaseLevelFn = "level" - BaseNewCaptureFn = "newCapture" - BaseNewCtxFromTxFn = "ctxFromTx" + BasePushCaptureFn = "pushCapture" BaseNewTxFn = "newTransaction" BasePersistentFn = "persistent" BaseStateBlockNumberFn = "stateBlockNumber" - BaseSwapCtxFn = "swapCtx" - CptLogDbFn = "cpt/logDb" - CptRecorderFn = "cpt/recorder" - CptForgetFn = "cpt/forget" + CptKvtLogFn = "kvtLog" + CptLevelFn = "level" + CptPopFn = "pop" + CptStopCaptureFn = "stopCapture" CtxForgetFn = "ctx/forget" CtxGetAccountsFn = "getAccounts" CtxGetGenericFn = "getGeneric" + CtxNewCtxByKeyFn = "newCtxByKey" + CtxSwapCtxFn = "swapCtx" KvtDelFn = "del" KvtGetFn = "get" diff --git a/nimbus/no-tracer.nim b/nimbus/tracer.nim similarity index 50% rename from nimbus/no-tracer.nim rename to nimbus/tracer.nim index 7280bac40..440cc511f 100644 --- a/nimbus/no-tracer.nim +++ b/nimbus/tracer.nim @@ -8,39 +8,93 @@ # at your option. This file may not be copied, modified, or distributed except # according to those terms. -# TODO: CoreDb module needs to be updated +{.push raises: [].} import std/[strutils, json], - ./common/common, - ./db/[core_db, ledger], - ./utils/utils, - ./evm/tracer/legacy_tracer, - ./constants, - ./transaction, - ./core/executor, - ./evm/[state, types], nimcrypto/utils as ncrutils, - web3/conversions, ./launcher, results, - ./beacon/web3_eth_conv + web3/conversions, + ./beacon/web3_eth_conv, + ./common/common, + ./constants, + ./core/executor, + ./db/[core_db, ledger], + ./evm/[code_bytes, state, types], + ./evm/tracer/legacy_tracer, + ./launcher, + ./transaction, + ./utils/utils -proc getParentHeader(self: CoreDbRef, header: BlockHeader): BlockHeader = - self.getBlockHeader(header.parentHash) +when not CoreDbEnableCaptJournal: + {.error: "Compiler flag missing for tracer, try -d:dbjapi_enabled".} type - SaveCtxEnv = object - db: CoreDbRef - ctx: CoreDbCtxRef + CaptCtxRef = ref object + db: CoreDbRef # not `nil` + root: common.Hash256 + ctx: CoreDbCtxRef # not `nil` + cpt: CoreDbCaptRef # not `nil` + restore: CoreDbCtxRef # `nil` unless `ctx` activated -proc newCtx(com: CommonRef; root: eth_types.Hash256): SaveCtxEnv = - let ctx = com.db.ctxFromTx(root).valueOr: - raiseAssert "setParentCtx: " & $$error - SaveCtxEnv(db: com.db, ctx: ctx) +const + senderName = "sender" + recipientName = "recipient" + minerName = "miner" + uncleName = "uncle" + internalTxName = "internalTx" -proc setCtx(saveCtx: SaveCtxEnv): SaveCtxEnv = - SaveCtxEnv(db: saveCtx.db, ctx: saveCtx.db.swapCtx saveCtx.ctx) +proc dumpMemoryDB*(node: JsonNode, cpt: CoreDbCaptRef) {.gcsafe.} +proc toJson*(receipts: seq[Receipt]): JsonNode {.gcsafe.} +# ------------------------------------------------------------------------------ +# Private helpers +# ------------------------------------------------------------------------------ + +template safeTracer(info: string; code: untyped) = + try: + code + except CatchableError as e: + raiseAssert info & " name=" & $e.name & " msg=" & e.msg + +# ------------------- + +proc init( + T: type CaptCtxRef; + com: CommonRef; + root: common.Hash256; + ): T + {.raises: [CatchableError].} = + let ctx = block: + let rc = com.db.ctx.newCtxByKey(root) + if rc.isErr: + raiseAssert "newCptCtx: " & $$rc.error + rc.value + T(db: com.db, root: root, cpt: com.db.pushCapture(), ctx: ctx) + +proc init( + T: type CaptCtxRef; + com: CommonRef; + topHeader: BlockHeader; + ): T + {.raises: [CatchableError].} = + T.init(com, com.db.getBlockHeader(topHeader.parentHash).stateRoot) + +proc activate(cc: CaptCtxRef): CaptCtxRef {.discardable.} = + ## Install/activate new context `cc.ctx`, old one in `cc.restore` + doAssert not cc.isNil + doAssert cc.restore.isNil # otherwise activated, already + cc.restore = cc.ctx.swapCtx cc.db + cc + +proc release(cc: CaptCtxRef) = + if not cc.restore.isNil: # switch to original context (if any) + let ctx = cc.restore.swapCtx(cc.db) + doAssert ctx == cc.ctx + cc.ctx.forget() # dispose + cc.cpt.pop() # discard top layer of actions tracer + +# ------------------- proc `%`(x: openArray[byte]): JsonNode = result = %toHex(x, false) @@ -57,17 +111,25 @@ proc toJson(receipt: Receipt): JsonNode = else: result["status"] = %receipt.status -proc dumpReceipts*(chainDB: CoreDbRef, header: BlockHeader): JsonNode = +proc dumpReceiptsImpl( + chainDB: CoreDbRef; + header: BlockHeader; + ): JsonNode + {.raises: [CatchableError].} = result = newJArray() for receipt in chainDB.getReceipts(header.receiptsRoot): result.add receipt.toJson -proc toJson*(receipts: seq[Receipt]): JsonNode = - result = newJArray() - for receipt in receipts: - result.add receipt.toJson +# ------------------------------------------------------------------------------ +# Private functions +# ------------------------------------------------------------------------------ -proc captureAccount(n: JsonNode, db: LedgerRef, address: EthAddress, name: string) = +proc captureAccount( + n: JsonNode; + db: LedgerRef; + address: EthAddress; + name: string; + ) = var jaccount = newJObject() jaccount["name"] = %name jaccount["address"] = %("0x" & $address) @@ -82,7 +144,7 @@ proc captureAccount(n: JsonNode, db: LedgerRef, address: EthAddress, name: strin let code = db.getCode(address) jaccount["codeHash"] = %("0x" & ($codeHash).toLowerAscii) - jaccount["code"] = %("0x" & toHex(code, true)) + jaccount["code"] = %("0x" & code.bytes.toHex(true)) jaccount["storageRoot"] = %("0x" & ($storageRoot).toLowerAscii) var storage = newJObject() @@ -92,48 +154,26 @@ proc captureAccount(n: JsonNode, db: LedgerRef, address: EthAddress, name: strin n.add jaccount -proc dumpMemoryDB*(node: JsonNode, db: CoreDbRef) = - var n = newJObject() - for k, v in db.ctx.getKvt(): - n[k.toHex(false)] = %v - node["state"] = n -proc dumpMemoryDB*(node: JsonNode, kvt: TableRef[common.Blob, common.Blob]) = - var n = newJObject() - for k, v in kvt: - n[k.toHex(false)] = %v - node["state"] = n +proc traceTransactionImpl( + com: CommonRef; + header: BlockHeader; + transactions: openArray[Transaction]; + txIndex: uint64; + tracerFlags: set[TracerFlags] = {}; + ): JsonNode + {.raises: [CatchableError].}= + if header.txRoot == EMPTY_ROOT_HASH: + return newJNull() -proc dumpMemoryDB*(node: JsonNode, capture: CoreDbCaptRef) = - node.dumpMemoryDB capture.logDb - -const - senderName = "sender" - recipientName = "recipient" - minerName = "miner" - uncleName = "uncle" - internalTxName = "internalTx" - -proc traceTransaction*(com: CommonRef, header: BlockHeader, - transactions: openArray[Transaction], txIndex: uint64, - tracerFlags: set[TracerFlags] = {}): JsonNode = let - # we add a memory layer between backend/lower layer db - # and capture state db snapshot during transaction execution - capture = com.db.newCapture.value tracerInst = newLegacyTracer(tracerFlags) - captureCom = com.clone(capture.recorder) - - saveCtx = setCtx com.newCtx(com.db.getParentHeader(header).stateRoot) - vmState = BaseVMState.new(header, captureCom).valueOr: - return newJNull() + cc = activate CaptCtxRef.init(com, header) + vmState = BaseVMState.new(header, com).valueOr: return newJNull() stateDb = vmState.stateDB - defer: - saveCtx.setCtx().ctx.forget() - capture.forget() + defer: cc.release() - if header.txRoot == EMPTY_ROOT_HASH: return newJNull() doAssert(transactions.calcTxRoot == header.txRoot) doAssert(transactions.len != 0) @@ -142,8 +182,7 @@ proc traceTransaction*(com: CommonRef, header: BlockHeader, before = newJArray() after = newJArray() stateDiff = %{"before": before, "after": after} - beforeRoot: common.Hash256 - beforeCtx: SaveCtxEnv + stateCtx = CaptCtxRef(nil) let miner = vmState.coinbase() @@ -159,13 +198,14 @@ proc traceTransaction*(com: CommonRef, header: BlockHeader, before.captureAccount(stateDb, miner, minerName) stateDb.persist() stateDiff["beforeRoot"] = %($stateDb.rootHash) - beforeRoot = stateDb.rootHash - beforeCtx = com.newCtx beforeRoot + discard com.db.ctx.getAccounts.state(updateOk=true) # lazy hashing! + stateCtx = CaptCtxRef.init(com, stateDb.rootHash) let rc = vmState.processTransaction(tx, sender, header) gasUsed = if rc.isOk: rc.value else: 0 if idx.uint64 == txIndex: + discard com.db.ctx.getAccounts.state(updateOk=true) # lazy hashing! after.captureAccount(stateDb, sender, senderName) after.captureAccount(stateDb, recipient, recipientName) after.captureAccount(stateDb, miner, minerName) @@ -176,13 +216,12 @@ proc traceTransaction*(com: CommonRef, header: BlockHeader, # internal transactions: let - saveCtxBefore = setCtx beforeCtx - stateBefore = LedgerRef.init(capture.recorder, beforeRoot) - defer: - saveCtxBefore.setCtx().ctx.forget() + cx = activate stateCtx + ldgBefore = LedgerRef.init(com.db, cx.root) + defer: cx.release() for idx, acc in tracedAccountsPairs(tracerInst): - before.captureAccount(stateBefore, acc, internalTxName & $idx) + before.captureAccount(ldgBefore, acc, internalTxName & $idx) for idx, acc in tracedAccountsPairs(tracerInst): after.captureAccount(stateDb, acc, internalTxName & $idx) @@ -195,30 +234,34 @@ proc traceTransaction*(com: CommonRef, header: BlockHeader, # now we dump captured state db if TracerFlags.DisableState notin tracerFlags: - result.dumpMemoryDB(capture) + result.dumpMemoryDB(cx.cpt) -proc dumpBlockState*(com: CommonRef, blk: EthBlock, dumpState = false): JsonNode = + +proc dumpBlockStateImpl( + com: CommonRef; + blk: EthBlock; + dumpState = false; + ): JsonNode + {.raises: [CatchableError].} = template header: BlockHeader = blk.header + let - parent = com.db.getParentHeader(header) - capture = com.db.newCapture.value - captureCom = com.clone(capture.recorder) - # we only need a stack dump when scanning for internal transaction address + cc = activate CaptCtxRef.init(com, header) + parent = com.db.getBlockHeader(header.parentHash) + + # only need a stack dump when scanning for internal transaction address captureFlags = {DisableMemory, DisableStorage, EnableAccount} tracerInst = newLegacyTracer(captureFlags) - - saveCtx = setCtx com.newCtx(parent.stateRoot) - vmState = BaseVMState.new(header, captureCom, tracerInst).valueOr: - return newJNull() + vmState = BaseVMState.new(header, com, tracerInst).valueOr: + return newJNull() miner = vmState.coinbase() - defer: - saveCtx.setCtx().ctx.forget() - capture.forget() + + defer: cc.release() var before = newJArray() after = newJArray() - stateBefore = LedgerRef.init(capture.recorder, parent.stateRoot) + stateBefore = LedgerRef.init(com.db, parent.stateRoot) for idx, tx in blk.transactions: let sender = tx.getSender @@ -259,22 +302,24 @@ proc dumpBlockState*(com: CommonRef, blk: EthBlock, dumpState = false): JsonNode result = %{"before": before, "after": after} if dumpState: - result.dumpMemoryDB(capture) + result.dumpMemoryDB(cc.cpt) -proc traceBlock*(com: CommonRef, blk: EthBlock, tracerFlags: set[TracerFlags] = {}): JsonNode = + +proc traceBlockImpl( + com: CommonRef; + blk: EthBlock; + tracerFlags: set[TracerFlags] = {}; + ): JsonNode + {.raises: [CatchableError].} = template header: BlockHeader = blk.header + let - capture = com.db.newCapture.value - captureCom = com.clone(capture.recorder) + cc = activate CaptCtxRef.init(com, header) tracerInst = newLegacyTracer(tracerFlags) + vmState = BaseVMState.new(header, com, tracerInst).valueOr: + return newJNull() - saveCtx = setCtx com.newCtx(com.db.getParentHeader(header).stateRoot) - vmState = BaseVMState.new(header, captureCom, tracerInst).valueOr: - return newJNull() - - defer: - saveCtx.setCtx().ctx.forget() - capture.forget() + defer: cc.release() if header.txRoot == EMPTY_ROOT_HASH: return newJNull() doAssert(blk.transactions.calcTxRoot == header.txRoot) @@ -293,24 +338,33 @@ proc traceBlock*(com: CommonRef, blk: EthBlock, tracerFlags: set[TracerFlags] = result["gas"] = %gasUsed if TracerFlags.DisableState notin tracerFlags: - result.dumpMemoryDB(capture) + result.dumpMemoryDB(cc.cpt) -proc traceTransactions*(com: CommonRef, header: BlockHeader, transactions: openArray[Transaction]): JsonNode = +proc traceTransactionsImpl( + com: CommonRef; + header: BlockHeader; + transactions: openArray[Transaction]; + ): JsonNode + {.raises: [CatchableError].} = result = newJArray() for i in 0 ..< transactions.len: - result.add traceTransaction(com, header, transactions, i.uint64, {DisableState}) + result.add traceTransactionImpl( + com, header, transactions, i.uint64, {DisableState}) -proc dumpDebuggingMetaData*(vmState: BaseVMState, blk: EthBlock, launchDebugger = true) = +proc dumpDebuggingMetaDataImpl( + vmState: BaseVMState; + blk: EthBlock; + launchDebugger = true; + ) {.raises: [CatchableError].} = template header: BlockHeader = blk.header + let - com = vmState.com + cc = activate CaptCtxRef.init(vmState.com, header) blockNumber = header.number - capture = com.db.newCapture.value - captureCom = com.clone(capture.recorder) bloom = createBloom(vmState.receipts) - defer: - capture.forget() + + defer: cc.release() let blockSummary = %{ "receiptsRoot": %("0x" & toHex(calcReceiptsRoot(vmState.receipts).data)), @@ -320,17 +374,82 @@ proc dumpDebuggingMetaData*(vmState: BaseVMState, blk: EthBlock, launchDebugger var metaData = %{ "blockNumber": %blockNumber.toHex, - "txTraces": traceTransactions(captureCom, header, blk.transactions), - "stateDump": dumpBlockState(captureCom, blk), - "blockTrace": traceBlock(captureCom, blk, {DisableState}), + "txTraces": traceTransactionsImpl(vmState.com, header, blk.transactions), + "stateDump": dumpBlockStateImpl(vmState.com, blk), + "blockTrace": traceBlockImpl(vmState.com, blk, {DisableState}), "receipts": toJson(vmState.receipts), "block": blockSummary } - metaData.dumpMemoryDB(capture) + metaData.dumpMemoryDB(cc.cpt) let jsonFileName = "debug" & $blockNumber & ".json" if launchDebugger: launchPremix(jsonFileName, metaData) else: writeFile(jsonFileName, metaData.pretty()) + +# ------------------------------------------------------------------------------ +# Public functions +# ------------------------------------------------------------------------------ + +proc traceBlock*( + com: CommonRef; + blk: EthBlock; + tracerFlags: set[TracerFlags] = {}; + ): JsonNode = + "traceBlock".safeTracer: + result = com.traceBlockImpl(blk, tracerFlags) + +proc toJson*(receipts: seq[Receipt]): JsonNode = + result = newJArray() + for receipt in receipts: + result.add receipt.toJson + +proc dumpMemoryDB*(node: JsonNode, cpt: CoreDbCaptRef) = + var n = newJObject() + for (k,v) in cpt.kvtLog: + n[k.toHex(false)] = %v + node["state"] = n + +proc dumpReceipts*(chainDB: CoreDbRef, header: BlockHeader): JsonNode = + "dumpReceipts".safeTracer: + result = chainDB.dumpReceiptsImpl header + +proc traceTransaction*( + com: CommonRef; + header: BlockHeader; + txs: openArray[Transaction]; + txIndex: uint64; + tracerFlags: set[TracerFlags] = {}; + ): JsonNode = + "traceTransaction".safeTracer: + result = com.traceTransactionImpl(header, txs, txIndex,tracerFlags) + +proc dumpBlockState*( + com: CommonRef; + blk: EthBlock; + dumpState = false; + ): JsonNode = + "dumpBlockState".safeTracer: + result = com.dumpBlockStateImpl(blk, dumpState) + +proc traceTransactions*( + com: CommonRef; + header: BlockHeader; + transactions: openArray[Transaction]; + ): JsonNode = + "traceTransactions".safeTracer: + result = com.traceTransactionsImpl(header, transactions) + +proc dumpDebuggingMetaData*( + vmState: BaseVMState; + blk: EthBlock; + launchDebugger = true; + ) = + "dumpDebuggingMetaData".safeTracer: + vmState.dumpDebuggingMetaDataImpl(blk, launchDebugger) + +# ------------------------------------------------------------------------------ +# End +# ------------------------------------------------------------------------------ diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 960ee06d6..c7e93a788 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -16,7 +16,7 @@ cliBuilder: ./test_genesis, ./test_precompiles, ./test_generalstate_json, - #./test_tracer_json, -- temporarily disabled + ./test_tracer_json, #./test_persistblock_json, -- fails #./test_rpc, -- fails ./test_filters, diff --git a/tests/replay/pp.nim b/tests/replay/pp.nim index 4e7fedc7f..79a8f6294 100644 --- a/tests/replay/pp.nim +++ b/tests/replay/pp.nim @@ -66,7 +66,7 @@ func pp*(h: BlockHeader; sep = " "): string = &"receiptsRoot={h.receiptsRoot.pp}{sep}" & &"stateRoot={h.stateRoot.pp}{sep}" & &"baseFee={h.baseFeePerGas}{sep}" & - &"withdrawalsRoot={h.withdrawalsRoot.get(EMPTY_ROOT_HASH)}{sep}" & + &"withdrawalsRoot={h.withdrawalsRoot.get(EMPTY_ROOT_HASH).pp}{sep}" & &"blobGasUsed={h.blobGasUsed.get(0'u64)}{sep}" & &"excessBlobGas={h.excessBlobGas.get(0'u64)}" diff --git a/tests/test_tracer_json.nim b/tests/test_tracer_json.nim index afe95cb1d..975d0059e 100644 --- a/tests/test_tracer_json.nim +++ b/tests/test_tracer_json.nim @@ -9,16 +9,16 @@ # or distributed except according to those terms. import - std/[json, os, sets, tables, strutils], + std/[json, os, tables, strutils], stew/byteutils, chronicles, unittest2, results, ./test_helpers, - ../nimbus/sync/protocol/snap/snap_types, - ../nimbus/db/aristo/aristo_merge, - ../nimbus/db/kvt/kvt_utils, ../nimbus/db/aristo, + ../nimbus/db/aristo/[aristo_desc, aristo_layers, aristo_nearby, aristo_part], + ../nimbus/db/aristo/aristo_part/part_debug, + ../nimbus/db/kvt/kvt_utils, ../nimbus/[tracer, evm/types], ../nimbus/common/common @@ -28,14 +28,17 @@ proc setErrorLevel {.used.} = proc preLoadAristoDb(cdb: CoreDbRef; jKvp: JsonNode; num: BlockNumber) = ## Hack for `Aristo` pre-lading using the `snap` protocol proof-loader + const + info = "preLoadAristoDb" var - proof: seq[SnapProof] # for pre-loading MPT - predRoot: Hash256 # from predecessor header - txRoot: Hash256 # header with block number `num` - rcptRoot: Hash256 # ditto + proof: seq[Blob] # for pre-loading MPT + predRoot: Hash256 # from predecessor header + txRoot: Hash256 # header with block number `num` + rcptRoot: Hash256 # ditto let - adb = cdb.mpt - kdb = cdb.kvt + adb = cdb.ctx.mpt # `Aristo` db + kdb = cdb.ctx.kvt # `Kvt` db + ps = PartStateRef.init adb # Partial DB descriptor # Fill KVT and collect `proof` data for (k,v) in jKvp.pairs: @@ -45,7 +48,7 @@ proc preLoadAristoDb(cdb: CoreDbRef; jKvp: JsonNode; num: BlockNumber) = if key.len == 32: doAssert key == val.keccakHash.data if val != @[0x80u8]: # Exclude empty item - proof.add SnapProof(val) + proof.add val else: if key[0] == 0: try: @@ -60,19 +63,62 @@ proc preLoadAristoDb(cdb: CoreDbRef; jKvp: JsonNode; num: BlockNumber) = discard check kdb.put(key, val).isOk - # TODO: `getColumn(CtXyy)` does not exists anymore. There is only the generic - # `MPT` left that can be retrieved with `getGeneric()`, optionally with - # argument `clearData=true` - - # Install sub-trie roots onto production db - if txRoot.isValid: - doAssert adb.mergeProof(txRoot, VertexID(CtTxs)).isOk - if rcptRoot.isValid: - doAssert adb.mergeProof(rcptRoot, VertexID(CtReceipts)).isOk - doAssert adb.mergeProof(predRoot, VertexID(CtAccounts)).isOk - # Set up production MPT - doAssert adb.mergeProof(proof).isOk + ps.partPut(proof, AutomaticPayload).isOkOr: + raiseAssert info & ": partPut => " & $error + + # Handle transaction sub-tree + if txRoot.isValid: + var txs: seq[Transaction] + for (key,pyl) in adb.rightPairs LeafTie(root: ps.partGetSubTree txRoot): + let + inx = key.path.to(UInt256).truncate(uint) + tx = rlp.decode(pyl.rawBlob, Transaction) + # + # FIXME: Is this might be a bug in the test data? + # + # The single item test key is always `128`. For non-single test + # lists, the keys are `1`,`2`, ..,`N`, `128` (some single digit + # number `N`.) + # + # Unless the `128` item value is put at the start of the argument + # list `txs[]` for `persistTransactions()`, the `tracer` module + # will throw an exception at + # `doAssert(transactions.calcTxRoot == header.txRoot)` in the + # function `traceTransactionImpl()`. + # + if (inx and 0x80) != 0: + txs = @[tx] & txs + else: + txs.add tx + cdb.persistTransactions(num, txRoot, txs) + + # Handle receipts sub-tree + if rcptRoot.isValid: + var rcpts: seq[Receipt] + for (key,pyl) in adb.rightPairs LeafTie(root: ps.partGetSubTree rcptRoot): + let + inx = key.path.to(UInt256).truncate(uint) + rcpt = rlp.decode(pyl.rawBlob, Receipt) + # FIXME: See comment at `txRoot` section. + if (inx and 0x80) != 0: + rcpts = @[rcpt] & rcpts + else: + rcpts.add rcpt + cdb.persistReceipts(rcptRoot, rcpts) + + # Save keys to database + for (rvid,key) in ps.vkPairs: + adb.layersPutKey(rvid, key) + + ps.check().isOkOr: + raiseAssert info & ": check => " & $error + + #echo ">>> preLoadAristoDb (9)", + # "\n ps\n ", ps.pp(byKeyOk=false,byVidOk=false), + # "" + # ----------- + #if true: quit() # use tracerTestGen.nim to generate additional test data proc testFixtureImpl(node: JsonNode, testStatusIMPL: var TestStatus, memoryDB: CoreDbRef) = @@ -98,15 +144,25 @@ proc testFixtureImpl(node: JsonNode, testStatusIMPL: var TestStatus, memoryDB: C let stateDump = dumpBlockState(com, blk) let blockTrace = traceBlock(com, blk, {DisableState}) + # Fix hex representation + for inx in 0 ..< node["txTraces"].len: + for key in ["beforeRoot", "afterRoot"]: + # Here, `node["txTraces"]` stores a string while `txTraces` uses a + # `Hash256` which might expand to a didfferent upper/lower case. + var strHash = txTraces[inx]["stateDiff"][key].getStr.toUpperAscii + if strHash.len < 64: + strHash = '0'.repeat(64 - strHash.len) & strHash + txTraces[inx]["stateDiff"][key] = %(strHash) + check node["txTraces"] == txTraces check node["stateDump"] == stateDump check node["blockTrace"] == blockTrace + for i in 0 ..< receipts.len: let receipt = receipts[i] let stateDiff = txTraces[i]["stateDiff"] check receipt["root"].getStr().toLowerAscii() == stateDiff["afterRoot"].getStr().toLowerAscii() - proc testFixtureAristo(node: JsonNode, testStatusIMPL: var TestStatus) = node.testFixtureImpl(testStatusIMPL, newCoreDbRef AristoDbMemory)