mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-02-05 00:36:45 +00:00
56d5c382d7
* Misc fixes detail: * Fix de-serialisation for account leafs * Update node recovery from unit tests * Remove `LegacyAccount` from `PayloadRef` object why: Legacy accounts use a hash key as storage root which is detrimental to the working of the Aristo database which uses a vertex ID. * Dissolve `hashify_helper` into `aristo_utils` and `aristo_transcode` why: Functions are of general interest so they should live in first level code files. * Added left/right iterators over leaf nodes * Some helper/wrapper functions that might be useful
442 lines
15 KiB
Nim
442 lines
15 KiB
Nim
# 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.
|
|
|
|
## Aristo DB -- Transaction interface
|
|
## ==================================
|
|
##
|
|
{.push raises: [].}
|
|
|
|
import
|
|
std/[sets, tables],
|
|
chronicles,
|
|
eth/common,
|
|
stew/results,
|
|
"."/[aristo_delete, aristo_desc, aristo_get, aristo_hashify,
|
|
aristo_hike, aristo_init, aristo_layer, aristo_merge, aristo_nearby]
|
|
|
|
logScope:
|
|
topics = "aristo-tx"
|
|
|
|
type
|
|
AristoTxRef* = ref object
|
|
## This descriptor replaces the `AristoDbRef` one for transaction based
|
|
## database operations and management.
|
|
parent: AristoTxRef ## Parent transaction (if any)
|
|
db: AristoDbRef ## Database access
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public functions: Constructor/destructor
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc to*(
|
|
db: AristoDbRef; # `init()` result
|
|
T: type AristoTxRef; # Type discriminator
|
|
): T =
|
|
## Embed the database descritor `db` into the transaction based one. After
|
|
## this operation, the argument descriptor should not be used anymore.
|
|
##
|
|
## The function will return a new transaction descriptor unless the stack of
|
|
## the argument `db` is already filled (e.g. using `push()` on the `db`.)
|
|
if db.stack.len == 0:
|
|
return AristoTxRef(db: db)
|
|
|
|
proc to*(
|
|
rc: Result[AristoDbRef,AristoError]; # `init()` result
|
|
T: type AristoTxRef; # Type discriminator
|
|
): Result[T, AristoError] =
|
|
## Variant of `to()` which passes on any constructor errors.
|
|
##
|
|
## Example:
|
|
## ::
|
|
## let rc = AristoDbRef.init(BackendRocksDB,"/var/tmp/rdb").to(AristoTxRef)
|
|
## ...
|
|
## let tdb = rc.value
|
|
## ...
|
|
##
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
let tdb = rc.value.to(AristoTxRef)
|
|
if tdb.isNil:
|
|
return err(TxDbStackNonEmpty)
|
|
ok tdb
|
|
|
|
proc done*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
flush = false; # Delete persistent data (if supported)
|
|
): Result[void,AristoError]
|
|
{.discardable.} =
|
|
## Database and transaction handle destructor. The `flush` argument is passed
|
|
## on to the database backend destructor. When used in the `BackendRocksDB`
|
|
## database, a `true` value for `flush` will wipe the entire database from
|
|
## the hard disc.
|
|
##
|
|
## Note that the function argument `tdb` must not have any pending open
|
|
## transaction layers, i.e. `tdb.isBase()` must return `true`.
|
|
if not tdb.parent.isNil or tdb.db.isNil:
|
|
return err(TxBaseHandleExpected)
|
|
tdb.db.finish flush
|
|
ok()
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public functions: Classifiers
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc isBase*(tdb: AristoTxRef): bool =
|
|
## The function returns `true` if the argument handle `tdb` is the one that
|
|
## was returned from the `to()` constructor. A handle where this function
|
|
## returns `true` is called a *base level* handle.
|
|
##
|
|
## A *base level* handle may be a valid argument for the `begin()` function
|
|
## but not for either `commit()` ot `rollback()`.
|
|
tdb.parent.isNil and not tdb.db.isNil
|
|
|
|
proc isTop*(tdb: AristoTxRef): bool =
|
|
## If the function returns `true` for the argument handle `tdb`, then this
|
|
## handle can be used on any of the following functions.
|
|
not tdb.parent.isNil and not tdb.db.isNil
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public functions: Transaction frame
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc begin*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
): Result[AristoTxRef,(VertexID,AristoError)] =
|
|
## Starts a new transaction. If successful, the function will return a new
|
|
## handle (or descriptor) which replaces the argument handle `tdb`. This
|
|
## argument handle `tdb` is rendered invalid for as long as the new
|
|
## transaction handle is valid. While valid, this new handle is called a
|
|
## *top level* handle.
|
|
##
|
|
## If the argument `tdb` is a *base level* or a *top level* handle, this
|
|
## function succeeds. Otherwise it will return the error
|
|
## `TxValidHandleExpected`.
|
|
##
|
|
## Example:
|
|
## ::
|
|
## proc doSomething(tdb: AristoTxRef) =
|
|
## let tx = tdb.begin.value # will crash on failure
|
|
## defer: tx.rollback()
|
|
## ...
|
|
## tx.commit()
|
|
##
|
|
if tdb.db.isNil:
|
|
return err((VertexID(0),TxValidHandleExpected))
|
|
|
|
tdb.db.push()
|
|
|
|
let pTx = AristoTxRef(parent: tdb, db: tdb.db)
|
|
tdb.db = AristoDbRef(nil)
|
|
ok pTx
|
|
|
|
|
|
proc rollback*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
): Result[AristoTxRef,(VertexID,AristoError)]
|
|
{.discardable.} =
|
|
## Given a *top level* handle, this function discards all database operations
|
|
## performed through this handle and returns the previous one which becomes
|
|
## either the *top level* or the *base level* handle, again.
|
|
##
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err((VertexID(0),TxTopHandleExpected))
|
|
|
|
block:
|
|
let rc = tdb.db.pop(merge = false)
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
|
|
let pTx = tdb.parent
|
|
pTx.db = tdb.db
|
|
|
|
tdb.parent = AristoTxRef(nil)
|
|
tdb.db = AristoDbRef(nil)
|
|
ok pTx
|
|
|
|
|
|
proc commit*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
hashify = false; # Always calc Merkle hashes if `true`
|
|
): Result[AristoTxRef,(VertexID,AristoError)]
|
|
{.discardable.} =
|
|
## Given a *top level* handle, this function accepts all database operations
|
|
## performed through this handle and merges it to the previous layer. It
|
|
## returns this previous layer which becomes either the *top level* or the
|
|
## *base level* handle, again.
|
|
##
|
|
## If the function return value is a *base level* handle, all the accumulated
|
|
## prevoius database operations will have been hashified and successfully
|
|
## stored on the persistent database.
|
|
##
|
|
## If the argument `hashify` is set `true`, the function will always hashify
|
|
## (i.e. calculate Merkle hashes) regardless of whether it is stored on the
|
|
## backend.
|
|
##
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err((VertexID(0),TxTopHandleExpected))
|
|
|
|
block:
|
|
let rc = tdb.db.pop(merge = true)
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
|
|
let pTx = tdb.parent
|
|
pTx.db = tdb.db
|
|
|
|
# Hashify and save (if any)
|
|
if hashify or pTx.parent.isNil:
|
|
let rc = tdb.db.hashify()
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
if pTx.parent.isNil:
|
|
let rc = tdb.db.save()
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
|
|
tdb.db = AristoDbRef(nil)
|
|
tdb.parent = AristoTxRef(nil)
|
|
ok pTx
|
|
|
|
|
|
proc collapse*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
commit: bool; # Commit is `true`, otherwise roll back
|
|
): Result[AristoTxRef,(VertexID,AristoError)] =
|
|
## Variation of `commit()` or `rollback()` performing the equivalent of
|
|
## ::
|
|
## while tx.isTop:
|
|
## let rc =
|
|
## if commit: tx.commit()
|
|
## else: tx.rollback()
|
|
## ...
|
|
## tx = rc.value
|
|
##
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err((VertexID(0),TxTopHandleExpected))
|
|
|
|
# Get base layer
|
|
var pTx = tdb.parent
|
|
while not pTx.parent.isNil:
|
|
pTx = pTx.parent
|
|
pTx.db = tdb.db
|
|
|
|
# Hashify and save, or complete rollback
|
|
if commit:
|
|
block:
|
|
let rc = tdb.db.hashify()
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
block:
|
|
let rc = tdb.db.save()
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
else:
|
|
let rc = tdb.db.retool(flushStack = true)
|
|
if rc.isErr:
|
|
return err((VertexID(0),rc.error))
|
|
|
|
tdb.db = AristoDbRef(nil)
|
|
tdb.parent = AristoTxRef(nil)
|
|
ok pTx
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public functions: DB manipulations
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc put*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
leaf: LeafTiePayload; # Leaf item to add to the database
|
|
): Result[bool,AristoError] =
|
|
## Add leaf entry to transaction layer.
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err(TxTopHandleExpected)
|
|
|
|
let report = tdb.db.merge @[leaf]
|
|
if report.error != AristoError(0):
|
|
return err(report.error)
|
|
|
|
ok(0 < report.merged)
|
|
|
|
|
|
proc del*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
leaf: LeafTie; # `Patricia Trie` path root-to-leaf
|
|
): Result[void,(VertexID,AristoError)] =
|
|
## Delete leaf entry from transaction layer.
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err((VertexID(0),TxTopHandleExpected))
|
|
|
|
tdb.db.delete leaf
|
|
|
|
|
|
proc get*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
leaf: LeafTie; # `Patricia Trie` path root-to-leaf
|
|
): Result[PayloadRef,(VertexID,AristoError)] =
|
|
## Get leaf entry from database filtered through the transaction layer.
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err((VertexID(0),TxTopHandleExpected))
|
|
|
|
let hike = leaf.hikeUp tdb.db
|
|
if hike.error != AristoError(0):
|
|
let vid = if hike.legs.len == 0: VertexID(0) else: hike.legs[^1].wp.vid
|
|
return err((vid,hike.error))
|
|
|
|
ok hike.legs[^1].wp.vtx.lData
|
|
|
|
|
|
proc key*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
vid: VertexID;
|
|
): Result[HashKey,(VertexID,AristoError)] =
|
|
## Get the Merkle hash key for the argument vertex ID `vid`. This function
|
|
## hashifies (i.e. calculates Merkle hashe keys) unless available on the
|
|
## requested vertex ID.
|
|
##
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err((VertexID(0),TxTopHandleExpected))
|
|
|
|
if tdb.db.top.kMap.hasKey vid:
|
|
block:
|
|
let key = tdb.db.top.kMap.getOrVoid(vid).key
|
|
if key.isValid:
|
|
return ok(key)
|
|
let rc = tdb.db.hashify()
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
block:
|
|
let key = tdb.db.top.kMap.getOrVoid(vid).key
|
|
if key.isValid:
|
|
return ok(key)
|
|
return err((vid,TxCacheKeyFetchFail))
|
|
|
|
block:
|
|
let rc = tdb.db.getKeyBackend vid
|
|
if rc.isOk:
|
|
return ok(rc.value)
|
|
|
|
return err((vid,TxBeKeyFetchFail))
|
|
|
|
proc rootKey*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
): Result[HashKey,(VertexID,AristoError)] =
|
|
## Get the Merkle hash key for the main state root (with vertex ID `1`.)
|
|
tdb.key VertexID(1)
|
|
|
|
|
|
proc changeLog*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
clear = false; # Delete history
|
|
): seq[AristoChangeLogRef] =
|
|
## Get the save history, i.e. the changed states before the database was
|
|
## updated on disc. If the argument `chear` is set `true`, the history log
|
|
## on the descriptor is cleared.
|
|
##
|
|
## The argument `tdb` must be a *top level* descriptor, i.e. `tdb.isTop()`
|
|
## returns `true`. Otherwise the function `changeLog()` always returns an
|
|
## empty list.
|
|
##
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return
|
|
result = tdb.db.history
|
|
if clear:
|
|
tdb.db.history.setlen(0)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public functions: DB traversal
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc right*(
|
|
lty: LeafTie; # Some `Patricia Trie` path
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
): Result[LeafTie,(VertexID,AristoError)] =
|
|
## Finds the next leaf to the right (if any.) For details see
|
|
## `aristo_nearby.right()`.
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err((VertexID(0),TxTopHandleExpected))
|
|
lty.right tdb.db
|
|
|
|
iterator right*(
|
|
tdb: AristoTxRef; # Database layer
|
|
start = low(LeafTie); # Before or at first value
|
|
): (LeafTie,PayloadRef) =
|
|
## Traverse the sub-trie implied by the argument `start` with increasing
|
|
## order. For details see `aristo_nearby.left()`.
|
|
if not tdb.db.isNil and not tdb.parent.isNil:
|
|
for (k,v) in tdb.db.right start:
|
|
yield (k,v)
|
|
|
|
|
|
proc left*(
|
|
lty: LeafTie; # Some `Patricia Trie` path
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
): Result[LeafTie,(VertexID,AristoError)] =
|
|
## Finds the next leaf to the left (if any.) For details see
|
|
## `aristo_nearby.left()`.
|
|
if tdb.db.isNil or tdb.parent.isNil:
|
|
return err((VertexID(0),TxTopHandleExpected))
|
|
lty.left tdb.db
|
|
|
|
iterator left*(
|
|
tdb: AristoTxRef; # Database layer
|
|
start = high(LeafTie); # Before or at first value
|
|
): (LeafTie,PayloadRef) =
|
|
## Traverse the sub-trie implied by the argument `start` with decreasing
|
|
## order. For details see `aristo_nearby.left()`.
|
|
if not tdb.db.isNil and not tdb.parent.isNil:
|
|
for (k,v) in tdb.db.left start:
|
|
yield (k,v)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public helpers, miscellaneous
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc level*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
): (int,int) =
|
|
## This function returns the nesting level of the transaction and the length
|
|
## of the internal stack. Both values must be equal (otherwise there would
|
|
## be an internal error.)
|
|
##
|
|
## The argument `tdb` must be a *top level* or *base level* descriptor, i.e.
|
|
## `tdb.isTop() or tdb.isBase()` evaluate `true`. Otherwise `(-1,-1)` is
|
|
## returned.
|
|
##
|
|
if tdb.db.isNil:
|
|
return (-1,-1)
|
|
|
|
if tdb.parent.isNil:
|
|
return (0, tdb.db.stack.len)
|
|
|
|
# Count base layer
|
|
var
|
|
count = 1
|
|
pTx = tdb.parent
|
|
while not pTx.parent.isNil:
|
|
count.inc
|
|
pTx = pTx.parent
|
|
|
|
(count, tdb.db.stack.len)
|
|
|
|
proc db*(
|
|
tdb: AristoTxRef; # Database, transaction wrapper
|
|
): AristoDbRef =
|
|
## Getter, provides access to the Aristo database cache and backend.
|
|
##
|
|
## The getter directive returns a valid object reference if the argument
|
|
## `tdb` is a *top level* or *base level* descriptor, i.e.
|
|
## `tdb.isTop() or tdb.isBase()` evaluate `true`.
|
|
##
|
|
tdb.db
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# End
|
|
# ------------------------------------------------------------------------------
|