storage hike cache (#2484)

This PR adds a storage hike cache similar to the account hike cache
already present - this cache is less efficient because account storage
is already partically cached in the account ledger but nonetheless helps
keep hiking down.

Notably, there's an opportunity to optimise this cache and the others so
that they cooperate better insteado of overlapping, which is left for a
future PR.

This PR also fixes an O(N) memory usage for storage slots where the
delete would keep the full storage in a work list which on mainnet can
grow very large - the work list is replaced with a more conventional
recursive `O(log N)` approach.
This commit is contained in:
Jacek Sieka 2024-07-14 19:12:10 +02:00 committed by GitHub
parent f3a56002ca
commit 9d91191154
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 150 additions and 51 deletions

View File

@ -58,7 +58,7 @@ proc collapseBranch(
db: AristoDbRef; # Database, top layer
hike: Hike; # Fully expanded path
nibble: byte; # Applicable link for `Branch` vertex
): Result[void,(VertexID,AristoError)] =
): Result[void,AristoError] =
## Convert/merge vertices:
## ::
## current | becomes | condition
@ -95,7 +95,7 @@ proc collapseBranch(
xt.vtx.ePfx = par.vtx.ePfx & xt.vtx.ePfx
of Leaf:
return err((par.vid,DelLeafUnexpected))
return err(DelLeafUnexpected)
else: # (3)
# Replace `br` (use `xt` as-is)
@ -110,7 +110,7 @@ proc collapseExt(
hike: Hike; # Fully expanded path
nibble: byte; # Link for `Branch` vertex `^2`
vtx: VertexRef; # Follow up extension vertex (nibble)
): Result[void,(VertexID,AristoError)] =
): Result[void,AristoError] =
## Convert/merge vertices:
## ::
## ^3 ^2 `vtx` | ^3 ^2 |
@ -145,7 +145,7 @@ proc collapseExt(
xt.vtx.ePfx = par.vtx.ePfx & xt.vtx.ePfx
of Leaf:
return err((par.vid,DelLeafUnexpected))
return err(DelLeafUnexpected)
else: # (3)
# Replace ^2 by `^2 & vtx` (use `xt` as-is)
@ -160,7 +160,7 @@ proc collapseLeaf(
hike: Hike; # Fully expanded path
nibble: byte; # Link for `Branch` vertex `^2`
vtx: VertexRef; # Follow up leaf vertex (from nibble)
): Result[void,(VertexID,AristoError)] =
): Result[void,AristoError] =
## Convert/merge vertices:
## ::
## current | becomes | condition
@ -205,7 +205,7 @@ proc collapseLeaf(
# Grandparent exists
let gpr = hike.legs[^4].wp.dup # Writable vertex
if gpr.vtx.vType != Branch:
return err((gpr.vid,DelBranchExpexted))
return err(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)
@ -217,7 +217,7 @@ proc collapseLeaf(
# Continue below
of Leaf:
return err((par.vid,DelLeafUnexpected))
return err(DelLeafUnexpected)
else: # (4)
# Replace ^2 by `^2 & vtx` (use `lf` as-is) # `br` is root vertex
@ -261,17 +261,47 @@ proc delSubTreeImpl(
ok()
proc delStoTreeImpl(
db: AristoDbRef; # Database, top layer
rvid: RootedVertexID; # Root vertex
accPath: Hash256;
stoPath: NibblesBuf;
): Result[void,AristoError] =
## Implementation of *delete* sub-trie.
let vtx = db.getVtxRc(rvid).valueOr:
if error == GetVtxNotFound:
return ok()
return err(error)
case vtx.vType
of Branch:
for i in 0..15:
if vtx.bVid[i].isValid:
? db.delStoTreeImpl(
(rvid.root, vtx.bVid[i]), accPath,
stoPath & NibblesBuf.nibble(byte i))
of Extension:
?db.delStoTreeImpl((rvid.root, vtx.eVid), accPath, stoPath & vtx.ePfx)
of Leaf:
let stoPath = Hash256(data: (stoPath & vtx.lPfx).getBytes())
db.layersPutStoLeaf(AccountKey.mixUp(accPath, stoPath), nil)
db.disposeOfVtx(rvid)
ok()
proc deleteImpl(
db: AristoDbRef; # Database, top layer
hike: Hike; # Fully expanded path
): Result[void,(VertexID,AristoError)] =
): Result[void,AristoError] =
## Implementation of *delete* functionality.
# Remove leaf entry
let lf = hike.legs[^1].wp
if lf.vtx.vType != Leaf:
return err((lf.vid,DelLeafExpexted))
return err(DelLeafExpexted)
db.disposeOfVtx((hike.root, lf.vid))
@ -282,7 +312,7 @@ proc deleteImpl(
wp.vtx = wp.vtx.dup # make sure that layers are not impliciteley modified
wp
if br.vtx.vType != Branch:
return err((br.vid,DelBranchExpexted))
return err(DelBranchExpexted)
# Unlink child vertex from structural table
br.vtx.bVid[hike.legs[^2].nibble] = VertexID(0)
@ -296,7 +326,7 @@ proc deleteImpl(
let nibble = block:
let rc = br.vtx.branchStillNeeded()
if rc.isErr:
return err((br.vid,DelBranchWithoutRefs))
return err(DelBranchWithoutRefs)
rc.value
# Convert to `Extension` or `Leaf` vertex
@ -306,7 +336,7 @@ proc deleteImpl(
let vid = br.vtx.bVid[nibble]
VidVtxPair(vid: vid, vtx: db.getVtx (hike.root, vid))
if not nxt.vtx.isValid:
return err((nxt.vid, DelVidStaleVtx))
return err(DelVidStaleVtx)
# Collapse `Branch` vertex `br` depending on `nxt` vertex type
case nxt.vtx.vType:
@ -339,10 +369,9 @@ proc deleteAccountRecord*(
# Delete storage tree if present
if stoID.isValid:
? db.delSubTreeImpl stoID
? db.delStoTreeImpl((stoID, stoID), accPath, NibblesBuf())
db.deleteImpl(hike).isOkOr:
return err(error[1])
?db.deleteImpl(hike)
db.layersPutAccLeaf(accPath, nil)
@ -375,8 +404,7 @@ proc deleteGenericData*(
return err(DelPathNotFound)
return err(error[1])
db.deleteImpl(hike).isOkOr:
return err(error[1])
?db.deleteImpl(hike)
ok(not db.getVtx((root, root)).isValid)
@ -428,8 +456,9 @@ proc deleteStorageData*(
# Mark account path Merkle keys for update
db.updateAccountForHasher accHike
db.deleteImpl(stoHike).isOkOr:
return err(error[1])
?db.deleteImpl(stoHike)
db.layersPutStoLeaf(AccountKey.mixUp(accPath, stoPath), nil)
# Make sure that an account leaf has no dangling sub-trie
if db.getVtx((stoID, stoID)).isValid:
@ -440,7 +469,6 @@ proc deleteStorageData*(
leaf.lData.stoID = VertexID(0)
db.layersPutAccLeaf(accPath, leaf)
db.layersPutVtx((accHike.root, wpAcc.vid), leaf)
db.layersResKey((accHike.root, wpAcc.vid))
ok(true)
proc deleteStorageTree*(
@ -464,14 +492,13 @@ proc deleteStorageTree*(
# Mark account path Merkle keys for update
db.updateAccountForHasher accHike
? db.delSubTreeImpl stoID
? db.delStoTreeImpl((stoID, stoID), accPath, NibblesBuf())
# De-register the deleted storage tree from the accounts record
let leaf = wpAcc.vtx.dup # Dup on modify
leaf.lData.stoID = VertexID(0)
db.layersPutAccLeaf(accPath, leaf)
db.layersPutVtx((accHike.root, wpAcc.vid), leaf)
db.layersResKey((accHike.root, wpAcc.vid))
ok()
# ------------------------------------------------------------------------------

View File

@ -83,10 +83,15 @@ proc deltaPersistent*(
? be.putEndFn writeBatch # Finalise write batch
# Copy back updated payloads
for accPath, pyl in db.balancer.accLeaves:
for accPath, vtx in db.balancer.accLeaves:
let accKey = accPath.to(AccountKey)
if not db.accLeaves.lruUpdate(accKey, pyl):
discard db.accLeaves.lruAppend(accKey, pyl, accLruSize)
if not db.accLeaves.lruUpdate(accKey, vtx):
discard db.accLeaves.lruAppend(accKey, vtx, accLruSize)
for mixPath, vtx in db.balancer.stoLeaves:
let mixKey = mixPath.to(AccountKey)
if not db.stoLeaves.lruUpdate(mixKey, vtx):
discard db.stoLeaves.lruAppend(mixKey, vtx, accLruSize)
# Update dudes and this descriptor
? updateSiblings.update().commit()

View File

@ -23,7 +23,7 @@
import
std/[hashes, sets, tables],
stew/keyed_queue,
stew/[assign2, keyed_queue],
eth/common,
results,
./aristo_constants,
@ -92,6 +92,10 @@ type
## TODO a better solution would probably be to cache this in a type
## exposed to the high-level API
stoLeaves*: KeyedQueue[AccountKey, VertexRef]
## Mixed account/storage path to payload cache - same as above but caches
## the full lookup of storage slots
AristoDbAction* = proc(db: AristoDbRef) {.gcsafe, raises: [].}
## Generic call back function/closure.
@ -110,6 +114,18 @@ template `==`*(a, b: AccountKey): bool =
template to*(a: Hash256, T: type AccountKey): T =
AccountKey((ref Hash256)(data: a.data))
template mixUp*(T: type AccountKey, accPath, stoPath: Hash256): Hash256 =
# Insecure but fast way of mixing the values of two hashes, for the purpose
# of quick lookups - this is certainly not a good idea for general Hash256
# values but account paths are generated from accounts which would be hard
# to create pre-images for, for the purpose of collisions with a particular
# storage slot
var v {.noinit.}: Hash256
for i in 0..<v.data.len:
# `+` wraps leaving all bits used
v.data[i] = accPath.data[i] + stoPath.data[i]
v
func getOrVoid*[W](tab: Table[W,VertexRef]; w: W): VertexRef =
tab.getOrDefault(w, VertexRef(nil))

View File

@ -115,7 +115,8 @@ type
kMap*: Table[RootedVertexID,HashKey] ## Merkle hash key mapping
vTop*: VertexID ## Last used vertex ID
accLeaves*: Table[Hash256, VertexRef] ## Account path -> VertexRef
accLeaves*: Table[Hash256, VertexRef] ## Account path -> VertexRef
stoLeaves*: Table[Hash256, VertexRef] ## Storage path -> VertexRef
LayerRef* = ref LayerObj
LayerObj* = object

View File

@ -59,27 +59,26 @@ proc retrieveAccountPayload(
db: AristoDbRef;
accPath: Hash256;
): Result[LeafPayload,AristoError] =
if (let pyl = db.layersGetAccLeaf(accPath); pyl.isSome()):
if not pyl[].isValid():
if (let leafVtx = db.layersGetAccLeaf(accPath); leafVtx.isSome()):
if not leafVtx[].isValid():
return err(FetchPathNotFound)
return ok pyl[].lData
return ok leafVtx[].lData
let accKey = accPath.to(AccountKey)
if (let pyl = db.accLeaves.lruFetch(accKey); pyl.isSome()):
if not pyl[].isValid():
if (let leafVtx = db.accLeaves.lruFetch(accKey); leafVtx.isSome()):
if not leafVtx[].isValid():
return err(FetchPathNotFound)
return ok pyl[].lData
return ok leafVtx[].lData
# Updated payloads are stored in the layers so if we didn't find them there,
# it must have been in the database
let
payload = db.retrieveLeaf(VertexID(1), accPath.data).valueOr:
leafVtx = db.retrieveLeaf(VertexID(1), accPath.data).valueOr:
if error == FetchAccInaccessible:
discard db.accLeaves.lruAppend(accKey, nil, accLruSize)
return err(FetchPathNotFound)
return err(error)
ok db.accLeaves.lruAppend(accKey, payload, accLruSize).lData
ok db.accLeaves.lruAppend(accKey, leafVtx, accLruSize).lData
proc retrieveMerkleHash(
db: AristoDbRef;
@ -157,11 +156,7 @@ proc fetchStorageID*(
## Public helper function for retrieving a storage (vertex) ID for a
## given account.
let
payload = db.retrieveAccountPayload(accPath).valueOr:
if error == FetchAccInaccessible:
return err(FetchPathNotFound)
return err(error)
payload = ?db.retrieveAccountPayload(accPath)
stoID = payload.stoID
if not stoID.isValid:
@ -169,6 +164,42 @@ proc fetchStorageID*(
ok stoID
proc retrieveStoragePayload(
db: AristoDbRef;
accPath: Hash256;
stoPath: Hash256;
): Result[UInt256,AristoError] =
let mixPath = AccountKey.mixUp(accPath, stoPath)
if (let leafVtx = db.layersGetStoLeaf(mixPath); leafVtx.isSome()):
if not leafVtx[].isValid():
return err(FetchPathNotFound)
return ok leafVtx[].lData.stoData
let mixKey = mixPath.to(AccountKey)
if (let leafVtx = db.stoLeaves.lruFetch(mixKey); leafVtx.isSome()):
if not leafVtx[].isValid():
return err(FetchPathNotFound)
return ok leafVtx[].lData.stoData
# Updated payloads are stored in the layers so if we didn't find them there,
# it must have been in the database
let
leafVtx = db.retrieveLeaf(? db.fetchStorageID(accPath), stoPath.data).valueOr:
return err(error)
ok db.stoLeaves.lruAppend(mixKey, leafVtx, accLruSize).lData.stoData
proc hasStoragePayload(
db: AristoDbRef;
accPath: Hash256;
stoPath: Hash256;
): Result[bool,AristoError] =
let error = db.retrieveStoragePayload(accPath, stoPath).errorOr:
return ok(true)
if error == FetchPathNotFound:
return ok(false)
err(error)
# ------------------------------------------------------------------------------
# Public functions
@ -249,9 +280,9 @@ proc fetchStorageData*(
## For a storage tree related to account `accPath`, fetch the data record
## from the database indexed by `path`.
##
let pyl = ? db.retrieveLeaf(? db.fetchStorageID accPath, stoPath.data)
assert pyl.lData.pType == StoData # debugging only
ok pyl.lData.stoData
let leafVtx = ? db.retrieveLeaf(? db.fetchStorageID accPath, stoPath.data)
assert leafVtx.lData.pType == StoData # debugging only
ok leafVtx.lData.stoData
proc fetchStorageState*(
db: AristoDbRef;
@ -273,7 +304,7 @@ proc hasPathStorage*(
## For a storage tree related to account `accPath`, query whether the data
## record indexed by `path` exists on the database.
##
db.hasPayload(? db.fetchStorageID accPath, stoPath.data)
db.hasStoragePayload(accPath, stoPath)
proc hasStorageData*(
db: AristoDbRef;

View File

@ -101,6 +101,15 @@ func layersGetAccLeaf*(db: AristoDbRef; accPath: Hash256): Opt[VertexRef] =
Opt.none(VertexRef)
func layersGetStoLeaf*(db: AristoDbRef; mixPath: Hash256): Opt[VertexRef] =
db.top.delta.stoLeaves.withValue(mixPath, item):
return Opt.some(item[])
for w in db.rstack:
w.delta.stoLeaves.withValue(mixPath, item):
return Opt.some(item[])
Opt.none(VertexRef)
# ------------------------------------------------------------------------------
# Public functions: setter variants
@ -147,8 +156,11 @@ proc layersUpdateVtx*(
db.layersResKey(rvid)
func layersPutAccLeaf*(db: AristoDbRef; accPath: Hash256; pyl: VertexRef) =
db.top.delta.accLeaves[accPath] = pyl
func layersPutAccLeaf*(db: AristoDbRef; accPath: Hash256; leafVtx: VertexRef) =
db.top.delta.accLeaves[accPath] = leafVtx
func layersPutStoLeaf*(db: AristoDbRef; mixPath: Hash256; leafVtx: VertexRef) =
db.top.delta.stoLeaves[mixPath] = leafVtx
# ------------------------------------------------------------------------------
# Public functions
@ -165,8 +177,10 @@ func layersMergeOnto*(src: LayerRef; trg: var LayerObj) =
for (vid,key) in src.delta.kMap.pairs:
trg.delta.kMap[vid] = key
trg.delta.vTop = src.delta.vTop
for (accPath,pyl) in src.delta.accLeaves.pairs:
trg.delta.accLeaves[accPath] = pyl
for (accPath,leafVtx) in src.delta.accLeaves.pairs:
trg.delta.accLeaves[accPath] = leafVtx
for (mixPath,leafVtx) in src.delta.stoLeaves.pairs:
trg.delta.stoLeaves[mixPath] = leafVtx
func layersCc*(db: AristoDbRef; level = high(int)): LayerRef =
## Provide a collapsed copy of layers up to a particular transaction level.
@ -183,6 +197,7 @@ func layersCc*(db: AristoDbRef; level = high(int)): LayerRef =
kMap: layers[0].delta.kMap,
vTop: layers[^1].delta.vTop,
accLeaves: layers[0].delta.accLeaves,
stoLeaves: layers[0].delta.stoLeaves,
))
# Consecutively merge other layers on top
@ -191,8 +206,10 @@ func layersCc*(db: AristoDbRef; level = high(int)): LayerRef =
result.delta.sTab[vid] = vtx
for (vid,key) in layers[n].delta.kMap.pairs:
result.delta.kMap[vid] = key
for (accPath,pyl) in layers[n].delta.accLeaves.pairs:
result.delta.accLeaves[accPath] = pyl
for (accPath,vtx) in layers[n].delta.accLeaves.pairs:
result.delta.accLeaves[accPath] = vtx
for (mixPath,vtx) in layers[n].delta.stoLeaves.pairs:
result.delta.stoLeaves[mixPath] = vtx
# ------------------------------------------------------------------------------
# Public iterators

View File

@ -137,6 +137,8 @@ proc mergeStorageData*(
# Mark account path Merkle keys for update
resetKeys()
db.layersPutStoLeaf(AccountKey.mixUp(accPath, stoPath), rc.value)
if not stoID.isValid:
# Make sure that there is an account that refers to that storage trie
let leaf = vtx.dup # Dup on modify