# # Copyright (c) 2021 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. import std/[algorithm, hashes, options, sequtils, sets, strutils, strformat, tables, times], chronos, eth/[common/eth_types, p2p, rlp], eth/trie/[db, nibbles, trie_defs], nimcrypto/keccak, stew/byteutils, stint, rocksdb, ../../../constants, ../../../db/[kvstore_rocksdb, select_backend, storage_types], "../.."/[protocol, types], ../range_desc, ./rocky_bulk_load {.push raises: [Defect].} logScope: topics = "snap-proof" const BasicChainTrieDebugging = false RepairTreeDebugging = false type AccountsDbError* = enum NothingSerious = 0 AccountSmallerThanBase AccountsNotSrictlyIncreasing AccountRangesOverlap AccountRepairBlocked BoundaryProofFailed Rlp2Or17ListEntries RlpBlobExpected RlpBranchLinkExpected RlpEncoding RlpExtPathEncoding RlpNonEmptyBlobExpected UnresolvedRepairNode NoRocksDbBackend CannotOpenRocksDbBulkSession AddBulkItemFailed CommitBulkItemsFailed AccountNotFound HexaryGetFn = proc(key: Blob): Blob {.gcsafe.} ## For testing/debugging AccountsDbXKeyKind = enum ## Extends `storage_types.DbDBKeyKind` for testing/debugging ChainDbStateRootPfx = 200 # on trie db layer RockyBulkStateRootPfx # for rocksdb bulk load RockyBulkHexary # for rocksdb bulk load AccountsDbXKey = object ## Extends `storage_types.DbDBKey` for testing/debugging data*: array[65, byte] dataEndPos*: uint8 # the last populated position in the data AccountLoadStats* = object dura*: array[4,times.Duration] ## Accumulated time statistics size*: array[2,uint64] ## Accumulated size statistics ByteArray32* = array[32,byte] ByteArray33* = array[33,byte] NodeKey = ## Internal DB record reference type distinct ByteArray32 RepairKey = ## Internal DB record, `byte & NodeKey` distinct ByteArray33 RNodeKind = enum Branch Extension Leaf RNodeState = enum Static = 0 ## Inserted as proof record Locked ## Like `Static`, only added on-the-fly Mutable ## Open for modification RNodeRef = ref object ## For building a temporary repair tree state: RNodeState ## `Static` if added as proof data case kind: RNodeKind of Leaf: lPfx: NibblesSeq ## Portion of path segment lData: Blob of Extension: ePfx: NibblesSeq ## Portion of path segment eLink: RepairKey ## Single down link of Branch: bLink: array[16,RepairKey] ## Down links bData: Blob RPathStep = object ## For constructing tree traversal `seq[RPathStep]` path key: RepairKey ## Tree label, node hash node: RNodeRef ## Referes to data record nibble: int8 ## Branch node selector (if any) RPathXStep = object ## Extended `RPathStep` needed for `NodeKey` assignmant pos: int ## Some position into `seq[RPathStep]` step: RPathStep ## Modified copy of an `RPathStep` canLock: bool ## Can set `Locked` state RPath = object path: seq[RPathStep] tail: NibblesSeq ## Stands for non completed leaf path RAccount = object ## Temporarily stashed account data. Proper account records have non-empty ## payload. Records with empty payload are lower boundary records. pathTag: NodeTag ## Equivalent to account hash nodeKey: RepairKey ## Leaf hash into hexary repair table payload: Blob ## Data payload RepairTreeDB = object tab: Table[RepairKey,RNodeRef] ## Repair table acc: seq[RAccount] ## Accounts to appprove of repairKeyGen: uint64 ## Unique tmp key generator AccountsDbRef* = ref object db: TrieDatabaseRef ## General database rocky: RocksStoreRef ## Set if rocksdb is available aStats: AccountLoadStats ## Accumulated time and statistics AccountsDbSessionRef* = ref object keyMap: Table[RepairKey,uint] ## For debugging only (will go away) base: AccountsDbRef ## Back reference to common parameters rootKey: NodeKey ## Current root node peer: Peer ## For log messages rpDB: RepairTreeDB ## Repair database dStats: AccountLoadStats ## Time and size statistics const EmptyBlob = seq[byte].default EmptyNibbleRange = EmptyBlob.initNibbleRange RockyBulkCache = "accounts.sst" static: # Not that there is no doubt about this ... doAssert NodeKey.default.ByteArray32.initNibbleRange.len == 64 # Make sure that `DBKeyKind` extension does not overlap doAssert high(DBKeyKind).int < low(AccountsDbXKeyKind).int # ------------------------------------------------------------------------------ # Private helpers # ------------------------------------------------------------------------------ proc to(tag: NodeTag; T: type NodeKey): T = tag.UInt256.toBytesBE.T proc to(key: NodeKey; T: type UInt256): T = T.fromBytesBE(key.ByteArray32) proc to(key: NodeKey; T: type NodeTag): T = key.to(UInt256).T proc to(h: Hash256; T: type NodeKey): T = h.data.T proc to(key: NodeKey; T: type Hash256): T = result.Hash256.data = key.ByteArray32 proc to(key: NodeKey; T: type NibblesSeq): T = key.ByteArray32.initNibbleRange proc to(key: NodeKey; T: type RepairKey): T = (addr result.ByteArray33[1]).copyMem(unsafeAddr key.ByteArray32[0], 32) proc to(tag: NodeTag; T: type RepairKey): T = tag.to(NodeKey).to(RepairKey) proc isZero[T: NodeTag|NodeKey|RepairKey](a: T): bool = a == T.default proc `==`(a, b: NodeKey): bool = a.ByteArray32 == b.ByteArray32 proc `==`(a, b: RepairKey): bool = a.ByteArray33 == b.ByteArray33 proc hash(a: NodeKey): Hash = a.ByteArray32.hash proc hash(a: RepairKey): Hash = a.ByteArray33.hash proc digestTo(data: Blob; T: type NodeKey): T = keccak256.digest(data).data.T proc isNodeKey(a: RepairKey): bool = a.ByteArray33[0] == 0 proc newRepairKey(ps: AccountsDbSessionRef): RepairKey = ps.rpDB.repairKeyGen.inc var src = ps.rpDB.repairKeyGen.toBytesBE (addr result.ByteArray33[25]).copyMem(addr src[0], 8) result.ByteArray33[0] = 1 proc init(key: var NodeKey; data: openArray[byte]): bool = key.reset if data.len <= 32: if 0 < data.len: let trg = addr key.ByteArray32[32 - data.len] trg.copyMem(unsafeAddr data[0], data.len) return true proc dup(node: RNodeRef): RNodeRef = new result result[] = node[] proc convertTo(data: openArray[byte]; T: type NodeKey): T = discard result.init(data) proc convertTo(key: RepairKey; T: type NodeKey): T = if key.isNodeKey: discard result.init(key.ByteArray33[1 .. 32]) proc convertTo(key: RepairKey; T: type NodeTag): T = if key.isNodeKey: result = UInt256.fromBytesBE(key.ByteArray33[1 .. 32]).T proc convertTo(node: RNodeRef; T: type Blob): T = var writer = initRlpWriter() proc appendOk(writer: var RlpWriter; key: RepairKey): bool = if key.isZero: writer.append(EmptyBlob) elif key.isNodeKey: var hash: Hash256 (addr hash.data[0]).copyMem(unsafeAddr key.ByteArray33[1], 32) writer.append(hash) else: return false true case node.kind: of Branch: writer.startList(17) for n in 0 ..< 16: if not writer.appendOk(node.bLink[n]): return # empty `Blob` writer.append(node.bData) of Extension: writer.startList(2) writer.append(node.ePfx.hexPrefixEncode(isleaf = false)) if not writer.appendOk(node.eLink): return # empty `Blob` of Leaf: writer.startList(2) writer.append(node.lPfx.hexPrefixEncode(isleaf = true)) writer.append(node.lData) writer.finish() template noKeyError(info: static[string]; code: untyped) = try: code except KeyError as e: raiseAssert "Not possible (" & info & "): " & e.msg template elapsed(duration: times.Duration; code: untyped) = block: let start = getTime() block: code duration = getTime() - start # ------------------------------------------------------------------------------ # Private getters & setters # ------------------------------------------------------------------------------ proc xPfx(node: RNodeRef): NibblesSeq = case node.kind: of Leaf: return node.lPfx of Extension: return node.ePfx of Branch: doAssert node.kind != Branch # Ooops proc `xPfx=`(node: RNodeRef, val: NibblesSeq) = case node.kind: of Leaf: node.lPfx = val of Extension: node.ePfx = val of Branch: doAssert node.kind != Branch # Ooops proc xData(node: RNodeRef): Blob = case node.kind: of Branch: return node.bData of Leaf: return node.lData of Extension: doAssert node.kind != Extension # Ooops proc `xData=`(node: RNodeRef; val: Blob) = case node.kind: of Branch: node.bData = val of Leaf: node.lData = val of Extension: doAssert node.kind != Extension # Ooops # ------------------------------------------------------------------------------ # Private debugging helpers # ------------------------------------------------------------------------------ template noPpError(info: static[string]; code: untyped) = try: code except ValueError as e: raiseAssert "Inconveivable (" & info & "): " & e.msg except KeyError as e: raiseAssert "Not possible (" & info & "): " & e.msg except OsError as e: raiseAssert "Ooops (" & info & "): " & e.msg proc pp(s: string; hex = false): string = if hex: let n = (s.len + 1) div 2 (if s.len < 20: s else: s[0 .. 5] & ".." & s[s.len-8 .. s.len-1]) & "[" & (if 0 < n: "#" & $n else: "") & "]" elif s.len <= 30: s else: (if (s.len and 1) == 0: s[0 ..< 8] else: "0" & s[0 ..< 7]) & "..(" & $s.len & ").." & s[s.len-16 ..< s.len] proc pp(a: Hash256; collapse = true): string = if not collapse: a.data.mapIt(it.toHex(2)).join.toLowerAscii elif a == emptyRlpHash: "emptyRlpHash" elif a == blankStringHash: "blankStringHash" else: a.data.mapIt(it.toHex(2)).join[56 .. 63].toLowerAscii proc pp(a: NodeKey; collapse = true): string = Hash256(data: a.ByteArray32).pp(collapse) # --------- proc toKey(a: RepairKey; ps: AccountsDbSessionRef): uint = if not a.isZero: noPpError("pp(RepairKey)"): if not ps.keyMap.hasKey(a): ps.keyMap[a] = ps.keyMap.len.uint + 1 result = ps.keyMap[a] proc toKey(a: NodeKey; ps: AccountsDbSessionRef): uint = a.to(RepairKey).toKey(ps) proc toKey(a: NodeTag; ps: AccountsDbSessionRef): uint = a.to(NodeKey).toKey(ps) proc pp(a: NodeKey; ps: AccountsDbSessionRef): string = if a.isZero: "ø" else:"$" & $a.toKey(ps) proc pp(a: RepairKey; ps: AccountsDbSessionRef): string = if a.isZero: "ø" elif a.isNodeKey: "$" & $a.toKey(ps) else: "¶" & $a.toKey(ps) proc pp(a: NodeTag; ps: AccountsDbSessionRef): string = a.to(NodeKey).pp(ps) # --------- proc pp(q: openArray[byte]; noHash = false): string = if q.len == 32 and not noHash: var a: array[32,byte] for n in 0..31: a[n] = q[n] ($Hash256(data: a)).pp else: q.toSeq.mapIt(it.toHex(2)).join.toLowerAscii.pp(hex = true) proc pp(blob: Blob): string = blob.mapIt(it.toHex(2)).join proc pp(a: Account): string = noPpError("pp(Account)"): result = &"({a.nonce},{a.balance},{a.storageRoot},{a.codeHash})" proc pp(sa: SnapAccount): string = "(" & $sa.accHash & "," & sa.accBody.pp & ")" proc pp(al: seq[SnapAccount]): string = result = " @[" noPpError("pp(seq[SnapAccount])"): for n,rec in al: result &= &"| # <{n}>| {rec.pp}," if 10 < result.len: result[^1] = ']' else: result &= "]" proc pp(blobs: seq[Blob]): string = result = " @[" noPpError("pp(seq[Blob])"): for n,rec in blobs: result &= "| # <" & $n & ">| \"" & rec.pp & "\".hexToSeqByte," if 10 < result.len: result[^1] = ']' else: result &= "]" proc pp(branch: array[17,Blob]; ps: AccountsDbSessionRef): string = result = "[" noPpError("pp(array[17,Blob])"): for a in 0 .. 15: result &= branch[a].convertTo(NodeKey).pp(ps) & "," result &= branch[16].pp & "]" proc pp(branch: array[16,RepairKey]; ps: AccountsDbSessionRef): string = result = "[" noPpError("pp(array[17,Blob])"): for a in 0 .. 15: result &= branch[a].pp(ps) & "," result[^1] = ']' proc pp(hs: seq[NodeKey]; ps: AccountsDbSessionRef): string = "<" & hs.mapIt(it.pp(ps)).join(",") & ">" proc pp(hs: HashSet[NodeKey]; ps: AccountsDbSessionRef): string = "{" & toSeq(hs.items).mapIt(it.toKey(ps)).sorted.mapIt("$" & $it).join(",") & "}" proc pp(w: NibblesSeq): string = $w proc pp(n: RNodeRef; ps: AccountsDbSessionRef): string = proc ppStr(blob: Blob): string = if blob.len == 0: "" else: blob.pp.pp(hex = true) noPpError("pp(RNodeRef)"): let so = n.state.ord case n.kind: of Leaf: result = ["l","ł","L"][so] & &"({n.lPfx.pp},{n.lData.ppStr})" of Extension: result = ["e","€","E"][so] & &"({n.ePfx.pp},{n.eLink.pp(ps)})" of Branch: result = ["b","þ","B"][so] & &"({n.bLink.pp(ps)},{n.bData.ppStr})" proc pp(w: RPathStep; ps: AccountsDbSessionRef): string = noPpError("pp(seq[(NodeKey,RNodeRef)])"): let nibble = if 0 <= w.nibble: &"{w.nibble:x}" else: "ø" result = &"({w.key.pp(ps)},{nibble},{w.node.pp(ps)})" proc pp(w: openArray[RPathStep]; ps: AccountsDbSessionRef; indent = 4): string = let pfx = "\n" & " ".repeat(indent) noPpError("pp(seq[(NodeKey,RNodeRef)])"): result = w.toSeq.mapIt(it.pp(ps)).join(pfx) proc pp(w: RPath; ps: AccountsDbSessionRef; indent = 4): string = let pfx = "\n" & " ".repeat(indent) noPpError("pp(RPath)"): result = w.path.pp(ps,indent) & &"{pfx}({w.tail.pp})" proc pp(w: RPathXStep; ps: AccountsDbSessionRef): string = noPpError("pp(seq[(int,RPathStep)])"): let y = if w.canLock: "lockOk" else: "noLock" result = &"({w.pos},{y},{w.step.pp(ps)})" proc pp(w: seq[RPathXStep]; ps: AccountsDbSessionRef; indent = 4): string = let pfx = "\n" & " ".repeat(indent) noPpError("pp(seq[RPathXStep])"): result = w.mapIt(it.pp(ps)).join(pfx) proc pp(a: DbKey|AccountsDbXKey): string = a.data.toSeq.mapIt(it.toHex(2)).join # ------------------------------------------------------------------------------ # Private helpers for bulk load testing # ------------------------------------------------------------------------------ proc chainDbHexaryKey*(a: RepairKey): ByteArray32 = a.convertTo(NodeKey).ByteArray32 proc chainDbStateRootPfxKey*(a: NodeKey; b: NodeTag): AccountsDbXKey = result.data[0] = byte ord(ChainDbStateRootPfx) result.data[1 .. 32] = a.ByteArray32 result.data[33 .. 64] = b.to(NodeKey).ByteArray32 result.dataEndPos = uint8 64 proc rockyBulkHexaryKey(a: NodeTag): DbKey = result.data[0] = byte ord(RockyBulkHexary) result.data[1 .. 32] = a.to(NodeKey).ByteArray32 result.dataEndPos = uint8 32 proc rockyBulkStateRootPfxKey*(a: NodeKey; b: NodeTag): AccountsDbXKey = result.data[0] = byte ord(RockyBulkStateRootPfx) result.data[1 .. 32] = a.ByteArray32 result.data[33 .. 64] = b.to(NodeKey).ByteArray32 result.dataEndPos = uint8 64 template toOpenArray*(k: AccountsDbXKey): openArray[byte] = k.data.toOpenArray(0, int(k.dataEndPos)) template toOpenArray*(k: ByteArray32): openArray[byte] = k.toOpenArray(0, 31) proc storeAccountPathsOnChainDb( ps: AccountsDbSessionRef ): Result[void,AccountsDbError] = let dbTx = ps.base.db.beginTransaction defer: dbTx.commit for a in ps.rpDB.acc: if a.payload.len != 0: ps.base.db.put( ps.rootKey.chainDbStateRootPfxKey(a.pathTag).toOpenArray, a.payload) ok() proc storeHexaryNodesOnChainDb( ps: AccountsDbSessionRef ): Result[void,AccountsDbError] = let dbTx = ps.base.db.beginTransaction defer: dbTx.commit for (key,value) in ps.rpDB.tab.pairs: if not key.isNodeKey: let error = UnresolvedRepairNode trace "Unresolved node in repair table", error return err(error) ps.base.db.put(key.chainDbHexaryKey.toOpenArray, value.convertTo(Blob)) ok() proc storeAccountPathsOnRockyDb( ps: AccountsDbSessionRef ): Result[void,AccountsDbError] {.gcsafe, raises: [Defect,OSError,ValueError].} = if ps.base.rocky.isNil: return err(NoRocksDbBackend) let bulker = RockyBulkLoadRef.init(ps.base.rocky) defer: bulker.destroy() if not bulker.begin(RockyBulkCache): let error = CannotOpenRocksDbBulkSession trace "Rocky accounts session initiation failed", error, info=bulker.lastError() return err(error) for n,a in ps.rpDB.acc: if a.payload.len != 0: let key = ps.rootKey.rockyBulkStateRootPfxKey(a.pathTag) if not bulker.add(key.toOpenArray, a.payload): let error = AddBulkItemFailed trace "Rocky accounts bulk load failure", n, len=ps.rpDB.acc.len, error, info=bulker.lastError() return err(error) if bulker.finish().isErr: let error = CommitBulkItemsFailed trace "Rocky accounts commit failure", len=ps.rpDB.acc.len, error, info=bulker.lastError() return err(error) ok() proc storeHexaryNodesOnRockyDb( ps: AccountsDbSessionRef ): Result[void,AccountsDbError] {.gcsafe, raises: [Defect,OSError,KeyError,ValueError].} = if ps.base.rocky.isNil: return err(NoRocksDbBackend) let bulker = RockyBulkLoadRef.init(ps.base.rocky) defer: bulker.destroy() if not bulker.begin(RockyBulkCache): let error = CannotOpenRocksDbBulkSession trace "Rocky hexary session initiation failed", error, info=bulker.lastError() return err(error) #let keyList = toSeq(ps.rpDB.tab.keys) # .filterIt(it.isNodeKey) # .mapIt(it.convertTo(NodeTag)) # .sorted(cmp) var keyList = newSeq[NodeTag](ps.rpDB.tab.len) inx = 0 for repairKey in ps.rpDB.tab.keys: if repairKey.isNodeKey: keyList[inx] = repairKey.convertTo(NodeTag) inx.inc if inx < ps.rpDB.tab.len: return err(UnresolvedRepairNode) keyList.sort(cmp) for n,nodeTag in keyList: let key = nodeTag.rockyBulkHexaryKey() data = ps.rpDB.tab[nodeTag.to(RepairKey)].convertTo(Blob) if not bulker.add(key.toOpenArray, data): let error = AddBulkItemFailed trace "Rocky hexary bulk load failure", n, len=ps.rpDB.tab.len, error, info=bulker.lastError() return err(error) if bulker.finish().isErr: let error = CommitBulkItemsFailed trace "Rocky hexary commit failure", len=ps.rpDB.acc.len, error, info=bulker.lastError() return err(error) ok() # ------------------------------------------------------------------------------ # Private functions # ------------------------------------------------------------------------------ # Example trie from https://eth.wiki/en/fundamentals/patricia-tree # # lookup data: # "do": "verb" # "dog": "puppy" # "dodge": "coin" # "horse": "stallion" # # trie DB: # root: [16 A] # A: [* * * * B * * * [20+"orse" "stallion"] * * * * * * * *] # B: [00+"o" D] # D: [* * * * * * E * * * * * * * * * "verb"] # E: [17 [* * * * * * [35 "coin"] * * * * * * * * * "puppy"]] # # with first nibble of two-column rows: # hex bits | node type length # ---------+------------------ # 0 0000 | extension even # 1 0001 | extension odd # 2 0010 | leaf even # 3 0011 | leaf odd # # and key path: # "do": 6 4 6 f # "dog": 6 4 6 f 6 7 # "dodge": 6 4 6 f 6 7 6 5 # "horse": 6 8 6 f 7 2 7 3 6 5 # proc hexaryImport( ps: AccountsDbSessionRef; recData: Blob ): Result[void,AccountsDbError] {.gcsafe, raises: [Defect, RlpError].} = ## Decode a single trie item for adding to the table and add it to the ## database. Branch and exrension record links are collected. let nodeKey = recData.digestTo(NodeKey) repairKey = nodeKey.to(RepairKey) # for repair table var rlp = recData.rlpFromBytes blobs = newSeq[Blob](2) # temporary, cache links: array[16,RepairKey] # reconstruct branch node blob16: Blob # reconstruct branch node top = 0 # count entries rNode: RNodeRef # repair tree node # Collect lists of either 2 or 17 blob entries. for w in rlp.items: case top of 0, 1: if not w.isBlob: return err(RlpBlobExpected) blobs[top] = rlp.read(Blob) of 2 .. 15: var key: NodeKey if not key.init(rlp.read(Blob)): return err(RlpBranchLinkExpected) # Update ref pool links[top] = key.to(RepairKey) of 16: if not w.isBlob: return err(RlpBlobExpected) blob16 = rlp.read(Blob) else: return err(Rlp2Or17ListEntries) top.inc # Verify extension data case top of 2: if blobs[0].len == 0: return err(RlpNonEmptyBlobExpected) let (isLeaf, pathSegment) = hexPrefixDecode blobs[0] if isLeaf: rNode = RNodeRef( kind: Leaf, lPfx: pathSegment, lData: blobs[1]) else: var key: NodeKey if not key.init(blobs[1]): return err(RlpExtPathEncoding) # Update ref pool rNode = RNodeRef( kind: Extension, ePfx: pathSegment, eLink: key.to(RepairKey)) of 17: for n in [0,1]: var key: NodeKey if not key.init(blobs[n]): return err(RlpBranchLinkExpected) # Update ref pool links[n] = key.to(RepairKey) rNode = RNodeRef( kind: Branch, bLink: links, bData: blob16) else: discard # Add to repair database ps.rpDB.tab[repairKey] = rNode # Add to hexary trie database -- disabled, using bulk import later #ps.base.db.put(nodeKey.ByteArray32, recData) when RepairTreeDebugging: # Rebuild blob from repair record let nodeBlob = rNode.convertTo(Blob) if nodeBlob != recData: echo "*** hexaryImport oops:", " kind=", rNode.kind, " key=", repairKey.pp(ps), " nodeBlob=", nodeBlob.pp, " recData=", recData.pp doAssert nodeBlob == recData ok() # ------------------------------------------------------------------------------ # Private functions, repair tree action helpers # ------------------------------------------------------------------------------ proc rTreeExtendLeaf( ps: AccountsDbSessionRef; rPath: RPath; key: RepairKey ): RPath = ## Append a `Leaf` node to a `Branch` node (see `rTreeExtend()`.) if 0 < rPath.tail.len: let nibble = rPath.path[^1].nibble leaf = RNodeRef( state: Mutable, kind: Leaf, lPfx: rPath.tail) ps.rpDB.tab[key] = leaf if not key.isNodeKey: rPath.path[^1].node.bLink[nibble] = key return RPath( path: rPath.path & RPathStep(key: key, node: leaf, nibble: -1), tail: EmptyNibbleRange) proc rTreeExtendLeaf( ps: AccountsDbSessionRef; rPath: RPath; key: RepairKey; node: RNodeRef ): RPath = ## Register `node` and append/link a `Leaf` node to a `Branch` node (see ## `rTreeExtend()`.) if 1 < rPath.tail.len and node.state == Mutable: let nibble = rPath.tail[0].int8 xStep = RPathStep(key: key, node: node, nibble: nibble) xPath = RPath(path: rPath.path & xStep, tail: rPath.tail.slice(1)) return ps.rTreeExtendLeaf(xPath, ps.newRepairKey()) proc rTreeSplitNode( ps: AccountsDbSessionRef; rPath: RPath; key: RepairKey; node: RNodeRef ): RPath = ## Replace `Leaf` or `Extension` node in tuple `(key,node)` by parts (see ## `rTreeExtend()`): ## ## left(Extension) -> middle(Branch) -> right(Extension or Leaf) ## ^ ^ ## | | ## added-to-path added-to-path ## ## where either `left()` or `right()` extensions might be missing. ## let nibbles = node.xPfx lLen = rPath.tail.sharedPrefixLen(nibbles) if nibbles.len == 0 or rPath.tail.len <= lLen: return # Ooops (^^^^^ otherwise `rPath` was not the longest) var mKey = key let mNibble = nibbles[lLen] # exists as `lLen < tail.len` rPfx = nibbles.slice(lLen + 1) # might be empty OK result = rPath # Insert node (if any): left(Extension) if 0 < lLen: let lNode = RNodeRef( state: Mutable, kind: Extension, ePfx: result.tail.slice(0,lLen), eLink: ps.newRepairKey()) ps.rpDB.tab[key] = lNode result.path.add RPathStep(key: key, node: lNode, nibble: -1) result.tail = result.tail.slice(lLen) mKey = lNode.eLink # Insert node: middle(Branch) let mNode = RNodeRef( state: Mutable, kind: Branch) ps.rpDB.tab[mKey] = mNode result.path.add RPathStep(key: mKey, node: mNode, nibble: -1) # no nibble yet # Insert node (if any): right(Extension) -- not to be registered in `rPath` if 0 < rPfx.len: let rKey = ps.newRepairKey() # Re-use argument node mNode.bLink[mNibble] = rKey ps.rpDB.tab[rKey] = node node.xPfx = rPfx # Otherwise merge argument node elif node.kind == Extension: mNode.bLink[mNibble] = node.eLink else: # Oops, does it make sense, at all? mNode.bData = node.lData # ------------------------------------------------------------------------------ # Private functions, repair tree actions # ------------------------------------------------------------------------------ proc rTreeFollow(nodeKey: NodeKey; ps: AccountsDbSessionRef): RPath = ## Compute logest possible path matching the `nodeKey` nibbles. result.tail = nodeKey.to(NibblesSeq) noKeyError("rTreeFollow"): var key = ps.rootKey.to(RepairKey) while ps.rpDB.tab.hasKey(key) and 0 < result.tail.len: let node = ps.rpDB.tab[key] case node.kind: of Leaf: if result.tail.len == result.tail.sharedPrefixLen(node.lPfx): # Bingo, got full path result.path.add RPathStep(key: key, node: node, nibble: -1) result.tail = EmptyNibbleRange return of Branch: let nibble = result.tail[0].int8 if node.bLink[nibble].isZero: return result.path.add RPathStep(key: key, node: node, nibble: nibble) result.tail = result.tail.slice(1) key = node.bLink[nibble] of Extension: if node.ePfx.len != result.tail.sharedPrefixLen(node.ePfx): return result.path.add RPathStep(key: key, node: node, nibble: -1) result.tail = result.tail.slice(node.ePfx.len) key = node.eLink proc rTreeFollow(nodeTag: NodeTag; ps: AccountsDbSessionRef): RPath = ## Variant of `rTreeFollow()` nodeTag.to(NodeKey).rTreeFollow(ps) proc rTreeInterpolate(rPath: RPath; ps: AccountsDbSessionRef): RPath = ## Extend path, add missing nodes to tree. The last node added will be ## a `Leaf` node if this function succeeds. ## ## The function assumed that the `RPath` argument is the longest possible ## as just constructed by `rTreeFollow()` if 0 < rPath.path.len and 0 < rPath.tail.len: noKeyError("rTreeExtend"): let step = rPath.path[^1] case step.node.kind: of Branch: # Now, the slot must not be empty. An empty slot would lead to a # rejection of this record as last valid step, contrary to the # assumption `path` is the longest one. if step.nibble < 0: return # sanitary check failed let key = step.node.bLink[step.nibble] if key.isZero: return # sanitary check failed # Case: unused slot => add leaf record if not ps.rpDB.tab.hasKey(key): return ps.rTreeExtendLeaf(rPath, key) # So a `child` node exits but it is something that could not be used to # extend the argument `path` which is assumed the longest possible one. let child = ps.rpDB.tab[key] case child.kind: of Branch: # So a `Leaf` node can be linked into the `child` branch return ps.rTreeExtendLeaf(rPath, key, child) # Need to split the right `grandChild` in `child -> grandChild` # into parts: # # left(Extension) -> middle(Branch) # | | # | +-----> right(Extension or Leaf) ... # +---------> new Leaf record # # where either `left()` or `right()` extensions might be missing of Extension, Leaf: var xPath = ps.rTreeSplitNode(rPath, key, child) if 0 < xPath.path.len: # Append `Leaf` node xPath.path[^1].nibble = xPath.tail[0].int8 xPath.tail = xPath.tail.slice(1) return ps.rTreeExtendLeaf(xPath, ps.newRepairKey()) of Leaf: return # Oops of Extension: let key = step.node.eLink var child: RNodeRef if ps.rpDB.tab.hasKey(key): child = ps.rpDB.tab[key] # `Extension` can only be followed by a `Branch` node if child.kind != Branch: return else: # Case: unused slot => add `Branch` and `Leaf` record child = RNodeRef( state: Mutable, kind: Branch) ps.rpDB.tab[key] = child # So a `Leaf` node can be linked into the `child` branch return ps.rTreeExtendLeaf(rPath, key, child) proc rTreeInterpolate( rPath: RPath; ps: AccountsDbSessionRef; payload: Blob ): RPath = ## Variant of `rTreeExtend()` which completes a `Leaf` record. result = rPath.rTreeInterpolate(ps) if 0 < result.path.len and result.tail.len == 0: let node = result.path[^1].node if node.kind != Extension and node.state == Mutable: node.xData = payload proc rTreeUpdateKeys(rPath: RPath; ps: AccountsDbSessionRef): Result[void,int] = ## The argument `rPath` is assumed to organise database nodes as ## ## root -> ... -> () -> () -> ... -> () -> () ... ## |-------------| |------------| |------ ## static nodes locked nodes mutable nodes ## ## Where ## * Static nodes are read-only nodes provided by the proof database ## * Locked nodes are added read-only nodes that satisfy the proof condition ## * Mutable nodes are incomplete nodes ## ## Then update nodes from the right end and set all the mutable nodes ## locked if possible. var rTop = rPath.path.len stack: seq[RPathXStep] if 0 < rTop and rPath.path[^1].node.state == Mutable and rPath.path[0].node.state != Mutable: # Set `Leaf` entry let leafNode = rPath.path[^1].node.dup stack.add RPathXStep( pos: rTop - 1, canLock: true, step: RPathStep( node: leafNode, key: leafNode.convertTo(Blob).digestTo(NodeKey).to(RepairKey), nibble: -1)) while true: rTop.dec # Update parent node (note that `2 <= rPath.path.len`) let thisKey = stack[^1].step.key preStep = rPath.path[rTop-1] preNibble = preStep.nibble # End reached if preStep.node.state != Mutable: # Verify the tail matches var key = RepairKey.default case preStep.node.kind: of Branch: key = preStep.node.bLink[preNibble] of Extension: key = preStep.node.eLink of Leaf: discard if key != thisKey: return err(rTop-1) when RepairTreeDebugging: echo "*** rTreeUpdateKeys", " rPath\n ", rPath.pp(ps), "\n stack\n ", stack.pp(ps) # Ok, replace database records by stack entries var lockOk = true for n in countDown(stack.len-1,0): let item = stack[n] ps.rpDB.tab.del(rPath.path[item.pos].key) ps.rpDB.tab[item.step.key] = item.step.node if lockOk: if item.canLock: item.step.node.state = Locked else: lockOk = false if not lockOk: return err(rTop-1) # repeat break # Done ok() stack.add RPathXStep( pos: rTop - 1, step: RPathStep( node: preStep.node.dup, # (!) nibble: preNibble, key: preStep.key)) case stack[^1].step.node.kind: of Branch: stack[^1].step.node.bLink[preNibble] = thisKey # Check whether all keys are proper, non-temporary keys stack[^1].canLock = true for n in 0 ..< 16: if not stack[^1].step.node.bLink[n].isNodeKey: stack[^1].canLock = false break of Extension: stack[^1].step.node.eLink = thisKey stack[^1].canLock = thisKey.isNodeKey of Leaf: return err(rTop-1) # Must not overwrite a non-temprary key if stack[^1].canLock: stack[^1].step.key = stack[^1].step.node.convertTo(Blob).digestTo(NodeKey).to(RepairKey) ok() # ------------------------------------------------------------------------------ # Private walk along hexary trie records # ------------------------------------------------------------------------------ proc hexaryFollow( ps: AccountsDbSessionRef; root: NodeKey; path: NibblesSeq; getFn: HexaryGetFn ): (int, bool, Blob) {.gcsafe, raises: [Defect,RlpError]} = ## Returns the number of matching digits/nibbles from the argument `path` ## found in the proofs trie. let nNibbles = path.len var inPath = path recKey = root.ByteArray32.toSeq leafBlob: Blob emptyRef = false when BasicChainTrieDebugging: trace "follow", rootKey=root.pp(ps), path while true: let value = getFn(recKey) if value.len == 0: break var nodeRlp = rlpFromBytes value case nodeRlp.listLen: of 2: let (isLeaf, pathSegment) = hexPrefixDecode nodeRlp.listElem(0).toBytes sharedNibbles = inPath.sharedPrefixLen(pathSegment) fullPath = sharedNibbles == pathSegment.len inPathLen = inPath.len inPath = inPath.slice(sharedNibbles) # Leaf node if isLeaf: let leafMode = sharedNibbles == inPathLen if fullPath and leafMode: leafBlob = nodeRlp.listElem(1).toBytes when BasicChainTrieDebugging: let nibblesLeft = inPathLen - sharedNibbles trace "follow leaf", fullPath, leafMode, sharedNibbles, nibblesLeft, pathSegment, newPath=inPath break # Extension node if fullPath: let branch = nodeRlp.listElem(1) if branch.isEmpty: when BasicChainTrieDebugging: trace "follow extension", newKey="n/a" emptyRef = true break recKey = branch.toBytes when BasicChainTrieDebugging: trace "follow extension", newKey=recKey.convertTo(NodeKey).pp(ps), newPath=inPath else: when BasicChainTrieDebugging: trace "follow extension", fullPath, sharedNibbles, pathSegment, inPathLen, newPath=inPath break of 17: # Branch node if inPath.len == 0: leafBlob = nodeRlp.listElem(1).toBytes break let inx = inPath[0].int branch = nodeRlp.listElem(inx) if branch.isEmpty: when BasicChainTrieDebugging: trace "follow branch", newKey="n/a" emptyRef = true break inPath = inPath.slice(1) recKey = branch.toBytes when BasicChainTrieDebugging: trace "follow branch", newKey=recKey.convertTo(NodeKey).pp(ps), inx, newPath=inPath else: when BasicChainTrieDebugging: trace "follow oops", nColumns = nodeRlp.listLen break # end while let pathLen = nNibbles - inPath.len when BasicChainTrieDebugging: trace "follow done", recKey, emptyRef, pathLen, leafSize=leafBlob.len (pathLen, emptyRef, leafBlob) proc hexaryFollow( ps: AccountsDbSessionRef; root: NodeKey; path: NodeKey; getFn: HexaryGetFn ): (int, bool, Blob) {.gcsafe, raises: [Defect,RlpError]} = ## Variant of `hexaryFollow()` ps.hexaryFollow(root, path.to(NibblesSeq), getFn) # ------------------------------------------------------------------------------ # Public constructor # ------------------------------------------------------------------------------ proc init*( T: type AccountsDbRef; db: TrieDatabaseRef ): T = ## Main object constructor T(db: db) proc init*( T: type AccountsDbRef; db: ChainDb ): T = ## Variant of `init()` result = T(db: db.trieDB, rocky: db.rocksStoreRef) if not result.rocky.isNil: # A cache file might hang about from a previous crash try: discard result.rocky.clearCacheFile(RockyBulkCache) except OSError as e: result.rocky = nil error "Cannot clear rocksdb cache, bulkload disabled", exception=($e.name), msg=e.msg proc init*( T: type AccountsDbSessionRef; pv: AccountsDbRef; root: Hash256; peer: Peer = nil ): T = ## Start a new session, do some actions an then discard the session ## descriptor (probably after commiting data.) AccountsDbSessionRef(base: pv, peer: peer, rootKey: root.to(NodeKey)) # ------------------------------------------------------------------------------ # Public functions, session related # ------------------------------------------------------------------------------ proc merge*( ps: AccountsDbSessionRef; proof: SnapAccountProof ): Result[void,AccountsDbError] {.gcsafe, raises: [Defect, RlpError].} = ## Import account proof records (as received with the snap message ## `AccountRange`) into the hexary trie of the repair database. These hexary ## trie records can be extended to a full trie at a later stage and used for ## validating account data. for n,rlpRec in proof: let rc = ps.hexaryImport(rlpRec) if rc.isErr: trace "merge(SnapAccountProof)", peer=ps.peer, proofs=ps.rpDB.tab.len, accounts=ps.rpDB.acc.len, error=rc.error return err(rc.error) ok() proc merge*( ps: AccountsDbSessionRef; base: NodeTag; acc: seq[SnapAccount]; ): Result[void,AccountsDbError] {.gcsafe, raises: [Defect, RlpError].} = ## Import account records (as received with the snap message `AccountRange`) ## into the accounts list of the repair database. The accounts, together ## with some hexary trie records for proof can be used for validating ## the argument account data. ## if acc.len != 0: let pathTag0 = acc[0].accHash.to(NodeTag) pathTagTop = acc[^1].accHash.to(NodeTag) saveLen = ps.rpDB.acc.len # For error logging (peer, proofs, accounts) = (ps.peer, ps.rpDB.tab.len, ps.rpDB.acc.len) var error = NothingSerious saveQ: seq[RAccount] prependOk = false if 0 < ps.rpDB.acc.len: if pathTagTop <= ps.rpDB.acc[0].pathTag: # Prepend `acc` argument before `ps.rpDB.acc` saveQ = ps.rpDB.acc prependOk = true # Append, verify that there is no overlap elif pathTag0 <= ps.rpDB.acc[^1].pathTag: return err(AccountRangesOverlap) block collectAccounts: # Verify lower bound if pathTag0 < base: error = AccountSmallerThanBase trace "merge(seq[SnapAccount])", peer, proofs, base, accounts, error break collectAccounts # Add base for the records (no payload). Note that the assumption # holds: `ps.rpDB.acc[^1].tag <= base` if base < pathTag0: ps.rpDB.acc.add RAccount(pathTag: base) # Check for the case that accounts are appended elif 0 < ps.rpDB.acc.len and pathTag0 <= ps.rpDB.acc[^1].pathTag: error = AccountsNotSrictlyIncreasing trace "merge(seq[SnapAccount])", peer, proofs, base, accounts, error break collectAccounts # Add first account ps.rpDB.acc.add RAccount( pathTag: pathTag0, payload: acc[0].accBody.encode) # Veify & add other accounts for n in 1 ..< acc.len: let nodeTag = acc[n].accHash.to(NodeTag) if nodeTag <= ps.rpDB.acc[^1].pathTag: # Recover accounts list and return error ps.rpDB.acc.setLen(saveLen) error = AccountsNotSrictlyIncreasing trace "merge(seq[SnapAccount])", peer, proofs, base, accounts, error break collectAccounts ps.rpDB.acc.add RAccount( pathTag: nodeTag, payload: acc[n].accBody.encode) # End block `collectAccounts` if prependOk: if error == NothingSerious: ps.rpDB.acc = ps.rpDB.acc & saveQ else: ps.rpDB.acc = saveQ if error != NothingSerious: return err(error) ok() proc interpolate*(ps: AccountsDbSessionRef): Result[void,AccountsDbError] = ## Verifiy accounts by interpolating the collected accounts on the hexary ## trie of the repair database. If all accounts can be represented in the ## hexary trie, they are vonsidered validated. ## ## Note: ## This function temporary and proof-of-concept. for production purposes, ## it must be replaced by the new facility of the upcoming re-factored ## database layer. ## # Walk top down and insert/complete missing account access nodes for n in countDown(ps.rpDB.acc.len-1,0): let acc = ps.rpDB.acc[n] if acc.payload.len != 0: let rPath = acc.pathTag.rTreeFollow(ps) var repairKey = acc.nodeKey if repairKey.isZero and 0 < rPath.path.len and rPath.tail.len == 0: repairKey = rPath.path[^1].key ps.rpDB.acc[n].nodeKey = repairKey if repairKey.isZero: let update = rPath.rTreeInterpolate(ps, acc.payload) final = acc.pathTag.rTreeFollow(ps) if update != final: return err(AccountRepairBlocked) ps.rpDB.acc[n].nodeKey = rPath.path[^1].key # Replace temporary repair keys by proper hash based node keys. var reVisit: seq[NodeTag] for n in countDown(ps.rpDB.acc.len-1,0): let acc = ps.rpDB.acc[n] if not acc.nodeKey.isZero: let rPath = acc.pathTag.rTreeFollow(ps) if rPath.path[^1].node.state == Mutable: let rc = rPath.rTreeUpdateKeys(ps) if rc.isErr: reVisit.add acc.pathTag while 0 < reVisit.len: var again: seq[NodeTag] for nodeTag in reVisit: let rc = nodeTag.rTreeFollow(ps).rTreeUpdateKeys(ps) if rc.isErr: again.add nodeTag if reVisit.len <= again.len: return err(BoundaryProofFailed) reVisit = again ok() proc dbImports*(ps: AccountsDbSessionRef): Result[void,AccountsDbError] = ## Experimental: try several db-import modes and record statistics var als: AccountLoadStats noPpError("dbImports"): als.dura[0].elapsed: let rc = ps.storeAccountPathsOnChainDb() if rc.isErr: return rc als.dura[1].elapsed: let rc = ps.storeHexaryNodesOnChainDb() if rc.isErr: return rc als.dura[2].elapsed: let rc = ps.storeAccountPathsOnRockyDb() if rc.isErr: return rc als.dura[3].elapsed: let rc = ps.storeHexaryNodesOnRockyDb() if rc.isErr: return rc for n in 0 ..< 4: ps.dStats.dura[n] += als.dura[n] ps.base.aStats.dura[n] += als.dura[n] ps.dStats.size[0] += ps.rpDB.acc.len.uint64 ps.base.aStats.size[0] += ps.rpDB.acc.len.uint64 ps.dStats.size[1] += ps.rpDB.tab.len.uint64 ps.base.aStats.size[1] += ps.rpDB.tab.len.uint64 ok() proc sortMerge*(base: openArray[NodeTag]): NodeTag = ## Helper for merging several `(NodeTag,seq[SnapAccount])` data sets ## so that there are no overlap which would be rejected by `merge()`. ## ## This function selects a `NodeTag` from a list. result = high(NodeTag) for w in base: if w < result: result = w proc sortMerge*(acc: openArray[seq[SnapAccount]]): seq[SnapAccount] = ## Helper for merging several `(NodeTag,seq[SnapAccount])` data sets ## so that there are no overlap which would be rejected by `merge()`. ## ## This function flattens and sorts the argument account lists. noPpError("sortMergeAccounts"): var accounts: Table[NodeTag,SnapAccount] for accList in acc: for item in accList: accounts[item.accHash.to(NodeTag)] = item result = toSeq(accounts.keys).sorted(cmp).mapIt(accounts[it]) proc nHexaryRecords*(ps: AccountsDbSessionRef): int = ## Number of hexary record entries in the session database. ps.rpDB.tab.len proc nAccountRecords*(ps: AccountsDbSessionRef): int = ## Number of account records in the session database. This number includes ## lower bound entries (which are not accoiunts, strictly speaking.) ps.rpDB.acc.len # ------------------------------------------------------------------------------ # Public functions # ------------------------------------------------------------------------------ proc dbBackendRocksDb*(pv: AccountsDbRef): bool = ## Returns `true` if rocksdb features are available not pv.rocky.isNil proc importAccounts*( pv: AccountsDbRef; peer: Peer, ## for log messages root: Hash256; ## state root base: NodeTag; ## before or at first account entry in `data` data: SnapAccountRange; ## `snap/1 ` reply data storeData = false ): Result[void,AccountsDbError] = ## Validate and accounts and proofs (as received with the snap message ## `AccountRange`). This function combines the functionality of the `merge()` ## and the `interpolate()` functions. ## ## At a later stage, that function also will bulk-import the accounts into ## the block chain database ## ## Note that the `peer` argument is for log messages, only. let ps = AccountsDbSessionRef.init(pv, root, peer) try: block: let rc = ps.merge(data.proof) if rc.isErr: return err(rc.error) block: let rc = ps.merge(base, data.accounts) if rc.isErr: return err(rc.error) except RlpError: return err(RlpEncoding) block: # Note: # `interpolate()` is a temporary proof-of-concept function. For # production purposes, it must be replaced by the new facility of # the upcoming re-factored database layer. let rc = ps.interpolate() if rc.isErr: return err(rc.error) if storeData: # Experimental let rc = ps.dbImports() if rc.isErr: return err(rc.error) trace "Accounts and proofs ok", peer, root=root.data.toHex, proof=data.proof.len, base, accounts=data.accounts.len ok() # ------------------------------------------------------------------------------ # Debugging # ------------------------------------------------------------------------------ proc getChainDbAccount*( ps: AccountsDbSessionRef; accHash: Hash256 ): Result[Account,AccountsDbError] = ## Fetch account via `BaseChainDB` try: let getFn: HexaryGetFn = proc(key: Blob): Blob = ps.base.db.get(key) (_, _, leafBlob) = ps.hexaryFollow(ps.rootKey, accHash.to(NodeKey), getFn) if 0 < leafBlob.len: let acc = rlp.decode(leafBlob,Account) return ok(acc) except RlpError: return err(RlpEncoding) except Defect as e: raise e except Exception as e: raiseAssert "Ooops getChainDbAccount(): name=" & $e.name & " msg=" & e.msg err(AccountNotFound) proc getRockyAccount*( ps: AccountsDbSessionRef; accHash: Hash256 ): Result[Account,AccountsDbError] = ## Fetch account via rocksdb table (paraellel to `BaseChainDB`) try: let getFn: HexaryGetFn = proc(key: Blob): Blob = var tag: NodeTag discard tag.init(key) ps.base.db.get(tag.rockyBulkHexaryKey().toOpenArray) (_, _, leafBlob) = ps.hexaryFollow(ps.rootKey, accHash.to(NodeKey), getFn) if 0 < leafBlob.len: let acc = rlp.decode(leafBlob,Account) return ok(acc) except RlpError: return err(RlpEncoding) except Defect as e: raise e except Exception as e: raiseAssert "Ooops getChainDbAccount(): name=" & $e.name & " msg=" & e.msg err(AccountNotFound) proc dbImportStats*(ps: AccountsDbSessionRef): AccountLoadStats = ## Session data load statistics ps.dStats proc dbImportStats*(pv: AccountsDbRef): AccountLoadStats = ## Accumulated data load statistics pv.aStats proc assignPrettyKeys*(ps: AccountsDbSessionRef) = ## Prepare foe pretty pringing/debugging. Run early enough this function ## sets the root key to `"$"`, for instance. noPpError("validate(1)"): # Make keys assigned in pretty order for printing var keysList = toSeq(ps.rpDB.tab.keys) let rootKey = ps.rootKey.to(RepairKey) discard rootKey.toKey(ps) if ps.rpDB.tab.hasKey(rootKey): keysList = @[rootKey] & keysList for key in keysList: let node = ps.rpDB.tab[key] discard key.toKey(ps) case node.kind: of Branch: (for w in node.bLink: discard w.toKey(ps)) of Extension: discard node.eLink.toKey(ps) of Leaf: discard proc dumpPath*(ps: AccountsDbSessionRef; key: NodeTag): seq[string] = ## Pretty print helper compiling the path into the repair tree for the ## argument `key`. let rPath = key.rTreeFollow(ps) rPath.path.mapIt(it.pp(ps)) & @["(" & rPath.tail.pp & ")"] proc dumpProofsDB*(ps: AccountsDbSessionRef): seq[string] = ## Dump the entries from the repair tree. var accu = @[(0u, "($0" & "," & ps.rootKey.pp(ps) & ")")] for key,node in ps.rpDB.tab.pairs: accu.add (key.toKey(ps), "(" & key.pp(ps) & "," & node.pp(ps) & ")") proc cmpIt(x, y: (uint,string)): int = cmp(x[0],y[0]) result = accu.sorted(cmpIt).mapIt(it[1]) # --------- proc dumpRoot*(root: Hash256; name = "snapRoot*"): string = noPpError("dumpRoot"): result = "import\n" result &= " eth/common/eth_types,\n" result &= " nimcrypto/hash,\n" result &= " stew/byteutils\n\n" result &= "const\n" result &= &" {name} =\n" result &= &" \"{root.pp(false)}\".toDigest\n" proc dumpSnapAccountRange*( base: NodeTag; data: SnapAccountRange; name = "snapData*" ): string = noPpError("dumpSnapAccountRange"): result = &" {name} = (" result &= &"\n \"{base.to(Hash256).pp(false)}\".toDigest," result &= "\n @[" let accPfx = "\n " for n in 0 ..< data.accounts.len: let hash = data.accounts[n].accHash body = data.accounts[n].accBody if 0 < n: result &= accPfx result &= &"# <{n}>" result &= &"{accPfx}(\"{hash.pp(false)}\".toDigest," result &= &"{accPfx} {body.nonce}u64," result &= &"{accPfx} \"{body.balance}\".parse(Uint256)," result &= &"{accPfx} \"{body.storageRoot.pp(false)}\".toDigest," result &= &"{accPfx} \"{body.codehash.pp(false)}\".toDigest)," if result[^1] == ',': result[^1] = ']' else: result &= "]" result &= ",\n @[" let blobPfx = "\n " for n in 0 ..< data.proof.len: let blob = data.proof[n] if 0 < n: result &= blobPfx result &= &"# <{n}>" result &= &"{blobPfx}\"{blob.pp}\".hexToSeqByte," if result[^1] == ',': result[^1] = ']' else: result &= "]" result &= ")\n" # ------------------------------------------------------------------------------ # End # ------------------------------------------------------------------------------