nimbus-eth1/nimbus/sync/snap/worker/db/hexary_nearby.nim

459 lines
15 KiB
Nim
Raw Normal View History

# nimbus-eth1
# 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.
{.push raises: [].}
import
std/tables,
eth/[common, trie/nibbles],
stew/results,
../../range_desc,
"."/[hexary_desc, hexary_error, hexary_nodes_helper, hexary_paths]
# ------------------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------------------
proc `<=`(a, b: NibblesSeq): bool =
## Compare nibbles, different lengths are padded to the right with zeros
let abMin = min(a.len, b.len)
for n in 0 ..< abMin:
if a[n] < b[n]:
return true
if b[n] < a[n]:
return false
# otherwise a[n] == b[n]
# Assuming zero for missing entries
if b.len < a.len:
for n in abMin + 1 ..< a.len:
if 0 < a[n]:
return false
true
proc `<`(a, b: NibblesSeq): bool =
not (b <= a)
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
proc complete(
path: RPath|XPath; # Partially expanded path
key: RepairKey|NodeKey|Blob; # Start key
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
pathLenMax: int; # Beware of loops (if any)
doLeast: static[bool]; # Direction: *least* or *most*
): auto
{.gcsafe, raises: [CatchableError].} =
## Extend path using least or last nodes without recursion.
var uPath = typeof(path)(root: path.root, path: path.path)
let firstNode = key.getNode(db)
if firstNode.isErr:
return Result[typeof(path),HexaryError].err(firstNode.error)
var
key = key
node = firstNode.value
while uPath.path.len < pathLenMax:
case node.kind:
of Leaf:
uPath.path.add typeof(path.path[0])(key: key, node: node, nibble: -1)
return ok(uPath) # done
of Extension:
let newKey = node.eLink
if not newkey.isZeroLink:
let newNode = newKey.getNode(db)
if newNode.isOK:
uPath.path.add typeof(path.path[0])(key: key, node: node, nibble: -1)
key = newKey
node = newNode.value
continue
return err(NearbyExtensionError) # Oops, no way
of Branch:
let n = block:
when doLeast:
node.branchNibbleMin 0
else:
node.branchNibbleMax 15
if 0 <= n:
let
newKey = node.bLink[n]
newNode = newKey.getNode(db)
if newNode.isOK:
uPath.path.add typeof(path.path[0])(key: key, node: node, nibble: n)
key = newKey
node = newNode.value
continue
return err(NearbyBranchError) # Oops, no way
err(NearbyNestingTooDeep)
proc zeroAdjust(
path: XPath|RPath; # Partially expanded path
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
doLeast: static[bool]; # Direction: *least* or *most*
): auto
{.gcsafe, raises: [CatchableError].} =
## Adjust empty argument path to the first node entry to the right. Ths
## applies is the argument path `path` is before the first entry in the
## database. The result is a path which is aligned with the first entry.
proc accept(p: typeof(path); pfx: NibblesSeq): bool =
when doLeast:
p.tail <= pfx
else:
pfx <= p.tail
proc branchBorderNibble(w: typeof(path.path[0].node); n: int8): int8 =
when doLeast:
w.branchNibbleMin n
else:
w.branchNibbleMax n
if path.path.len != 0:
return Result[typeof(path),HexaryError].ok(path)
let root = path.root.getNode(db)
if root.isOk:
block fail:
var pfx: NibblesSeq
case root.value.kind:
of Branch:
# Find first non-dangling link and assign it
if path.tail.len == 0:
break fail
let n = root.value.branchBorderNibble path.tail[0].int8
if n < 0:
# Before or after the database range
return err(NearbyBeyondRange)
pfx = @[n.byte].initNibbleRange.slice(1)
of Extension:
let ePfx = root.value.ePfx
# Must be followed by a branch node
if path.tail.len < 2 or not path.accept(ePfx):
break fail
let node = root.value.eLink.getNode(db)
if node.isErr:
break fail
let n = node.value.branchBorderNibble path.tail[1].int8
if n < 0:
# Before or after the database range
return err(NearbyBeyondRange)
pfx = ePfx & @[n.byte].initNibbleRange.slice(1)
of Leaf:
pfx = root.value.lPfx
if not path.accept(pfx):
# Before or after the database range
return err(NearbyBeyondRange)
let newPath = pfx.padPartialPath(0).hexaryPath(path.root, db)
if 0 < newPath.path.len:
return ok(newPath)
err(NearbyEmptyPath)
proc finalise(
path: XPath|RPath; # Partially expanded path
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
moveRight: static[bool]; # Direction of next node
): auto
{.gcsafe, raises: [CatchableError].} =
## Handle some pathological cases after main processing failed
proc beyond(p: typeof(path); pfx: NibblesSeq): bool =
when moveRight:
pfx < p.tail
else:
p.tail < pfx
proc branchBorderNibble(w: typeof(path.path[0].node)): int8 =
when moveRight:
w.branchNibbleMax 15
else:
w.branchNibbleMin 0
# Just for completeness (this case should have been handled, already)
if path.path.len == 0:
return Result[typeof(path),HexaryError].err(NearbyEmptyPath)
# Check whether the path is beyond the database range
if 0 < path.tail.len: # nothing to compare against, otherwise
let top = path.path[^1]
# Note that only a `Branch` nodes has a non-zero nibble
if 0 <= top.nibble and top.nibble == top.node.branchBorderNibble:
# Check the following up node
let rc = top.node.bLink[top.nibble].getNode(db)
if rc.isErr:
return err(NearbyDanglingLink)
var pfx: NibblesSeq
case rc.value.kind:
of Leaf:
pfx = rc.value.lPfx
of Extension:
pfx = rc.value.ePfx
of Branch:
pfx = @[rc.value.branchBorderNibble.byte].initNibbleRange.slice(1)
if path.beyond pfx:
return err(NearbyBeyondRange)
# Pathological cases
# * finalise right: nfffff.. for n < f or
# * finalise left: n00000.. for 0 < n
if path.path[0].node.kind == Branch or
(1 < path.path.len and path.path[1].node.kind == Branch):
return err(NearbyFailed) # no more nodes
err(NearbyUnexpectedNode) # error
proc nearbyNext(
path: RPath|XPath; # Partially expanded path
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
moveRight: static[bool]; # Direction of next node
pathLenMax = 64; # Beware of loops (if any)
): auto
{.gcsafe, raises: [CatchableError].} =
## Unified implementation of `hexaryNearbyRight()` and `hexaryNearbyLeft()`.
proc accept(nibble: int8): bool =
## Accept `nibble` unless on boundaty dependent on `moveRight`
when moveRight:
nibble < 15
else:
0 < nibble
proc accept(p: typeof(path); pfx: NibblesSeq): bool =
when moveRight:
p.tail <= pfx
else:
pfx <= p.tail
proc branchNibbleNext(w: typeof(path.path[0].node); n: int8): int8 =
when moveRight:
w.branchNibbleMin(n + 1)
else:
w.branchNibbleMax(n - 1)
# Some easy cases
var path = block:
let rc = path.zeroAdjust(db, doLeast=moveRight)
if rc.isErr:
return Result[typeof(path),HexaryError].err(rc.error)
rc.value
var
uPath = path
start = true
while 0 < uPath.path.len:
let top = uPath.path[^1]
case top.node.kind:
of Leaf:
return ok(uPath)
of Branch:
if top.nibble < 0 or uPath.tail.len == 0:
return err(NearbyUnexpectedNode)
of Extension:
uPath.tail = top.node.ePfx & uPath.tail
uPath.path.setLen(uPath.path.len - 1)
continue
var
step = top
let
uPathLen = uPath.path.len # in case of backtracking
uPathTail = uPath.tail # in case of backtracking
# Look ahead checking next node
if start:
let
topLink = top.node.bLink[top.nibble]
nextNode = block:
if topLink.isZeroLink:
return err(NearbyDanglingLink) # error
let rc = topLink.getNode(db)
if rc.isErr:
return err(rc.error) # error
rc.value
case nextNode.kind
of Leaf:
if uPath.accept(nextNode.lPfx):
return uPath.complete(topLink, db, pathLenMax, doLeast=moveRight)
of Extension:
if uPath.accept(nextNode.ePfx):
return uPath.complete(topLink, db, pathLenMax, doLeast=moveRight)
of Branch:
let nextNibble = uPath.tail[0].int8
if start and accept(nextNibble):
# Step down and complete with a branch link on the child node
step = typeof(path.path[0])(
key: topLink,
node: nextNode,
nibble: nextNibble)
uPath.path &= step
# Find the next item to the right/left of the current top entry
let n = step.node.branchNibbleNext step.nibble
if 0 <= n:
uPath.path[^1].nibble = n
return uPath.complete(
step.node.bLink[n], db, pathLenMax, doLeast=moveRight)
if start:
# Retry without look ahead
start = false
# Restore `uPath` (pop temporary extra step)
if uPathLen < uPath.path.len:
uPath.path.setLen(uPathLen)
uPath.tail = uPathTail
else:
# Pop current `Branch` node on top and append nibble to `tail`
uPath.tail = @[top.nibble.byte].initNibbleRange.slice(1) & uPath.tail
uPath.path.setLen(uPath.path.len - 1)
# End while
# Handle some pathological cases
return path.finalise(db, moveRight)
proc nearbyNext(
baseTag: NodeTag; # Some node
rootKey: NodeKey; # State root
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
moveRight: static[bool]; # Direction of next node
pathLenMax = 64; # Beware of loops (if any)
): Result[NodeTag,HexaryError]
{.gcsafe, raises: [CatchableError].} =
## Variant of `nearbyNext()`, convenience wrapper
let rc = baseTag.hexaryPath(rootKey, db).nearbyNext(db, moveRight)
if rc.isErr:
return err(rc.error)
let path = rc.value
if 0 < path.path.len and path.path[^1].node.kind == Leaf:
let nibbles = path.getNibbles
if nibbles.len == 64:
return ok(nibbles.getBytes.convertTo(NodeTag))
err(NearbyLeafExpected)
# ------------------------------------------------------------------------------
# Public functions, moving and right boundary proof
# ------------------------------------------------------------------------------
proc hexaryNearbyRight*(
path: RPath|XPath; # Partially expanded path
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
): auto
{.gcsafe, raises: [CatchableError].} =
## Extends the maximally extended argument nodes `path` to the right (i.e.
## with non-decreasing path value). This is similar to the
## `hexary_path.next()` function, only that this algorithm does not
## backtrack if there are dangling links in between and rather returns
## an error.
##
## In the case that there is no more leaf node to the right of the argument
## path, the particular error code `NearbyBeyondRange` is returned.
##
## This code is intended to be used for verifying a left-bound proof to
## verify that there is no leaf node *right* of a boundary path value.
path.nearbyNext(db, moveRight=true)
proc hexaryNearbyRight*(
baseTag: NodeTag; # Some node
rootKey: NodeKey; # State root
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
): Result[NodeTag,HexaryError]
{.gcsafe, raises: [CatchableError].} =
## Variant of `hexaryNearbyRight()` working with `NodeTag` arguments rather
## than `RPath` or `XPath` ones.
baseTag.nearbyNext(rootKey, db, moveRight=true)
proc hexaryNearbyLeft*(
path: RPath|XPath; # Partially expanded path
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
): auto
{.gcsafe, raises: [CatchableError].} =
## Similar to `hexaryNearbyRight()`.
##
## This code is intended to be used for verifying a right-bound proof to
## verify that there is no leaf node *left* to a boundary path value.
path.nearbyNext(db, moveRight=false)
proc hexaryNearbyLeft*(
baseTag: NodeTag; # Some node
rootKey: NodeKey; # State root
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
): Result[NodeTag,HexaryError]
{.gcsafe, raises: [CatchableError].} =
## Similar to `hexaryNearbyRight()` for `NodeKey` arguments.
baseTag.nearbyNext(rootKey, db, moveRight=false)
# ------------------------------------------------------------------------------
# Public debugging helpers
# ------------------------------------------------------------------------------
proc hexaryNearbyRightMissing*(
path: RPath|XPath; # Partially expanded path
db: HexaryTreeDbRef|HexaryGetFn; # Database abstraction
): Result[bool,HexaryError]
{.gcsafe, raises: [KeyError].} =
## Returns `true` if the maximally extended argument nodes `path` is the
## rightmost on the hexary trie database. It verifies that there is no more
## leaf entry to the right of the argument `path`. This function is an
## an alternative to
## ::
## let rc = path.hexaryNearbyRight(db)
## if rc.isOk:
## # not at the end => false
## ...
## elif rc.error != NearbyBeyondRange:
## # problem with database => error
## ...
## else:
## # no nore nodes => true
## ...
## and is intended mainly for debugging.
if path.path.len == 0:
return err(NearbyEmptyPath)
if 0 < path.tail.len:
return err(NearbyPathTail)
let top = path.path[^1]
if top.node.kind != Branch or top.nibble < 0:
return err(NearbyBranchError)
let nextNode = block:
let topLink = top.node.bLink[top.nibble]
if topLink.isZeroLink:
return err(NearbyDanglingLink) # error
let rc = topLink.getNode(db)
if rc.isErr:
return err(rc.error) # error
rc.value
case nextNode.kind
of Leaf:
return ok(nextNode.lPfx < path.tail)
of Extension:
return ok(nextNode.ePfx < path.tail)
of Branch:
return ok(nextNode.branchNibbleMin(path.tail[0].int8) < 0)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------