nimbus-eth1/nimbus/db/aristo/aristo_merge/merge_payload_helper.nim
Jacek Sieka 81e75622cf
storage: store root id together with vid, for better locality of refe… (#2449)
The state and account MPT:s currenty share key space in the database
based on that vertex id:s are assigned essentially randomly, which means
that when two adjacent slot values from the same contract are accessed,
they might reside at large distance from each other.

Here, we prefix each vertex id by its root causing them to be sorted
together thus bringing all data belonging to a particular contract
closer together - the same effect also happens for the main state MPT
whose nodes now end up clustered together more tightly.

In the future, the prefix given to the storage keys can also be used to
perform range operations such as reading all the storage at once and/or
deleting an account with a batch operation.

Notably, parts of the API already supported this rooting concept while
parts didn't - this PR makes the API consistent by always working with a
root+vid.
2024-07-04 15:46:52 +02:00

455 lines
14 KiB
Nim

# 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.
{.push raises: [].}
import
std/typetraits,
eth/common,
results,
".."/[aristo_desc, aristo_get, aristo_hike, aristo_layers, aristo_vid]
# ------------------------------------------------------------------------------
# Private getters & setters
# ------------------------------------------------------------------------------
proc xPfx(vtx: VertexRef): NibblesBuf =
case vtx.vType:
of Leaf:
return vtx.lPfx
of Extension:
return vtx.ePfx
of Branch:
doAssert vtx.vType != Branch # Ooops
# ------------------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------------------
proc clearMerkleKeys(
db: AristoDbRef; # Database, top layer
hike: Hike; # Implied vertex IDs to clear hashes for
vid: VertexID; # Additional vertex IDs to clear
) =
for w in hike.legs:
db.layersResKey((hike.root, w.wp.vid))
db.layersResKey((hike.root, vid))
# -----------
proc insertBranch(
db: AristoDbRef; # Database, top layer
hike: Hike; # Current state
linkID: VertexID; # Vertex ID to insert
linkVtx: VertexRef; # Vertex to insert
payload: PayloadRef; # Leaf data payload
): Result[Hike,AristoError] =
##
## Insert `Extension->Branch` vertex chain or just a `Branch` vertex
##
## ... --(linkID)--> <linkVtx>
##
## <-- immutable --> <---- mutable ----> ..
##
## will become either
##
## --(linkID)-->
## <extVtx> --(local1)-->
## <forkVtx>[linkInx] --(local2)--> <linkVtx*>
## [leafInx] --(local3)--> <leafVtx>
##
## or in case that there is no common prefix
##
## --(linkID)-->
## <forkVtx>[linkInx] --(local2)--> <linkVtx*>
## [leafInx] --(local3)--> <leafVtx>
##
## *) vertex was slightly modified or removed if obsolete `Extension`
##
let n = linkVtx.xPfx.sharedPrefixLen hike.tail
# Verify minimum requirements
if hike.tail.len == n:
# Should have been tackeld by `hikeUp()`, already
return err(MergeLeafGarbledHike)
if linkVtx.xPfx.len == n:
return err(MergeBranchLinkVtxPfxTooShort)
# Provide and install `forkVtx`
let
forkVtx = VertexRef(vType: Branch)
linkInx = linkVtx.xPfx[n]
leafInx = hike.tail[n]
var
leafLeg = Leg(nibble: -1)
# Install `forkVtx`
block:
# Clear Merkle hashes (aka hash keys) unless proof mode.
db.clearMerkleKeys(hike, linkID)
if linkVtx.vType == Leaf:
# Double check path prefix
if 64 < hike.legsTo(NibblesBuf).len + linkVtx.lPfx.len:
return err(MergeBranchLinkLeafGarbled)
let
local = db.vidFetch(pristine = true)
linkDup = linkVtx.dup
db.layersUpdateVtx((hike.root, local), linkDup)
linkDup.lPfx = linkDup.lPfx.slice(1+n)
forkVtx.bVid[linkInx] = local
elif linkVtx.ePfx.len == n + 1:
# This extension `linkVtx` becomes obsolete
forkVtx.bVid[linkInx] = linkVtx.eVid
else:
let
local = db.vidFetch
linkDup = linkVtx.dup
db.layersUpdateVtx((hike.root, local), linkDup)
linkDup.ePfx = linkDup.ePfx.slice(1+n)
forkVtx.bVid[linkInx] = local
block:
let local = db.vidFetch(pristine = true)
forkVtx.bVid[leafInx] = local
leafLeg.wp.vid = local
leafLeg.wp.vtx = VertexRef(
vType: Leaf,
lPfx: hike.tail.slice(1+n),
lData: payload)
db.layersUpdateVtx((hike.root, local), leafLeg.wp.vtx)
# Update branch leg, ready to append more legs
var okHike = Hike(root: hike.root, legs: hike.legs)
# Update in-beween glue linking `branch --[..]--> forkVtx`
if 0 < n:
let extVtx = VertexRef(
vType: Extension,
ePfx: hike.tail.slice(0,n),
eVid: db.vidFetch)
db.layersUpdateVtx((hike.root, linkID), extVtx)
okHike.legs.add Leg(
nibble: -1,
wp: VidVtxPair(
vid: linkID,
vtx: extVtx))
db.layersUpdateVtx((hike.root, extVtx.eVid), forkVtx)
okHike.legs.add Leg(
nibble: leafInx.int8,
wp: VidVtxPair(
vid: extVtx.eVid,
vtx: forkVtx))
else:
db.layersUpdateVtx((hike.root, linkID), forkVtx)
okHike.legs.add Leg(
nibble: leafInx.int8,
wp: VidVtxPair(
vid: linkID,
vtx: forkVtx))
okHike.legs.add leafLeg
ok okHike
proc concatBranchAndLeaf(
db: AristoDbRef; # Database, top layer
hike: Hike; # Path top has a `Branch` vertex
brVid: VertexID; # Branch vertex ID from from `Hike` top
brVtx: VertexRef; # Branch vertex, linked to from `Hike`
payload: PayloadRef; # Leaf data payload
): Result[Hike,AristoError] =
## Append argument branch vertex passed as argument `(brID,brVtx)` and then
## a `Leaf` vertex derived from the argument `payload`.
##
if hike.tail.len == 0:
return err(MergeBranchGarbledTail)
let nibble = hike.tail[0].int8
if brVtx.bVid[nibble].isValid:
return err(MergeRootBranchLinkBusy)
# Clear Merkle hashes (aka hash keys) unless proof mode.
db.clearMerkleKeys(hike, brVid)
# Append branch vertex
var okHike = Hike(root: hike.root, legs: hike.legs)
okHike.legs.add Leg(wp: VidVtxPair(vtx: brVtx, vid: brVid), nibble: nibble)
# Append leaf vertex
let
brDup = brVtx.dup
vid = db.vidFetch(pristine = true)
vtx = VertexRef(
vType: Leaf,
lPfx: hike.tail.slice(1),
lData: payload)
brDup.bVid[nibble] = vid
db.layersUpdateVtx((hike.root, brVid), brDup)
db.layersUpdateVtx((hike.root, vid), vtx)
okHike.legs.add Leg(wp: VidVtxPair(vtx: vtx, vid: vid), nibble: -1)
ok okHike
# ------------------------------------------------------------------------------
# Private functions: add Particia Trie leaf vertex
# ------------------------------------------------------------------------------
proc mergePayloadTopIsBranchAddLeaf(
db: AristoDbRef; # Database, top layer
hike: Hike; # Path top has a `Branch` vertex
payload: PayloadRef; # Leaf data payload
): Result[Hike,AristoError] =
## Append a `Leaf` vertex derived from the argument `payload` after the top
## leg of the `hike` argument which is assumend to refert to a `Branch`
## vertex. If successful, the function returns the updated `hike` trail.
if hike.tail.len == 0:
return err(MergeBranchGarbledTail)
let nibble = hike.legs[^1].nibble
if nibble < 0:
return err(MergeBranchGarbledNibble)
let
parent = hike.legs[^1].wp.vid
branch = hike.legs[^1].wp.vtx
linkID = branch.bVid[nibble]
linkVtx = db.getVtx (hike.root, linkID)
if not linkVtx.isValid:
#
# .. <branch>[nibble] --(linkID)--> nil
#
# <-------- immutable ------------> <---- mutable ----> ..
#
# Not much else that can be done here
raiseAssert "Dangling edge:" &
" pfx=" & $hike.legsTo(hike.legs.len-1,NibblesBuf) &
" branch=" & $parent &
" nibble=" & $nibble &
" edge=" & $linkID &
" tail=" & $hike.tail
if linkVtx.vType == Branch:
# Slot link to a branch vertex should be handled by `hikeUp()`
#
# .. <branch>[nibble] --(linkID)--> <linkVtx>[]
#
# <-------- immutable ------------> <---- mutable ----> ..
#
return db.concatBranchAndLeaf(hike, linkID, linkVtx, payload)
db.insertBranch(hike, linkID, linkVtx, payload)
proc mergePayloadTopIsExtAddLeaf(
db: AristoDbRef; # Database, top layer
hike: Hike; # Path top has an `Extension` vertex
payload: PayloadRef; # Leaf data payload
): Result[Hike,AristoError] =
## Append a `Leaf` vertex derived from the argument `payload` after the top
## leg of the `hike` argument which is assumend to refert to a `Extension`
## vertex. If successful, the function returns the
## updated `hike` trail.
let
extVtx = hike.legs[^1].wp.vtx
extVid = hike.legs[^1].wp.vid
brVid = extVtx.eVid
brVtx = db.getVtx (hike.root, brVid)
var okHike = Hike(root: hike.root, legs: hike.legs)
if not brVtx.isValid:
# Blind vertex, promote to leaf vertex.
#
# --(extVid)--> <extVtx> --(brVid)--> nil
#
# <-------- immutable -------------->
#
let vtx = VertexRef(
vType: Leaf,
lPfx: extVtx.ePfx & hike.tail,
lData: payload)
db.layersUpdateVtx((hike.root, extVid), vtx)
okHike.legs[^1].wp.vtx = vtx
elif brVtx.vType != Branch:
return err(MergeBranchRootExpected)
else:
let
nibble = hike.tail[0].int8
linkID = brVtx.bVid[nibble]
#
# Required
#
# --(extVid)--> <extVtx> --(brVid)--> <brVtx>[nibble] --(linkID)--> nil
#
# <-------- immutable --------------> <-------- mutable ----------> ..
#
if linkID.isValid:
return err(MergeRootBranchLinkBusy)
# Clear Merkle hashes (aka hash keys) unless proof mode
db.clearMerkleKeys(hike, brVid)
let
brDup = brVtx.dup
vid = db.vidFetch(pristine = true)
vtx = VertexRef(
vType: Leaf,
lPfx: hike.tail.slice(1),
lData: payload)
brDup.bVid[nibble] = vid
db.layersUpdateVtx((hike.root, brVid), brDup)
db.layersUpdateVtx((hike.root, vid), vtx)
okHike.legs.add Leg(wp: VidVtxPair(vtx: brDup, vid: brVid), nibble: nibble)
okHike.legs.add Leg(wp: VidVtxPair(vtx: vtx, vid: vid), nibble: -1)
ok okHike
proc mergePayloadTopIsEmptyAddLeaf(
db: AristoDbRef; # Database, top layer
hike: Hike; # No path legs
rootVtx: VertexRef; # Root vertex
payload: PayloadRef; # Leaf data payload
): Result[Hike,AristoError] =
## Append a `Leaf` vertex derived from the argument `payload` after the
## argument vertex `rootVtx` and append both the empty arguent `hike`.
if rootVtx.vType == Branch:
let nibble = hike.tail[0].int8
if rootVtx.bVid[nibble].isValid:
return err(MergeRootBranchLinkBusy)
# Clear Merkle hashes (aka hash keys) unless proof mode
db.clearMerkleKeys(hike, hike.root)
let
rootDup = rootVtx.dup
leafVid = db.vidFetch(pristine = true)
leafVtx = VertexRef(
vType: Leaf,
lPfx: hike.tail.slice(1),
lData: payload)
rootDup.bVid[nibble] = leafVid
db.layersUpdateVtx((hike.root, hike.root), rootDup)
db.layersUpdateVtx((hike.root, leafVid), leafVtx)
return ok Hike(
root: hike.root,
legs: @[Leg(wp: VidVtxPair(vtx: rootDup, vid: hike.root), nibble: nibble),
Leg(wp: VidVtxPair(vtx: leafVtx, vid: leafVid), nibble: -1)])
db.insertBranch(hike, hike.root, rootVtx, payload)
proc mergePayloadUpdate(
db: AristoDbRef; # Database, top layer
hike: Hike; # Path to payload
payload: PayloadRef; # Payload value to add
): Result[Hike,AristoError] =
## Update leaf vertex if payloads differ
let leafLeg = hike.legs[^1]
# Update payloads if they differ
if leafLeg.wp.vtx.lData != payload:
let vid = leafLeg.wp.vid
# Update accounts storage root which is handled implicitly
if hike.root == VertexID(1):
payload.stoID = leafLeg.wp.vtx.lData.stoID
# Update vertex and hike
let vtx = VertexRef(
vType: Leaf,
lPfx: leafLeg.wp.vtx.lPfx,
lData: payload)
var hike = hike
hike.legs[^1].wp.vtx = vtx
# Modify top level cache
db.layersUpdateVtx((hike.root, vid), vtx)
db.clearMerkleKeys(hike, vid)
ok hike
elif db.layersGetVtx((hike.root, leafLeg.wp.vid)).isErr:
err(MergeLeafPathOnBackendAlready)
else:
err(MergeLeafPathCachedAlready)
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
proc mergePayloadImpl*(
db: AristoDbRef; # Database, top layer
root: VertexID; # MPT state root
path: openArray[byte]; # Leaf item to add to the database
payload: PayloadRef; # Payload value
): Result[void,AristoError] =
## Merge the argument `(root,path)` key-value-pair into the top level vertex
## table of the database `db`. The `path` argument is used to address the
## leaf vertex with the payload. It is stored or updated on the database
## accordingly.
##
let
nibblesPath = NibblesBuf.fromBytes(path)
hike = nibblesPath.hikeUp(root, db).to(Hike)
var okHike: Hike
if 0 < hike.legs.len:
case hike.legs[^1].wp.vtx.vType:
of Branch:
okHike = ? db.mergePayloadTopIsBranchAddLeaf(hike, payload)
of Leaf:
if 0 < hike.tail.len: # `Leaf` vertex problem?
return err(MergeLeafGarbledHike)
okHike = ? db.mergePayloadUpdate(hike, payload)
of Extension:
okHike = ? db.mergePayloadTopIsExtAddLeaf(hike, payload)
else:
# Empty hike
let rootVtx = db.getVtx (hike.root, hike.root)
if rootVtx.isValid:
okHike = ? db.mergePayloadTopIsEmptyAddLeaf(hike,rootVtx, payload)
else:
# Bootstrap for existing root ID
let wp = VidVtxPair(
vid: hike.root,
vtx: VertexRef(
vType: Leaf,
lPfx: nibblesPath,
lData: payload))
db.layersUpdateVtx((hike.root, wp.vid), wp.vtx)
okHike = Hike(root: wp.vid, legs: @[Leg(wp: wp, nibble: -1)])
# Double check the result (may be removed in future)
if okHike.to(NibblesBuf) != nibblesPath:
return err(MergeAssemblyFailed) # Ooops
ok()
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------