# nimbus-eth1
# 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.
## Aristo DB -- Patricia Trie delete funcionality
## ==============================================
##
## Delete by `Hike` type chain of vertices.
{.push raises: [].}
import
std/[sets, typetraits],
eth/[common, trie/nibbles],
results,
"."/[aristo_desc, aristo_get, aristo_hike, aristo_layers,
aristo_utils, aristo_vid]
type
SaveToVaeVidFn =
proc(err: AristoError): (VertexID,AristoError) {.gcsafe, raises: [].}
# ------------------------------------------------------------------------------
# Private heplers
# ------------------------------------------------------------------------------
func toVae(err: AristoError): (VertexID,AristoError) =
## Map single error to error pair with dummy vertex
(VertexID(0),err)
func toVae(vid: VertexID): SaveToVaeVidFn =
## Map single error to error pair with argument vertex
result =
proc(err: AristoError): (VertexID,AristoError) =
return (vid,err)
func toVae(err: (VertexID,AristoError,Hike)): (VertexID,AristoError) =
(err[0], err[1])
proc branchStillNeeded(vtx: VertexRef): Result[int,void] =
## Returns the nibble if there is only one reference left.
var nibble = -1
for n in 0 .. 15:
if vtx.bVid[n].isValid:
if 0 <= nibble:
return ok(-1)
nibble = n
if 0 <= nibble:
return ok(nibble)
# Oops, degenerated branch node
err()
# -----------
proc disposeOfVtx(
db: AristoDbRef; # Database, top layer
root: VertexID;
vid: VertexID; # Vertex IDs to clear
) =
# Remove entry
db.layersResVtx(root, vid)
db.layersResKey(root, vid)
db.vidDispose vid # Recycle ID
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
proc collapseBranch(
db: AristoDbRef; # Database, top layer
hike: Hike; # Fully expanded path
nibble: byte; # Applicable link for `Branch` vertex
): Result[void,(VertexID,AristoError)] =
## Convert/merge vertices:
## ::
## current | becomes | condition
## | |
## ^3 ^2 | ^3 ^2 |
## -------------------+---------------------+------------------
## Branch
Branch | Branch Branch | 2 < legs.len (1)
## Ext
Branch | Branch | 2 < legs.len (2)
##
Branch | Branch | legs.len == 2 (3)
##
## Depending on whether the parent `par` is an extension, merge `br` into
## `par`. Otherwise replace `br` by an extension.
##
let br = hike.legs[^2].wp
var xt = VidVtxPair( # Rewrite `br`
vid: br.vid,
vtx: VertexRef(
vType: Extension,
ePfx: @[nibble].initNibbleRange.slice(1),
eVid: br.vtx.bVid[nibble]))
if 2 < hike.legs.len: # (1) or (2)
let par = hike.legs[^3].wp
case par.vtx.vType:
of Branch: # (1)
# Replace `br` (use `xt` as-is)
discard
of Extension: # (2)
# Merge `br` into ^3 (update `xt`)
db.disposeOfVtx(hike.root, xt.vid)
xt.vid = par.vid
xt.vtx.ePfx = par.vtx.ePfx & xt.vtx.ePfx
of Leaf:
return err((par.vid,DelLeafUnexpected))
else: # (3)
# Replace `br` (use `xt` as-is)
discard
db.layersPutVtx(hike.root, xt.vid, xt.vtx)
ok()
proc collapseExt(
db: AristoDbRef; # Database, top layer
hike: Hike; # Fully expanded path
nibble: byte; # Link for `Branch` vertex `^2`
vtx: VertexRef; # Follow up extension vertex (nibble)
): Result[void,(VertexID,AristoError)] =
## Convert/merge vertices:
## ::
## ^3 ^2 `vtx` | ^3 ^2 |
## --------------------+-----------------------+------------------
## Branch
Ext | Branch | 2 < legs.len (1)
## Ext
Ext | | 2 < legs.len (2)
##
Ext | | legs.len == 2 (3)
##
## Merge `vtx` into `br` and unlink `vtx`.
##
let br = hike.legs[^2].wp
var xt = VidVtxPair( # Merge `vtx` into `br`
vid: br.vid,
vtx: VertexRef(
vType: Extension,
ePfx: @[nibble].initNibbleRange.slice(1) & vtx.ePfx,
eVid: vtx.eVid))
db.disposeOfVtx(hike.root, br.vtx.bVid[nibble]) # `vtx` is obsolete now
if 2 < hike.legs.len: # (1) or (2)
let par = hike.legs[^3].wp
case par.vtx.vType:
of Branch: # (1)
# Replace `br` by `^2 & vtx` (use `xt` as-is)
discard
of Extension: # (2)
# Replace ^3 by `^3 & ^2 & vtx` (update `xt`)
db.disposeOfVtx(hike.root, xt.vid)
xt.vid = par.vid
xt.vtx.ePfx = par.vtx.ePfx & xt.vtx.ePfx
of Leaf:
return err((par.vid,DelLeafUnexpected))
else: # (3)
# Replace ^2 by `^2 & vtx` (use `xt` as-is)
discard
db.layersPutVtx(hike.root, xt.vid, xt.vtx)
ok()
proc collapseLeaf(
db: AristoDbRef; # Database, top layer
hike: Hike; # Fully expanded path
nibble: byte; # Link for `Branch` vertex `^2`
vtx: VertexRef; # Follow up leaf vertex (from nibble)
): Result[void,(VertexID,AristoError)] =
## Convert/merge vertices:
## ::
## current | becomes | condition
## | |
## ^4 ^3 ^2 `vtx` | ^4 ^3 ^2 |
## -------------------------+----------------------------+------------------
## .. Branch
Leaf | .. Branch | 2 < legs.len (1)
## Branch Ext
Leaf | Branch | 3 < legs.len (2)
## Ext
Leaf | | legs.len == 3 (3)
##
Leaf | | legs.len == 2 (4)
##
## Merge `
` and `Leaf` replacing one and removing the other.
##
let br = hike.legs[^2].wp
var lf = VidVtxPair( # Merge `br` into `vtx`
vid: br.vtx.bVid[nibble],
vtx: VertexRef(
vType: Leaf,
lPfx: @[nibble].initNibbleRange.slice(1) & vtx.lPfx,
lData: vtx.lData))
db.layersResKey(hike.root, lf.vid) # `vtx` was modified
if 2 < hike.legs.len: # (1), (2), or (3)
db.disposeOfVtx(hike.root, br.vid) # `br` is obsolete now
# Merge `br` into the leaf `vtx` and unlink `br`.
let par = hike.legs[^3].wp.dup # Writable vertex
case par.vtx.vType:
of Branch: # (1)
# Replace `vtx` by `^2 & vtx` (use `lf` as-is)
par.vtx.bVid[hike.legs[^3].nibble] = lf.vid
db.layersPutVtx(hike.root, par.vid, par.vtx)
db.layersPutVtx(hike.root, lf.vid, lf.vtx)
return ok()
of Extension: # (2) or (3)
# Merge `^3` into `lf` but keep the leaf vertex ID unchanged. This
# can avoid some extra updates.
lf.vtx.lPfx = par.vtx.ePfx & lf.vtx.lPfx
if 3 < hike.legs.len: # (2)
# Grandparent exists
let gpr = hike.legs[^4].wp.dup # Writable vertex
if gpr.vtx.vType != Branch:
return err((gpr.vid,DelBranchExpexted))
db.disposeOfVtx(hike.root, par.vid) # `par` is obsolete now
gpr.vtx.bVid[hike.legs[^4].nibble] = lf.vid
db.layersPutVtx(hike.root, gpr.vid, gpr.vtx)
db.layersPutVtx(hike.root, lf.vid, lf.vtx)
return ok()
# No grandparent, so ^3 is root vertex # (3)
db.layersPutVtx(hike.root, par.vid, lf.vtx)
# Continue below
of Leaf:
return err((par.vid,DelLeafUnexpected))
else: # (4)
# Replace ^2 by `^2 & vtx` (use `lf` as-is) # `br` is root vertex
db.layersResKey(hike.root, br.vid) # root was changed
db.layersPutVtx(hike.root, br.vid, lf.vtx)
# Continue below
# Clean up stale leaf vertex which has moved to root position
db.disposeOfVtx(hike.root, lf.vid)
ok()
# -------------------------
proc delSubTreeImpl(
db: AristoDbRef; # Database, top layer
root: VertexID; # Root vertex
): Result[void,AristoError] =
## Implementation of *delete* sub-trie.
var
dispose = @[root]
rootVtx = db.getVtxRc(root).valueOr:
if error == GetVtxNotFound:
return ok()
return err(error)
follow = @[rootVtx]
# Collect list of nodes to delete
while 0 < follow.len:
var redo: seq[VertexRef]
for vtx in follow:
for vid in vtx.subVids:
# Exiting here leaves the tree as-is
let vtx = ? db.getVtxRc(vid)
redo.add vtx
dispose.add vid
redo.swap follow
# Mark collected vertices to be deleted
for vid in dispose:
db.disposeOfVtx(root, vid)
ok()
proc deleteImpl(
db: AristoDbRef; # Database, top layer
hike: Hike; # Fully expanded path
): Result[void,(VertexID,AristoError)] =
## Implementation of *delete* functionality.
# Remove leaf entry
let lf = hike.legs[^1].wp
if lf.vtx.vType != Leaf:
return err((lf.vid,DelLeafExpexted))
if lf.vid in db.pPrf:
return err((lf.vid, DelLeafLocked))
db.disposeOfVtx(hike.root, lf.vid)
if 1 < hike.legs.len:
# Get current `Branch` vertex `br`
let br = block:
var wp = hike.legs[^2].wp
wp.vtx = wp.vtx.dup # make sure that layers are not impliciteley modified
wp
if br.vtx.vType != Branch:
return err((br.vid,DelBranchExpexted))
# Unlink child vertex from structural table
br.vtx.bVid[hike.legs[^2].nibble] = VertexID(0)
db.layersPutVtx(hike.root, br.vid, br.vtx)
# Clear all Merkle hash keys up to the root key
for n in 0 .. hike.legs.len - 2:
let vid = hike.legs[n].wp.vid
if vid in db.top.final.pPrf:
return err((vid, DelBranchLocked))
db.layersResKey(hike.root, vid)
let nibble = block:
let rc = br.vtx.branchStillNeeded()
if rc.isErr:
return err((br.vid,DelBranchWithoutRefs))
rc.value
# Convert to `Extension` or `Leaf` vertex
if 0 <= nibble:
# Get child vertex (there must be one after a `Branch` node)
let nxt = block:
let vid = br.vtx.bVid[nibble]
VidVtxPair(vid: vid, vtx: db.getVtx vid)
if not nxt.vtx.isValid:
return err((nxt.vid, DelVidStaleVtx))
# Collapse `Branch` vertex `br` depending on `nxt` vertex type
case nxt.vtx.vType:
of Branch:
? db.collapseBranch(hike, nibble.byte)
of Extension:
? db.collapseExt(hike, nibble.byte, nxt.vtx)
of Leaf:
? db.collapseLeaf(hike, nibble.byte, nxt.vtx)
ok()
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc deleteAccountPayload*(
db: AristoDbRef;
path: openArray[byte];
): Result[void,AristoError] =
## Delete the account leaf entry addressed by the argument `path`. If this
## leaf entry referres to a storage tree, this one will be deleted as well.
##
let
hike = path.initNibbleRange.hikeUp(VertexID(1), db).valueOr:
if error[1] in HikeAcceptableStopsNotFound:
return err(DelPathNotFound)
return err(error[1])
stoID = hike.legs[^1].wp.vtx.lData.account.storageID
# Delete storage tree if present
if stoID.isValid:
? db.delSubTreeImpl stoID
db.deleteImpl(hike).isOkOr:
return err(error[1])
ok()
proc deleteGenericData*(
db: AristoDbRef;
root: VertexID;
path: openArray[byte];
): Result[bool,AristoError] =
## Delete the leaf data entry addressed by the argument `path`. The MPT
## sub-tree the leaf data entry is subsumed under is passed as argument
## `root` which must be greater than `VertexID(1)` and smaller than
## `LEAST_FREE_VID`.
##
## The return value is `true` if the argument `path` deleted was the last
## one and the tree does not exist anymore.
##
# Verify that `root` is neither an accounts tree nor a strorage tree.
if not root.isValid:
return err(DelRootVidMissing)
elif root == VertexID(1):
return err(DelAccRootNotAccepted)
elif LEAST_FREE_VID <= root.distinctBase:
return err(DelStoRootNotAccepted)
let hike = path.initNibbleRange.hikeUp(root, db).valueOr:
if error[1] in HikeAcceptableStopsNotFound:
return err(DelPathNotFound)
return err(error[1])
db.deleteImpl(hike).isOkOr:
return err(error[1])
ok(not db.getVtx(root).isValid)
proc deleteGenericTree*(
db: AristoDbRef; # Database, top layer
root: VertexID; # Root vertex
): Result[void,AristoError] =
## Variant of `deleteGenericData()` for purging the whole MPT sub-tree.
##
# Verify that `root` is neither an accounts tree nor a strorage tree.
if not root.isValid:
return err(DelRootVidMissing)
elif root == VertexID(1):
return err(DelAccRootNotAccepted)
elif LEAST_FREE_VID <= root.distinctBase:
return err(DelStoRootNotAccepted)
db.delSubTreeImpl root
proc deleteStorageData*(
db: AristoDbRef;
path: openArray[byte];
accPath: PathID; # Needed for accounts payload
): Result[bool,AristoError] =
## For a given account argument `accPath`, this function deletes the
## argument `path` from the associated storage tree (if any, at all.) If
## the if the argument `path` deleted was the last one on the storage tree,
## account leaf referred to by `accPath` will be updated so that it will
## not refer to a storage tree anymore. In the latter case only the function
## will return `true`.
##
let
accHike = ? db.retrieveStoAccHike accPath
wpAcc = accHike.legs[^1].wp
stoID = wpAcc.vtx.lData.account.storageID
if not stoID.isValid:
return err(DelStoRootMissing)
let stoHike = path.initNibbleRange.hikeUp(stoID, db).valueOr:
if error[1] in HikeAcceptableStopsNotFound:
return err(DelPathNotFound)
return err(error[1])
# Mark account path for update for `hashify()`
db.updateAccountForHasher accHike
db.deleteImpl(stoHike).isOkOr:
return err(error[1])
# Make sure that an account leaf has no dangling sub-trie
if db.getVtx(stoID).isValid:
return ok(false)
# De-register the deleted storage tree from the account record
let leaf = wpAcc.vtx.dup # Dup on modify
leaf.lData.account.storageID = VertexID(0)
db.layersPutVtx(VertexID(1), wpAcc.vid, leaf)
db.layersResKey(VertexID(1), wpAcc.vid)
ok(true)
proc deleteStorageTree*(
db: AristoDbRef; # Database, top layer
accPath: PathID; # Needed for accounts payload
): Result[void,AristoError] =
## Variant of `deleteStorageData()` for purging the whole storage tree
## associated to the account argument `accPath`.
##
let
accHike = db.retrieveStoAccHike(accPath).valueOr:
if error == UtilsAccInaccessible:
return err(DelStoAccMissing)
return err(error)
wpAcc = accHike.legs[^1].wp
stoID = wpAcc.vtx.lData.account.storageID
if not stoID.isValid:
return err(DelStoRootMissing)
# Mark account path for update for `hashify()`
db.updateAccountForHasher accHike
? db.delSubTreeImpl stoID
# De-register the deleted storage tree from the accounts record
let leaf = wpAcc.vtx.dup # Dup on modify
leaf.lData.account.storageID = VertexID(0)
db.layersPutVtx(VertexID(1), wpAcc.vid, leaf)
db.layersResKey(VertexID(1), wpAcc.vid)
ok()
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------