nimbus-eth1/nimbus/db/kvt/kvt_layers.nim
Jacek Sieka 768307d91d
Cache code and invalid jump destination tables (fixes #2268) (#2404)
It is common for many accounts to share the same code - at the database
level, code is stored by hash meaning only one copy exists per unique
program but when loaded in memory, a copy is made for each account.

Further, every time we execute the code, it must be scanned for invalid
jump destinations which slows down EVM exeuction.

Finally, the extcodesize call causes code to be loaded even if only the
size is needed.

This PR improves on all these points by introducing a shared
CodeBytesRef type whose code section is immutable and that can be shared
between accounts. Further, a dedicated `len` API call is added so that
the EXTCODESIZE opcode can operate without polluting the GC and code
cache, for cases where only the size is requested - rocksdb will in this
case cache the code itself in the row cache meaning that lookup of the
code itself remains fast when length is asked for first.

With 16k code entries, there's a 90% hit rate which goes up to 99%
during the 2.3M attack - the cache significantly lowers memory
consumption and execution time not only during this event but across the
board.
2024-06-21 09:44:10 +02:00

133 lines
4.5 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/[sequtils, sets, tables],
eth/common,
results,
./kvt_desc
# ------------------------------------------------------------------------------
# Public getters/helpers
# ------------------------------------------------------------------------------
func nLayersKeys*(db: KvtDbRef): int =
## Maximum number of ley/value entries on the cache layers. This is an upper
## bound for the number of effective key/value mappings held on the cache
## layers as there might be duplicate entries for the same key on different
## layers.
db.stack.mapIt(it.delta.sTab.len).foldl(a + b, db.top.delta.sTab.len)
# ------------------------------------------------------------------------------
# Public functions: get function
# ------------------------------------------------------------------------------
func layersLen*(db: KvtDbRef; key: openArray[byte]|seq[byte]): Opt[int] =
## Return `true` id the argument key is cached.
##
when key isnot seq[byte]:
let key = @key
db.top.delta.sTab.withValue(key, item):
return Opt.some(item[].len())
for w in db.rstack:
w.delta.sTab.withValue(key, item):
return Opt.some(item[].len())
Opt.none(int)
func layersHasKey*(db: KvtDbRef; key: openArray[byte]|seq[byte]): bool =
## Return `true` id the argument key is cached.
##
db.layersLen(key).isSome()
func layersGet*(db: KvtDbRef; key: openArray[byte]|seq[byte]): Opt[Blob] =
## Find an item on the cache layers. An `ok()` result might contain an
## empty value if it is stored on the cache that way.
##
when key isnot seq[byte]:
let key = @key
db.top.delta.sTab.withValue(key, item):
return Opt.some(item[])
for w in db.rstack:
w.delta.sTab.withValue(key, item):
return Opt.some(item[])
Opt.none(Blob)
# ------------------------------------------------------------------------------
# Public functions: put function
# ------------------------------------------------------------------------------
func layersPut*(db: KvtDbRef; key: openArray[byte]; data: openArray[byte]) =
## Store a (potentally empty) value on the top layer
db.top.delta.sTab[@key] = @data
# ------------------------------------------------------------------------------
# Public functions
# ------------------------------------------------------------------------------
func layersCc*(db: KvtDbRef; level = high(int)): LayerRef =
## Provide a collapsed copy of layers up to a particular transaction level.
## If the `level` argument is too large, the maximum transaction level is
## returned. For the result layer, the `txUid` value set to `0`.
let layers = if db.stack.len <= level: db.stack & @[db.top]
else: db.stack[0 .. level]
# Set up initial layer (bottom layer)
result = LayerRef(delta: LayerDeltaRef(sTab: layers[0].delta.sTab))
# Consecutively merge other layers on top
for n in 1 ..< layers.len:
for (key,val) in layers[n].delta.sTab.pairs:
result.delta.sTab[key] = val
# ------------------------------------------------------------------------------
# Public iterators
# ------------------------------------------------------------------------------
iterator layersWalk*(
db: KvtDbRef;
seen: var HashSet[Blob];
): tuple[key: Blob, data: Blob] =
## Walk over all key-value pairs on the cache layers. Note that
## entries are unsorted.
##
## The argument `seen` collects a set of all visited vertex IDs including
## the one with a zero vertex which are othewise skipped by the iterator.
## The `seen` argument must not be modified while the iterator is active.
##
for (key,val) in db.top.delta.sTab.pairs:
yield (key,val)
seen.incl key
for w in db.rstack:
for (key,val) in w.delta.sTab.pairs:
if key notin seen:
yield (key,val)
seen.incl key
iterator layersWalk*(
db: KvtDbRef;
): tuple[key: Blob, data: Blob] =
## Variant of `layersWalk()`.
var seen: HashSet[Blob]
for (key,val) in db.layersWalk seen:
yield (key,val)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------