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.
This commit is contained in:
Jacek Sieka 2024-06-21 09:44:10 +02:00 committed by GitHub
parent 83b3eeeb18
commit 768307d91d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 378 additions and 181 deletions

View File

@ -1,5 +1,5 @@
# Nimbus # Nimbus
# Copyright (c) 2018-2023 Status Research & Development GmbH # Copyright (c) 2018-2024 Status Research & Development GmbH
# Licensed under either of # Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0) # http://www.apache.org/licenses/LICENSE-2.0)
@ -8,9 +8,10 @@
# at your option. This file may not be copied, modified, or distributed except # at your option. This file may not be copied, modified, or distributed except
# according to those terms. # according to those terms.
import ../nimbus/vm/code_stream, strformat import ../nimbus/evm/code_stream, strformat
var c = newCodeStreamFromUnescaped("0x6003600202600055") var c =
CodeStream.init(CodeBytesRef.fromHex("0x6003600202600055").expect("valid code"))
let opcodes = c.decompile() let opcodes = c.decompile()
for op in opcodes: for op in opcodes:

View File

@ -18,8 +18,6 @@ import
../constants, ../constants,
../common/common ../common/common
from std/sequtils import mapIt
{.push raises: [].} {.push raises: [].}
type type

View File

@ -107,6 +107,19 @@ proc kvtMethods(cKvt: KvtCoreDbKvtRef): CoreDbKvtFns =
else: else:
rc.toRc(cKvt.base, info) rc.toRc(cKvt.base, info)
proc kvtLen(
cKvt: KvtCoreDbKvtRef;
k: openArray[byte];
info: static[string];
): CoreDbRc[int] =
let rc = cKvt.base.api.len(cKvt.kvt, k)
if rc.isOk:
ok(rc.value)
elif rc.error == GetNotFound:
err(rc.error.toError(cKvt.base, info, KvtNotFound))
else:
rc.toRc(cKvt.base, info)
proc kvtPut( proc kvtPut(
cKvt: KvtCoreDbKvtRef; cKvt: KvtCoreDbKvtRef;
k: openArray[byte]; k: openArray[byte];
@ -148,6 +161,9 @@ proc kvtMethods(cKvt: KvtCoreDbKvtRef): CoreDbKvtFns =
getFn: proc(k: openArray[byte]): CoreDbRc[Blob] = getFn: proc(k: openArray[byte]): CoreDbRc[Blob] =
cKvt.kvtGet(k, "getFn()"), cKvt.kvtGet(k, "getFn()"),
lenFn: proc(k: openArray[byte]): CoreDbRc[int] =
cKvt.kvtLen(k, "lenFn()"),
delFn: proc(k: openArray[byte]): CoreDbRc[void] = delFn: proc(k: openArray[byte]): CoreDbRc[void] =
cKvt.kvtDel(k, "delFn()"), cKvt.kvtDel(k, "delFn()"),

View File

@ -255,6 +255,12 @@ proc get*(kvt: CoreDbKvtRef; key: openArray[byte]): CoreDbRc[Blob] =
result = kvt.methods.getFn key result = kvt.methods.getFn key
kvt.ifTrackNewApi: debug newApiTxt, api, elapsed, key=key.toStr, result kvt.ifTrackNewApi: debug newApiTxt, api, elapsed, key=key.toStr, result
proc len*(kvt: CoreDbKvtRef; key: openArray[byte]): CoreDbRc[int] =
## This function always returns a non-empty `Blob` or an error code.
kvt.setTrackNewApi KvtLenFn
result = kvt.methods.lenFn key
kvt.ifTrackNewApi: debug newApiTxt, api, elapsed, key=key.toStr, result
proc getOrEmpty*(kvt: CoreDbKvtRef; key: openArray[byte]): CoreDbRc[Blob] = proc getOrEmpty*(kvt: CoreDbKvtRef; key: openArray[byte]): CoreDbRc[Blob] =
## This function sort of mimics the behaviour of the legacy database ## This function sort of mimics the behaviour of the legacy database
## returning an empty `Blob` if the argument `key` is not found on the ## returning an empty `Blob` if the argument `key` is not found on the

View File

@ -70,6 +70,7 @@ type
KvtDelFn = "kvt/del" KvtDelFn = "kvt/del"
KvtForgetFn = "kvt/forget" KvtForgetFn = "kvt/forget"
KvtGetFn = "kvt/get" KvtGetFn = "kvt/get"
KvtLenFn = "kvt/len"
KvtGetOrEmptyFn = "kvt/getOrEmpty" KvtGetOrEmptyFn = "kvt/getOrEmpty"
KvtHasKeyFn = "kvt/hasKey" KvtHasKeyFn = "kvt/hasKey"
KvtPairsIt = "kvt/pairs" KvtPairsIt = "kvt/pairs"

View File

@ -140,6 +140,7 @@ type
# -------------------------------------------------- # --------------------------------------------------
CoreDbKvtBackendFn* = proc(): CoreDbKvtBackendRef {.noRaise.} CoreDbKvtBackendFn* = proc(): CoreDbKvtBackendRef {.noRaise.}
CoreDbKvtGetFn* = proc(k: openArray[byte]): CoreDbRc[Blob] {.noRaise.} CoreDbKvtGetFn* = proc(k: openArray[byte]): CoreDbRc[Blob] {.noRaise.}
CoreDbKvtLenFn* = proc(k: openArray[byte]): CoreDbRc[int] {.noRaise.}
CoreDbKvtDelFn* = proc(k: openArray[byte]): CoreDbRc[void] {.noRaise.} CoreDbKvtDelFn* = proc(k: openArray[byte]): CoreDbRc[void] {.noRaise.}
CoreDbKvtPutFn* = CoreDbKvtPutFn* =
proc(k: openArray[byte]; v: openArray[byte]): CoreDbRc[void] {.noRaise.} proc(k: openArray[byte]; v: openArray[byte]): CoreDbRc[void] {.noRaise.}
@ -150,6 +151,7 @@ type
## Methods for key-value table ## Methods for key-value table
backendFn*: CoreDbKvtBackendFn backendFn*: CoreDbKvtBackendFn
getFn*: CoreDbKvtGetFn getFn*: CoreDbKvtGetFn
lenFn*: CoreDbKvtLenFn
delFn*: CoreDbKvtDelFn delFn*: CoreDbKvtDelFn
putFn*: CoreDbKvtPutFn putFn*: CoreDbKvtPutFn
hasKeyFn*: CoreDbKvtHasKeyFn hasKeyFn*: CoreDbKvtHasKeyFn

View File

@ -44,6 +44,7 @@ proc validateMethodsDesc(base: CoreDbBaseFns) =
proc validateMethodsDesc(kvt: CoreDbKvtFns) = proc validateMethodsDesc(kvt: CoreDbKvtFns) =
doAssert not kvt.backendFn.isNil doAssert not kvt.backendFn.isNil
doAssert not kvt.getFn.isNil doAssert not kvt.getFn.isNil
doAssert not kvt.lenFn.isNil
doAssert not kvt.delFn.isNil doAssert not kvt.delFn.isNil
doAssert not kvt.putFn.isNil doAssert not kvt.putFn.isNil
doAssert not kvt.hasKeyFn.isNil doAssert not kvt.hasKeyFn.isNil

View File

@ -50,6 +50,8 @@ type
backLevel: int): Result[KvtDbRef,KvtError] {.noRaise.} backLevel: int): Result[KvtDbRef,KvtError] {.noRaise.}
KvtApiGetFn* = proc(db: KvtDbRef, KvtApiGetFn* = proc(db: KvtDbRef,
key: openArray[byte]): Result[Blob,KvtError] {.noRaise.} key: openArray[byte]): Result[Blob,KvtError] {.noRaise.}
KvtApiLenFn* = proc(db: KvtDbRef,
key: openArray[byte]): Result[int,KvtError] {.noRaise.}
KvtApiHasKeyFn* = proc(db: KvtDbRef, KvtApiHasKeyFn* = proc(db: KvtDbRef,
key: openArray[byte]): Result[bool,KvtError] {.noRaise.} key: openArray[byte]): Result[bool,KvtError] {.noRaise.}
KvtApiIsCentreFn* = proc(db: KvtDbRef): bool {.noRaise.} KvtApiIsCentreFn* = proc(db: KvtDbRef): bool {.noRaise.}
@ -76,6 +78,7 @@ type
forget*: KvtApiForgetFn forget*: KvtApiForgetFn
forkTx*: KvtApiForkTxFn forkTx*: KvtApiForkTxFn
get*: KvtApiGetFn get*: KvtApiGetFn
len*: KvtApiLenFn
hasKey*: KvtApiHasKeyFn hasKey*: KvtApiHasKeyFn
isCentre*: KvtApiIsCentreFn isCentre*: KvtApiIsCentreFn
isTop*: KvtApiIsTopFn isTop*: KvtApiIsTopFn
@ -100,6 +103,7 @@ type
KvtApiProfForgetFn = "forget" KvtApiProfForgetFn = "forget"
KvtApiProfForkTxFn = "forkTx" KvtApiProfForkTxFn = "forkTx"
KvtApiProfGetFn = "get" KvtApiProfGetFn = "get"
KvtApiProfLenFn = "len"
KvtApiProfHasKeyFn = "hasKey" KvtApiProfHasKeyFn = "hasKey"
KvtApiProfIsCentreFn = "isCentre" KvtApiProfIsCentreFn = "isCentre"
KvtApiProfIsTopFn = "isTop" KvtApiProfIsTopFn = "isTop"
@ -114,6 +118,7 @@ type
KvtApiProfTxTopFn = "txTop" KvtApiProfTxTopFn = "txTop"
KvtApiProfBeGetKvpFn = "be/getKvp" KvtApiProfBeGetKvpFn = "be/getKvp"
KvtApiProfBeLenKvpFn = "be/lenKvp"
KvtApiProfBePutKvpFn = "be/putKvp" KvtApiProfBePutKvpFn = "be/putKvp"
KvtApiProfBePutEndFn = "be/putEnd" KvtApiProfBePutEndFn = "be/putEnd"
@ -176,6 +181,7 @@ func init*(api: var KvtApiObj) =
api.forget = forget api.forget = forget
api.forkTx = forkTx api.forkTx = forkTx
api.get = get api.get = get
api.len = len
api.hasKey = hasKey api.hasKey = hasKey
api.isCentre = isCentre api.isCentre = isCentre
api.isTop = isTop api.isTop = isTop
@ -203,6 +209,7 @@ func dup*(api: KvtApiRef): KvtApiRef =
forget: api.forget, forget: api.forget,
forkTx: api.forkTx, forkTx: api.forkTx,
get: api.get, get: api.get,
len: api.len,
hasKey: api.hasKey, hasKey: api.hasKey,
isCentre: api.isCentre, isCentre: api.isCentre,
isTop: api.isTop, isTop: api.isTop,
@ -275,6 +282,11 @@ func init*(
KvtApiProfGetFn.profileRunner: KvtApiProfGetFn.profileRunner:
result = api.get(a, b) result = api.get(a, b)
profApi.len =
proc(a: KvtDbRef, b: openArray[byte]): auto =
KvtApiProfLenFn.profileRunner:
result = api.len(a, b)
profApi.hasKey = profApi.hasKey =
proc(a: KvtDbRef, b: openArray[byte]): auto = proc(a: KvtDbRef, b: openArray[byte]): auto =
KvtApiProfHasKeyFn.profileRunner: KvtApiProfHasKeyFn.profileRunner:
@ -346,6 +358,12 @@ func init*(
result = be.getKvpFn(a) result = be.getKvpFn(a)
data.list[KvtApiProfBeGetKvpFn.ord].masked = true data.list[KvtApiProfBeGetKvpFn.ord].masked = true
beDup.lenKvpFn =
proc(a: openArray[byte]): auto =
KvtApiProfBeLenKvpFn.profileRunner:
result = be.lenKvpFn(a)
data.list[KvtApiProfBeLenKvpFn.ord].masked = true
beDup.putKvpFn = beDup.putKvpFn =
proc(a: PutHdlRef; b: openArray[(Blob,Blob)]) = proc(a: PutHdlRef; b: openArray[(Blob,Blob)]) =
be.putKvpFn(a,b) be.putKvpFn(a,b)

View File

@ -23,6 +23,9 @@ type
GetKvpFn* = GetKvpFn* =
proc(key: openArray[byte]): Result[Blob,KvtError] {.gcsafe, raises: [].} proc(key: openArray[byte]): Result[Blob,KvtError] {.gcsafe, raises: [].}
## Generic backend database retrieval function ## Generic backend database retrieval function
LenKvpFn* =
proc(key: openArray[byte]): Result[int,KvtError] {.gcsafe, raises: [].}
## Generic backend database retrieval function
# ------------- # -------------
@ -76,6 +79,7 @@ type
## Backend interface. ## Backend interface.
getKvpFn*: GetKvpFn ## Read key-value pair getKvpFn*: GetKvpFn ## Read key-value pair
lenKvpFn*: LenKvpFn ## Read key-value pair length
putBegFn*: PutBegFn ## Start bulk store session putBegFn*: PutBegFn ## Start bulk store session
putKvpFn*: PutKvpFn ## Bulk store key-value pairs putKvpFn*: PutKvpFn ## Bulk store key-value pairs
@ -88,6 +92,7 @@ type
proc init*(trg: var BackendObj; src: BackendObj) = proc init*(trg: var BackendObj; src: BackendObj) =
trg.getKvpFn = src.getKvpFn trg.getKvpFn = src.getKvpFn
trg.lenKvpFn = src.lenKvpFn
trg.putBegFn = src.putBegFn trg.putBegFn = src.putBegFn
trg.putKvpFn = src.putKvpFn trg.putKvpFn = src.putKvpFn
trg.putEndFn = src.putEndFn trg.putEndFn = src.putEndFn

View File

@ -82,6 +82,16 @@ proc getKvpFn(db: MemBackendRef): GetKvpFn =
return ok(move(data)) return ok(move(data))
err(GetNotFound) err(GetNotFound)
proc lenKvpFn(db: MemBackendRef): LenKvpFn =
result =
proc(key: openArray[byte]): Result[int,KvtError] =
if key.len == 0:
return err(KeyInvalid)
var data = db.mdb.tab.getOrVoid @key
if data.isValid:
return ok(data.len)
err(GetNotFound)
# ------------- # -------------
proc putBegFn(db: MemBackendRef): PutBegFn = proc putBegFn(db: MemBackendRef): PutBegFn =
@ -140,6 +150,7 @@ proc memoryBackend*: BackendRef =
mdb: MemDbRef()) mdb: MemDbRef())
db.getKvpFn = getKvpFn db db.getKvpFn = getKvpFn db
db.lenKvpFn = lenKvpFn db
db.putBegFn = putBegFn db db.putBegFn = putBegFn db
db.putKvpFn = putKvpFn db db.putKvpFn = putKvpFn db

View File

@ -92,6 +92,22 @@ proc getKvpFn(db: RdbBackendRef): GetKvpFn =
err(GetNotFound) err(GetNotFound)
proc lenKvpFn(db: RdbBackendRef): LenKvpFn =
result =
proc(key: openArray[byte]): Result[int,KvtError] =
# Get data record
var len = db.rdb.len(key).valueOr:
when extraTraceMessages:
debug logTxt "lenKvpFn() failed", key, error=error[0], info=error[1]
return err(error[0])
# Return if non-empty
if 0 < len:
return ok(len)
err(GetNotFound)
# ------------- # -------------
proc putBegFn(db: RdbBackendRef): PutBegFn = proc putBegFn(db: RdbBackendRef): PutBegFn =
@ -268,6 +284,7 @@ proc rocksDbKvtBackend*(
return err(error) return err(error)
db.getKvpFn = getKvpFn db db.getKvpFn = getKvpFn db
db.lenKvpFn = lenKvpFn db
db.putBegFn = putBegFn db db.putBegFn = putBegFn db
db.putKvpFn = putKvpFn db db.putKvpFn = putKvpFn db
@ -297,6 +314,7 @@ proc rocksDbKvtTriggeredBackend*(
return err((RdbBeHostError,$error)) return err((RdbBeHostError,$error))
db.getKvpFn = getKvpFn db db.getKvpFn = getKvpFn db
db.lenKvpFn = lenKvpFn db
db.putBegFn = putBegTriggeredFn db db.putBegFn = putBegTriggeredFn db
db.putKvpFn = putKvpFn db db.putKvpFn = putKvpFn db

View File

@ -53,6 +53,24 @@ proc get*(
res = EmptyBlob res = EmptyBlob
ok move(res) ok move(res)
proc len*(
rdb: RdbInst;
key: openArray[byte],
): Result[int,(KvtError,string)] =
var res: int
let onData: DataProc = proc(data: openArray[byte]) =
res = data.len
let gotData = rdb.store[KvtGeneric].get(key, onData).valueOr:
const errSym = RdbBeDriverGetError
when extraTraceMessages:
trace logTxt "len", error=errSym, info=error
return err((errSym,error))
if not gotData:
res = 0
ok res
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# End # End
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -31,18 +31,25 @@ func nLayersKeys*(db: KvtDbRef): int =
# Public functions: get function # Public functions: get function
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
func layersHasKey*(db: KvtDbRef; key: openArray[byte]|seq[byte]): bool = func layersLen*(db: KvtDbRef; key: openArray[byte]|seq[byte]): Opt[int] =
## Return `true` id the argument key is cached. ## Return `true` id the argument key is cached.
## ##
when key isnot seq[byte]: when key isnot seq[byte]:
let key = @key let key = @key
if db.top.delta.sTab.hasKey key:
return true db.top.delta.sTab.withValue(key, item):
return Opt.some(item[].len())
for w in db.rstack: for w in db.rstack:
if w.delta.sTab.hasKey key: w.delta.sTab.withValue(key, item):
return true 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] = func layersGet*(db: KvtDbRef; key: openArray[byte]|seq[byte]): Opt[Blob] =
## Find an item on the cache layers. An `ok()` result might contain an ## Find an item on the cache layers. An `ok()` result might contain an

View File

@ -36,6 +36,18 @@ proc getUbe*(
return be.getKvpFn key return be.getKvpFn key
err(GetNotFound) err(GetNotFound)
proc getUbeLen*(
db: KvtDbRef; # Database
key: openArray[byte]; # Key of database record
): Result[int,KvtError] =
## For the argument `key` return the associated value from the backend
## database if available.
##
let be = db.backend
if not be.isNil:
return be.lenKvpFn key
err(GetNotFound)
proc getBe*( proc getBe*(
db: KvtDbRef; # Database db: KvtDbRef; # Database
key: openArray[byte]; # Key of database record key: openArray[byte]; # Key of database record
@ -48,6 +60,18 @@ proc getBe*(
return ok(w[]) return ok(w[])
db.getUbe key db.getUbe key
proc getBeLen*(
db: KvtDbRef; # Database
key: openArray[byte]; # Key of database record
): Result[int,KvtError] =
## Get the vertex from the (filtered) backened if available.
if not db.balancer.isNil:
db.balancer.sTab.withValue(@key, w):
if w[].len == 0:
return err(GetNotFound)
return ok(w[].len)
db.getUbeLen key
# ------------ # ------------
proc put*( proc put*(
@ -95,6 +119,19 @@ proc get*(
return ok(move(data)) return ok(move(data))
proc len*(
db: KvtDbRef; # Database
key: openArray[byte]; # Key of database record
): Result[int,KvtError] =
## For the argument `key` return the associated value preferably from the
## top layer, or the database otherwise.
##
if key.len == 0:
return err(KeyInvalid)
let len = db.layersLen(key).valueOr:
return db.getBeLen key
ok(len)
proc hasKey*( proc hasKey*(
db: KvtDbRef; # Database db: KvtDbRef; # Database

View File

@ -25,6 +25,7 @@
## ##
import import
stew/keyed_queue,
std/[tables, hashes, sets], std/[tables, hashes, sets],
chronicles, chronicles,
eth/[common, rlp], eth/[common, rlp],
@ -33,10 +34,20 @@ import
"../.."/[constants, utils/utils], "../.."/[constants, utils/utils],
../access_list as ac_access_list, ../access_list as ac_access_list,
".."/[core_db, storage_types, transient_storage], ".."/[core_db, storage_types, transient_storage],
../../evm/code_bytes,
./distinct_ledgers ./distinct_ledgers
export code_bytes
const const
debugAccountsLedgerRef = false debugAccountsLedgerRef = false
codeLruSize = 16*1024
# An LRU cache of 16K items gives roughly 90% hit rate anecdotally on a
# small range of test blocks - this number could be studied in more detail
# Per EIP-170, a the code of a contract can be up to `MAX_CODE_SIZE` = 24kb,
# which would cause a worst case of 386MB memory usage though in reality
# code sizes are much smaller - it would make sense to study these numbers
# in greater detail.
type type
AccountFlag = enum AccountFlag = enum
@ -44,7 +55,6 @@ type
IsNew IsNew
Dirty Dirty
Touched Touched
CodeLoaded
CodeChanged CodeChanged
StorageChanged StorageChanged
NewlyCreated # EIP-6780: self destruct only in same transaction NewlyCreated # EIP-6780: self destruct only in same transaction
@ -54,7 +64,7 @@ type
AccountRef = ref object AccountRef = ref object
statement: CoreDbAccount statement: CoreDbAccount
flags: AccountFlags flags: AccountFlags
code: seq[byte] code: CodeBytesRef
originalStorage: TableRef[UInt256, UInt256] originalStorage: TableRef[UInt256, UInt256]
overlayStorage: Table[UInt256, UInt256] overlayStorage: Table[UInt256, UInt256]
@ -69,6 +79,16 @@ type
witnessCache: Table[EthAddress, WitnessData] witnessCache: Table[EthAddress, WitnessData]
isDirty: bool isDirty: bool
ripemdSpecial: bool ripemdSpecial: bool
code: KeyedQueue[Hash256, CodeBytesRef]
## The code cache provides two main benefits:
##
## * duplicate code is shared in memory beween accounts
## * the jump destination table does not have to be recomputed for every
## execution, for commonly called called contracts
##
## The former feature is specially important in the 2.3-2.7M block range
## when underpriced code opcodes are being run en masse - both advantages
## help performance broadly as well.
ReadOnlyStateDB* = distinct AccountsLedgerRef ReadOnlyStateDB* = distinct AccountsLedgerRef
@ -330,7 +350,7 @@ proc persistMode(acc: AccountRef): PersistMode =
proc persistCode(acc: AccountRef, ac: AccountsLedgerRef) = proc persistCode(acc: AccountRef, ac: AccountsLedgerRef) =
if acc.code.len != 0: if acc.code.len != 0:
let rc = ac.kvt.put( let rc = ac.kvt.put(
contractHashKey(acc.statement.codeHash).toOpenArray, acc.code) contractHashKey(acc.statement.codeHash).toOpenArray, acc.code.bytes())
if rc.isErr: if rc.isErr:
warn logTxt "persistCode()", warn logTxt "persistCode()",
codeHash=acc.statement.codeHash, error=($$rc.error) codeHash=acc.statement.codeHash, error=($$rc.error)
@ -412,33 +432,49 @@ proc getNonce*(ac: AccountsLedgerRef, address: EthAddress): AccountNonce =
if acc.isNil: emptyEthAccount.nonce if acc.isNil: emptyEthAccount.nonce
else: acc.statement.nonce else: acc.statement.nonce
proc getCode(acc: AccountRef, kvt: CoreDbKvtRef): lent seq[byte] = proc getCode*(ac: AccountsLedgerRef, address: EthAddress): CodeBytesRef =
if CodeLoaded notin acc.flags and CodeChanged notin acc.flags: # Always returns non-nil!
if acc.statement.codeHash != EMPTY_CODE_HASH:
var rc = kvt.get(contractHashKey(acc.statement.codeHash).toOpenArray)
if rc.isErr:
warn logTxt "getCode()", codeHash=acc.statement.codeHash, error=($$rc.error)
else:
acc.code = move(rc.value)
acc.flags.incl CodeLoaded
else:
acc.flags.incl CodeLoaded # avoid hash comparisons
acc.code
proc getCode*(ac: AccountsLedgerRef, address: EthAddress): seq[byte] =
let acc = ac.getAccount(address, false) let acc = ac.getAccount(address, false)
if acc.isNil: if acc.isNil:
return return CodeBytesRef()
acc.getCode(ac.kvt) if acc.code == nil:
acc.code =
if acc.statement.codeHash != EMPTY_CODE_HASH:
ac.code.lruFetch(acc.statement.codeHash).valueOr:
var rc = ac.kvt.get(contractHashKey(acc.statement.codeHash).toOpenArray)
if rc.isErr:
warn logTxt "getCode()", codeHash=acc.statement.codeHash, error=($$rc.error)
CodeBytesRef()
else:
let newCode = CodeBytesRef.init(move(rc.value))
ac.code.lruAppend(acc.statement.codeHash, newCode, codeLruSize)
else:
CodeBytesRef()
acc.code
proc getCodeSize*(ac: AccountsLedgerRef, address: EthAddress): int = proc getCodeSize*(ac: AccountsLedgerRef, address: EthAddress): int =
let acc = ac.getAccount(address, false) let acc = ac.getAccount(address, false)
if acc.isNil: if acc.isNil:
return return 0
acc.getCode(ac.kvt).len if acc.code == nil:
if acc.statement.codeHash == EMPTY_CODE_HASH:
return 0
acc.code = ac.code.lruFetch(acc.statement.codeHash).valueOr:
# On a cache miss, we don't fetch the code - instead, we fetch just the
# length - should the code itself be needed, it will typically remain
# cached and easily accessible in the database layer - this is to prevent
# EXTCODESIZE calls from messing up the code cache and thus causing
# recomputation of the jump destination table
var rc = ac.kvt.len(contractHashKey(acc.statement.codeHash).toOpenArray)
return rc.valueOr:
warn logTxt "getCodeSize()", codeHash=acc.statement.codeHash, error=($$rc.error)
0
acc.code.len()
proc getCommittedStorage*(ac: AccountsLedgerRef, address: EthAddress, slot: UInt256): UInt256 = proc getCommittedStorage*(ac: AccountsLedgerRef, address: EthAddress, slot: UInt256): UInt256 =
let acc = ac.getAccount(address, false) let acc = ac.getAccount(address, false)
@ -521,7 +557,9 @@ proc setCode*(ac: AccountsLedgerRef, address: EthAddress, code: seq[byte]) =
if acc.statement.codeHash != codeHash: if acc.statement.codeHash != codeHash:
var acc = ac.makeDirty(address) var acc = ac.makeDirty(address)
acc.statement.codeHash = codeHash acc.statement.codeHash = codeHash
acc.code = code # Try to reuse cache entry if it exists, but don't save the code - it's not
# a given that it will be executed within LRU range
acc.code = ac.code.lruFetch(codeHash).valueOr(CodeBytesRef.init(code))
acc.flags.incl CodeChanged acc.flags.incl CodeChanged
proc setStorage*(ac: AccountsLedgerRef, address: EthAddress, slot, value: UInt256) = proc setStorage*(ac: AccountsLedgerRef, address: EthAddress, slot, value: UInt256) =
@ -701,7 +739,7 @@ proc getStorageRoot*(ac: AccountsLedgerRef, address: EthAddress): Hash256 =
proc update(wd: var WitnessData, acc: AccountRef) = proc update(wd: var WitnessData, acc: AccountRef) =
# once the code is touched make sure it doesn't get reset back to false in another update # once the code is touched make sure it doesn't get reset back to false in another update
if not wd.codeTouched: if not wd.codeTouched:
wd.codeTouched = CodeChanged in acc.flags or CodeLoaded in acc.flags wd.codeTouched = CodeChanged in acc.flags or acc.code != nil
if not acc.originalStorage.isNil: if not acc.originalStorage.isNil:
for k, v in acc.originalStorage: for k, v in acc.originalStorage:
@ -801,7 +839,7 @@ proc getStorageRoot*(db: ReadOnlyStateDB, address: EthAddress): Hash256 {.borrow
proc getBalance*(db: ReadOnlyStateDB, address: EthAddress): UInt256 {.borrow.} proc getBalance*(db: ReadOnlyStateDB, address: EthAddress): UInt256 {.borrow.}
proc getStorage*(db: ReadOnlyStateDB, address: EthAddress, slot: UInt256): UInt256 {.borrow.} proc getStorage*(db: ReadOnlyStateDB, address: EthAddress, slot: UInt256): UInt256 {.borrow.}
proc getNonce*(db: ReadOnlyStateDB, address: EthAddress): AccountNonce {.borrow.} proc getNonce*(db: ReadOnlyStateDB, address: EthAddress): AccountNonce {.borrow.}
proc getCode*(db: ReadOnlyStateDB, address: EthAddress): seq[byte] {.borrow.} proc getCode*(db: ReadOnlyStateDB, address: EthAddress): CodeBytesRef {.borrow.}
proc getCodeSize*(db: ReadOnlyStateDB, address: EthAddress): int {.borrow.} proc getCodeSize*(db: ReadOnlyStateDB, address: EthAddress): int {.borrow.}
proc contractCollision*(db: ReadOnlyStateDB, address: EthAddress): bool {.borrow.} proc contractCollision*(db: ReadOnlyStateDB, address: EthAddress): bool {.borrow.}
proc accountExists*(db: ReadOnlyStateDB, address: EthAddress): bool {.borrow.} proc accountExists*(db: ReadOnlyStateDB, address: EthAddress): bool {.borrow.}

View File

@ -14,6 +14,7 @@
import import
eth/common, eth/common,
../../evm/code_bytes,
../../stateless/multi_keys, ../../stateless/multi_keys,
../core_db, ../core_db,
./base/[api_tracking, base_desc] ./base/[api_tracking, base_desc]
@ -33,6 +34,7 @@ type
ReadOnlyStateDB* = distinct LedgerRef ReadOnlyStateDB* = distinct LedgerRef
export export
code_bytes,
LedgerFnInx, LedgerFnInx,
LedgerProfListRef, LedgerProfListRef,
LedgerRef, LedgerRef,
@ -175,7 +177,7 @@ proc getBalance*(ldg: LedgerRef, eAddr: EthAddress): UInt256 =
result = ldg.ac.getBalance(eAddr) result = ldg.ac.getBalance(eAddr)
ldg.ifTrackApi: debug apiTxt, api, elapsed, eAddr, result ldg.ifTrackApi: debug apiTxt, api, elapsed, eAddr, result
proc getCode*(ldg: LedgerRef, eAddr: EthAddress): Blob = proc getCode*(ldg: LedgerRef, eAddr: EthAddress): CodeBytesRef =
ldg.beginTrackApi LdgGetCodeFn ldg.beginTrackApi LdgGetCodeFn
result = ldg.ac.getCode(eAddr) result = ldg.ac.getCode(eAddr)
ldg.ifTrackApi: debug apiTxt, api, elapsed, eAddr, result=result.toStr ldg.ifTrackApi: debug apiTxt, api, elapsed, eAddr, result=result.toStr
@ -371,7 +373,7 @@ proc getStorageRoot*(db: ReadOnlyStateDB, eAddr: EthAddress): Hash256 {.borrow.}
proc getBalance*(db: ReadOnlyStateDB, eAddr: EthAddress): UInt256 {.borrow.} proc getBalance*(db: ReadOnlyStateDB, eAddr: EthAddress): UInt256 {.borrow.}
proc getStorage*(db: ReadOnlyStateDB, eAddr: EthAddress, slot: UInt256): UInt256 {.borrow.} proc getStorage*(db: ReadOnlyStateDB, eAddr: EthAddress, slot: UInt256): UInt256 {.borrow.}
proc getNonce*(db: ReadOnlyStateDB, eAddr: EthAddress): AccountNonce {.borrow.} proc getNonce*(db: ReadOnlyStateDB, eAddr: EthAddress): AccountNonce {.borrow.}
proc getCode*(db: ReadOnlyStateDB, eAddr: EthAddress): seq[byte] {.borrow.} proc getCode*(db: ReadOnlyStateDB, eAddr: EthAddress): CodeBytesRef {.borrow.}
proc getCodeSize*(db: ReadOnlyStateDB, eAddr: EthAddress): int {.borrow.} proc getCodeSize*(db: ReadOnlyStateDB, eAddr: EthAddress): int {.borrow.}
proc contractCollision*(db: ReadOnlyStateDB, eAddr: EthAddress): bool {.borrow.} proc contractCollision*(db: ReadOnlyStateDB, eAddr: EthAddress): bool {.borrow.}
proc accountExists*(db: ReadOnlyStateDB, eAddr: EthAddress): bool {.borrow.} proc accountExists*(db: ReadOnlyStateDB, eAddr: EthAddress): bool {.borrow.}

74
nimbus/evm/code_bytes.nim Normal file
View File

@ -0,0 +1,74 @@
# Nimbus
# Copyright (c) 2018-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.
import stew/byteutils, results, ./interpreter/op_codes
export results
type CodeBytesRef* = ref object
## Code buffer that caches invalid jump positions used for verifying jump
## destinations - `bytes` is immutable once instances is created while
## `invalidPositions` will be built up on demand
bytes: seq[byte]
invalidPositions: seq[byte] # bit seq of invalid jump positions
processed: int
template bitpos(pos: int): (int, byte) =
(pos shr 3, 1'u8 shl (pos and 0x07))
func init*(T: type CodeBytesRef, bytes: sink seq[byte]): CodeBytesRef =
let ip = newSeq[byte]((bytes.len + 7) div 8)
CodeBytesRef(bytes: move(bytes), invalidPositions: ip)
func init*(T: type CodeBytesRef, bytes: openArray[byte]): CodeBytesRef =
CodeBytesRef.init(@bytes)
func init*(T: type CodeBytesRef, bytes: openArray[char]): CodeBytesRef =
CodeBytesRef.init(bytes.toOpenArrayByte(0, bytes.high()))
func fromHex*(T: type CodeBytesRef, hex: string): Opt[CodeBytesRef] =
try:
Opt.some(CodeBytesRef.init(hexToSeqByte(hex)))
except ValueError:
Opt.none(CodeBytesRef)
func invalidPosition(c: CodeBytesRef, pos: int): bool =
let (bpos, bbit) = bitpos(pos)
(c.invalidPositions[bpos] and bbit) > 0
func bytes*(c: CodeBytesRef): lent seq[byte] =
c[].bytes
func len*(c: CodeBytesRef): int =
len(c.bytes)
func isValidOpcode*(c: CodeBytesRef, position: int): bool =
if position >= len(c):
false
elif c.invalidPosition(position):
false
elif position <= c.processed:
true
else:
var i = c.processed
while i <= position:
var opcode = Op(c.bytes[i])
if opcode >= Op.Push1 and opcode <= Op.Push32:
var leftBound = (i + 1)
var rightBound = leftBound + (opcode.int - 95)
for z in leftBound ..< rightBound:
let (bpos, bbit) = bitpos(z)
c.invalidPositions[bpos] = c.invalidPositions[bpos] or bbit
i = rightBound
else:
i += 1
c.processed = i - 1
not c.invalidPosition(position)
func `==`*(a: CodeBytesRef, b: openArray[byte]): bool =
a.bytes == b

View File

@ -6,76 +6,52 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms. # at your option. This file may not be copied, modified, or distributed except according to those terms.
import import
chronicles, strformat, strutils, sequtils, parseutils, std/[sequtils, strutils], chronicles, eth/common, ./interpreter/op_codes, ./code_bytes
eth/common,
./interpreter/op_codes
logScope: export code_bytes
topics = "vm code_stream"
type type CodeStream* = object
CodeStream* = ref object code: CodeBytesRef
bytes*: seq[byte]
depthProcessed: int
invalidPositions: seq[byte] # bit seq of invalid jump positions
pc*: int pc*: int
proc `$`*(b: byte): string = func init*(T: type CodeStream, code: CodeBytesRef): T =
$(b.int) T(code: code)
template bitpos(pos: int): (int, byte) = func init*(T: type CodeStream, code: sink seq[byte]): T =
(pos shr 3, 1'u8 shl (pos and 0x07)) T(code: CodeBytesRef.init(move(code)))
proc newCodeStream*(codeBytes: sink seq[byte]): CodeStream = func init*(T: type CodeStream, code: openArray[byte]): T =
new(result) T(code: CodeBytesRef.init(code))
result.bytes = system.move(codeBytes)
result.pc = 0
result.invalidPositions = newSeq[byte]((result.bytes.len + 7) div 8)
result.depthProcessed = 0
proc invalidPosition(c: CodeStream, pos: int): bool = func init*(T: type CodeStream, code: openArray[char]): T =
let (bpos, bbit) = bitpos(pos) T(code: CodeBytesRef.init(code))
(c.invalidPositions[bpos] and bbit) > 0
proc newCodeStream*(codeBytes: string): CodeStream = template read*(c: var CodeStream, size: int): openArray[byte] =
newCodeStream(codeBytes.mapIt(it.byte))
proc newCodeStreamFromUnescaped*(code: string): CodeStream =
# from 0xunescaped
var codeBytes: seq[byte] = @[]
for z, c in code[2..^1]:
if z mod 2 == 1:
var value: int
discard parseHex(&"0x{code[z+1..z+2]}", value)
codeBytes.add(value.byte)
newCodeStream(codeBytes)
template read*(c: CodeStream, size: int): openArray[byte] =
# TODO: use openArray[bytes]
if c.pc + size - 1 < c.bytes.len: if c.pc + size - 1 < c.bytes.len:
let pos = c.pc let pos = c.pc
c.pc += size c.pc += size
c.bytes.toOpenArray(pos, pos + size - 1) c.code.bytes.toOpenArray(pos, pos + size - 1)
else: else:
c.pc = c.bytes.len c.pc = c.bytes.len
c.bytes.toOpenArray(0, -1) c.code.bytes.toOpenArray(0, -1)
proc readVmWord*(c: var CodeStream, n: static int): UInt256 = func readVmWord*(c: var CodeStream, n: static int): UInt256 =
## Reads `n` bytes from the code stream and pads ## Reads `n` bytes from the code stream and pads
## the remaining bytes with zeros. ## the remaining bytes with zeros.
let result_bytes = cast[ptr array[32, byte]](addr result) let result_bytes = cast[ptr array[32, byte]](addr result)
let last = min(c.pc + n, c.bytes.len) let last = min(c.pc + n, c.code.bytes.len)
let toWrite = last - c.pc let toWrite = last - c.pc
for i in 0 ..< toWrite : result_bytes[i] = c.bytes[last - i - 1] for i in 0 ..< toWrite:
result_bytes[i] = c.code.bytes[last - i - 1]
c.pc = last c.pc = last
proc len*(c: CodeStream): int = func len*(c: CodeStream): int =
len(c.bytes) len(c.code)
proc next*(c: var CodeStream): Op = func next*(c: var CodeStream): Op =
if c.pc != c.bytes.len: if c.pc != c.code.len:
result = Op(c.bytes[c.pc]) result = Op(c.code.bytes[c.pc])
inc c.pc inc c.pc
else: else:
result = Op.Stop result = Op.Stop
@ -86,55 +62,42 @@ iterator items*(c: var CodeStream): Op =
yield nextOpcode yield nextOpcode
nextOpcode = c.next() nextOpcode = c.next()
proc `[]`*(c: CodeStream, offset: int): Op = func `[]`*(c: CodeStream, offset: int): Op =
Op(c.bytes[offset]) Op(c.code.bytes[offset])
proc peek*(c: var CodeStream): Op = func peek*(c: var CodeStream): Op =
if c.pc < c.bytes.len: if c.pc < c.code.bytes.len:
Op(c.bytes[c.pc]) Op(c.code.bytes[c.pc])
else: else:
Op.Stop Op.Stop
proc updatePc*(c: var CodeStream, value: int) = func updatePc*(c: var CodeStream, value: int) =
c.pc = min(value, len(c)) c.pc = min(value, len(c))
proc isValidOpcode*(c: CodeStream, position: int): bool = func isValidOpcode*(c: CodeStream, position: int): bool =
if position >= len(c): c.code.isValidOpcode(position)
false
elif c.invalidPosition(position):
false
elif position <= c.depthProcessed:
true
else:
var i = c.depthProcessed
while i <= position:
var opcode = Op(c[i])
if opcode >= Op.Push1 and opcode <= Op.Push32:
var leftBound = (i + 1)
var rightBound = leftBound + (opcode.int - 95)
for z in leftBound ..< rightBound:
let (bpos, bbit) = bitpos(z)
c.invalidPositions[bpos] = c.invalidPositions[bpos] or bbit
i = rightBound
else:
i += 1
c.depthProcessed = i - 1
not c.invalidPosition(position) func bytes*(c: CodeStream): lent seq[byte] =
c.code.bytes()
proc decompile*(original: CodeStream): seq[(int, Op, string)] = proc decompile*(original: CodeStream): seq[(int, Op, string)] =
# behave as https://etherscan.io/opcode-tool # behave as https://etherscan.io/opcode-tool
var c = newCodeStream(original.bytes) var c = CodeStream.init(original.bytes)
while true: while true:
var op = c.next var op = c.next
if op >= Push1 and op <= Push32: if op >= Push1 and op <= Push32:
result.add( result.add(
(c.pc - 1, op, "0x" & c.read(op.int - 95).mapIt($(it.BiggestInt.toHex(2))).join(""))) (
c.pc - 1,
op,
"0x" & c.read(op.int - 95).mapIt($(it.BiggestInt.toHex(2))).join(""),
)
)
elif op != Op.Stop: elif op != Op.Stop:
result.add((c.pc - 1, op, "")) result.add((c.pc - 1, op, ""))
else: else:
result.add((-1, Op.Stop, "")) result.add((-1, Op.Stop, ""))
break break
proc atEnd*(c: CodeStream): bool = func atEnd*(c: CodeStream): bool =
c.pc >= c.bytes.len c.pc >= c.code.bytes.len

View File

@ -16,6 +16,7 @@ import
"."/[types], "."/[types],
./interpreter/[gas_meter, gas_costs, op_codes], ./interpreter/[gas_meter, gas_costs, op_codes],
./evm_errors, ./evm_errors,
./code_bytes,
../common/[common, evmforks], ../common/[common, evmforks],
../utils/utils, ../utils/utils,
stew/byteutils, stew/byteutils,
@ -203,9 +204,9 @@ template selfDestruct*(c: Computation, address: EthAddress) =
else: else:
c.execSelfDestruct(address) c.execSelfDestruct(address)
template getCode*(c: Computation, address: EthAddress): seq[byte] = template getCode*(c: Computation, address: EthAddress): CodeBytesRef =
when evmc_enabled: when evmc_enabled:
c.host.copyCode(address) CodeBytesRef.init(c.host.copyCode(address))
else: else:
c.vmState.readOnlyStateDB.getCode(address) c.vmState.readOnlyStateDB.getCode(address)
@ -236,14 +237,14 @@ proc newComputation*(vmState: BaseVMState, sysCall: bool, message: Message,
if result.msg.isCreate(): if result.msg.isCreate():
result.msg.contractAddress = result.generateContractAddress(salt) result.msg.contractAddress = result.generateContractAddress(salt)
result.code = newCodeStream(message.data) result.code = CodeStream.init(message.data)
message.data = @[] message.data = @[]
else: else:
result.code = newCodeStream( result.code = CodeStream.init(
vmState.readOnlyStateDB.getCode(message.codeAddress)) vmState.readOnlyStateDB.getCode(message.codeAddress))
func newComputation*(vmState: BaseVMState, sysCall: bool, func newComputation*(vmState: BaseVMState, sysCall: bool,
message: Message, code: seq[byte]): Computation = message: Message, code: CodeBytesRef): Computation =
new result new result
result.vmState = vmState result.vmState = vmState
result.msg = message result.msg = message
@ -251,7 +252,7 @@ func newComputation*(vmState: BaseVMState, sysCall: bool,
result.stack = EvmStackRef.new() result.stack = EvmStackRef.new()
result.returnStack = @[] result.returnStack = @[]
result.gasMeter.init(message.gas) result.gasMeter.init(message.gas)
result.code = newCodeStream(code) result.code = CodeStream.init(code)
result.sysCall = sysCall result.sysCall = sysCall
template gasCosts*(c: Computation): untyped = template gasCosts*(c: Computation): untyped =

View File

@ -176,8 +176,8 @@ proc extCodeCopyOp (k: var VmCtx): EvmResultVoid =
cpt.gasCosts[ExtCodeCopy].m_handler(cpt.memory.len, memPos, len), cpt.gasCosts[ExtCodeCopy].m_handler(cpt.memory.len, memPos, len),
reason = "ExtCodeCopy fee") reason = "ExtCodeCopy fee")
let codeBytes = cpt.getCode(address) let code = cpt.getCode(address)
cpt.memory.writePadded(codeBytes, memPos, codePos, len) cpt.memory.writePadded(code.bytes, memPos, codePos, len)
ok() ok()
@ -194,8 +194,8 @@ proc extCodeCopyEIP2929Op (k: var VmCtx): EvmResultVoid =
cpt.gasEip2929AccountCheck(address) cpt.gasEip2929AccountCheck(address)
? cpt.opcodeGastCost(ExtCodeCopy, gasCost, reason = "ExtCodeCopy EIP2929") ? cpt.opcodeGastCost(ExtCodeCopy, gasCost, reason = "ExtCodeCopy EIP2929")
let codeBytes = cpt.getCode(address) let code = cpt.getCode(address)
cpt.memory.writePadded(codeBytes, memPos, codePos, len) cpt.memory.writePadded(code.bytes(), memPos, codePos, len)
ok() ok()
# ----------- # -----------

View File

@ -543,7 +543,7 @@ proc accountCode(ud: RootRef, params: Args, parent: Node): RespResult {.apiPragm
let acc = AccountNode(parent) let acc = AccountNode(parent)
try: try:
let code = acc.db.getCode(acc.address) let code = acc.db.getCode(acc.address)
resp(code) resp(code.bytes())
except RlpError as ex: except RlpError as ex:
err(ex.msg) err(ex.msg)

View File

@ -246,7 +246,7 @@ proc setupEthRpc*(
let let
accDB = stateDBFromTag(quantityTag) accDB = stateDBFromTag(quantityTag)
address = data.ethAddr address = data.ethAddr
result = accDB.getCode(address) result = accDB.getCode(address).bytes()
template sign(privateKey: PrivateKey, message: string): seq[byte] = template sign(privateKey: PrivateKey, message: string): seq[byte] =
# message length encoded as ASCII representation of decimal # message length encoded as ASCII representation of decimal

View File

@ -166,7 +166,7 @@ proc setupHost(call: CallParams): TransactionHost =
# with the contract address. This differs from the previous Nimbus EVM API. # with the contract address. This differs from the previous Nimbus EVM API.
# Guarded under `evmc_enabled` for now so it doesn't break vm2. # Guarded under `evmc_enabled` for now so it doesn't break vm2.
when defined(evmc_enabled): when defined(evmc_enabled):
var code: seq[byte] var code: CodeBytesRef
if call.isCreate: if call.isCreate:
let sender = call.sender let sender = call.sender
let contractAddress = let contractAddress =
@ -174,7 +174,7 @@ proc setupHost(call: CallParams): TransactionHost =
host.msg.recipient = contractAddress.toEvmc host.msg.recipient = contractAddress.toEvmc
host.msg.input_size = 0 host.msg.input_size = 0
host.msg.input_data = nil host.msg.input_data = nil
code = call.input code = CodeBytesRef.init(call.input)
else: else:
# TODO: Share the underlying data, but only after checking this does not # TODO: Share the underlying data, but only after checking this does not
# cause problems with the database. # cause problems with the database.
@ -189,7 +189,7 @@ proc setupHost(call: CallParams): TransactionHost =
let cMsg = hostToComputationMessage(host.msg) let cMsg = hostToComputationMessage(host.msg)
host.computation = newComputation(vmState, call.sysCall, cMsg, code) host.computation = newComputation(vmState, call.sysCall, cMsg, code)
host.code = system.move(code) host.code = code
else: else:
if call.input.len > 0: if call.input.len > 0:

View File

@ -14,7 +14,7 @@ import
evmc/evmc, ../config evmc/evmc, ../config
# The built-in Nimbus EVM, via imported C function. # The built-in Nimbus EVM, via imported C function.
proc evmc_create_nimbus_evm(): ptr evmc_vm {.cdecl, importc, raises: [].} proc evmc_create_nimbus_evm(): ptr evmc_vm {.cdecl, importc, raises: [], gcsafe.}
# Import this module to link in the definition of `evmc_create_nimbus_evm`. # Import this module to link in the definition of `evmc_create_nimbus_evm`.
# Nim thinks the module is unused because the function is only called via # Nim thinks the module is unused because the function is only called via

View File

@ -123,26 +123,9 @@ proc evmcExecComputation*(host: TransactionHost): EvmcResult =
let hostContext = cast[evmc_host_context](host) let hostContext = cast[evmc_host_context](host)
host.hostInterface = hostInterface.unsafeAddr host.hostInterface = hostInterface.unsafeAddr
# Without `{.gcsafe.}:` here, the call via `vm.execute` results in a Nim
# compile-time error in a far away function. Starting here, a cascade of
# warnings takes place: "Warning: '...' is not GC-safe as it performs an
# indirect call here [GCUnsafe2]", then a list of "Warning: '...' is not
# GC-safe as it calls '...'" at each function up the call stack, to a high
# level function `persistBlocks` where it terminates compilation as an error
# instead of a warning.
#
# It is tempting to annotate all EVMC API functions with `{.cdecl, gcsafe.}`,
# overriding the function signatures from the Nim EVMC module. Perhaps we
# will do that, though it's conceptually dubious, as the two sides of the
# EVMC ABI live in different GC worlds (when loaded as a shared library with
# its own Nim runtime), very similar to calling between threads.
#
# TODO: But wait: Why does the Nim EVMC test program compile fine without
# any `gcsafe`, even with `--threads:on`?
{.gcsafe.}:
result = vm.execute(vm, hostInterface.unsafeAddr, hostContext, result = vm.execute(vm, hostInterface.unsafeAddr, hostContext,
evmc_revision(host.vmState.fork.ord), host.msg, evmc_revision(host.vmState.fork.ord), host.msg,
if host.code.len > 0: host.code[0].unsafeAddr if host.code.len > 0: host.code.bytes[0].unsafeAddr
else: nil, else: nil,
host.code.len.csize_t) host.code.len.csize_t)

View File

@ -218,7 +218,7 @@ proc copyCode(host: TransactionHost, address: HostAddress,
# #
# Note, when there is no code, `getCode` result is empty `seq`. It was `nil` # Note, when there is no code, `getCode` result is empty `seq`. It was `nil`
# when the DB was first implemented, due to Nim language changes since then. # when the DB was first implemented, due to Nim language changes since then.
var code: seq[byte] = host.vmState.readOnlyStateDB.getCode(address) let code = host.vmState.readOnlyStateDB.getCode(address)
var safe_len: int = code.len # It's safe to assume >= 0. var safe_len: int = code.len # It's safe to assume >= 0.
if code_offset >= safe_len.HostSize: if code_offset >= safe_len.HostSize:
@ -230,7 +230,7 @@ proc copyCode(host: TransactionHost, address: HostAddress,
safe_len = buffer_size.int safe_len = buffer_size.int
if safe_len > 0: if safe_len > 0:
copyMem(buffer_data, code[safe_offset].addr, safe_len) copyMem(buffer_data, code.bytes()[safe_offset].addr, safe_len)
return safe_len.HostSize return safe_len.HostSize
proc selfDestruct(host: TransactionHost, address, beneficiary: HostAddress) {.show.} = proc selfDestruct(host: TransactionHost, address, beneficiary: HostAddress) {.show.} =

View File

@ -10,7 +10,7 @@ import
std/sets, std/sets,
stint, evmc/evmc, stint, evmc/evmc,
eth/common/eth_types, eth/common/eth_types,
../evm/types ../evm/[code_bytes, types]
# Object `TransactionHost` represents "EVMC host" to the EVM. "Host services" # Object `TransactionHost` represents "EVMC host" to the EVM. "Host services"
# manage account state outside EVM such as balance transfers, storage, logs and # manage account state outside EVM such as balance transfers, storage, logs and
@ -59,7 +59,7 @@ type
computation*: Computation computation*: Computation
msg*: EvmcMessage msg*: EvmcMessage
input*: seq[byte] input*: seq[byte]
code*: seq[byte] code*: CodeBytesRef
cachedTxContext*: bool cachedTxContext*: bool
txContext*: EvmcTxContext txContext*: EvmcTxContext
depth*: int depth*: int

View File

@ -1,5 +1,5 @@
# Nimbus # Nimbus
# Copyright (c) 2023 Status Research & Development GmbH # Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of # Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0) # http://www.apache.org/licenses/LICENSE-2.0)
@ -75,7 +75,7 @@ proc dumpAccount*(db: LedgerRef, acc: EthAddress): DumpAccount =
nonce : db.getNonce(acc), nonce : db.getNonce(acc),
root : db.getStorageRoot(acc), root : db.getStorageRoot(acc),
codeHash: db.getCodeHash(acc), codeHash: db.getCodeHash(acc),
code : db.getCode(acc), code : db.getCode(acc).bytes(),
key : keccakHash(acc) key : keccakHash(acc)
) )
for k, v in db.cachedStorage(acc): for k, v in db.cachedStorage(acc):

View File

@ -11,7 +11,7 @@
import import
std/[json, strutils], std/[json, strutils],
json_rpc/[rpcclient], httputils, json_rpc/[rpcclient], httputils,
eth/[common, rlp], chronicles, eth/common, chronicles,
../nimbus/utils/utils, ../nimbus/utils/utils,
./parser ./parser

View File

@ -6,15 +6,12 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms. # at your option. This file may not be copied, modified, or distributed except according to those terms.
import import
std/[times, macros, strutils, os, osproc, threadpool], std/[times, macros, strutils, os, osproc],
unittest2, unittest2,
../nimbus/compile_info, ../nimbus/compile_info,
../nimbus/utils/utils ../nimbus/utils/utils
export strutils, os, unittest2, osproc, threadpool export strutils, os, unittest2, osproc
# AppVeyor may go out of memory with the default of 4
setMinPoolSize(2)
proc executeMyself(numModules: int, names: openArray[string]): int = proc executeMyself(numModules: int, names: openArray[string]): int =
let appName = getAppFilename() let appName = getAppFilename()

View File

@ -11,7 +11,7 @@ import unittest2, sequtils,
proc codeStreamMain*() = proc codeStreamMain*() =
suite "parse bytecode": suite "parse bytecode":
test "accepts bytes": test "accepts bytes":
let codeStream = newCodeStream("\x01") let codeStream = CodeStream.init("\x01")
check(codeStream.len == 1) check(codeStream.len == 1)
@ -22,14 +22,14 @@ proc codeStreamMain*() =
# CodeStream(code_bytes) # CodeStream(code_bytes)
test "next returns the correct opcode": test "next returns the correct opcode":
var codeStream = newCodeStream("\x01\x02\x30") var codeStream = CodeStream.init("\x01\x02\x30")
check(codeStream.next == Op.ADD) check(codeStream.next == Op.ADD)
check(codeStream.next == Op.MUL) check(codeStream.next == Op.MUL)
check(codeStream.next == Op.ADDRESS) check(codeStream.next == Op.ADDRESS)
test "peek returns next opcode without changing location": test "peek returns next opcode without changing location":
var codeStream = newCodeStream("\x01\x02\x30") var codeStream = CodeStream.init("\x01\x02\x30")
check(codeStream.pc == 0) check(codeStream.pc == 0)
check(codeStream.peek == Op.ADD) check(codeStream.peek == Op.ADD)
check(codeStream.pc == 0) check(codeStream.pc == 0)
@ -40,14 +40,14 @@ proc codeStreamMain*() =
test "stop opcode is returned when end reached": test "stop opcode is returned when end reached":
var codeStream = newCodeStream("\x01\x02") var codeStream = CodeStream.init("\x01\x02")
discard codeStream.next discard codeStream.next
discard codeStream.next discard codeStream.next
check(codeStream.next == Op.STOP) check(codeStream.next == Op.STOP)
# Seek has been dommented out for future deletion # Seek has been dommented out for future deletion
# test "seek reverts to original position on exit": # test "seek reverts to original position on exit":
# var codeStream = newCodeStream("\x01\x02\x30") # var codeStream = CodeStream.init("\x01\x02\x30")
# check(codeStream.pc == 0) # check(codeStream.pc == 0)
# codeStream.seek(1): # codeStream.seek(1):
# check(codeStream.pc == 1) # check(codeStream.pc == 1)
@ -56,13 +56,13 @@ proc codeStreamMain*() =
# check(codeStream.peek == Op.ADD) # check(codeStream.peek == Op.ADD)
test "[] returns opcode": test "[] returns opcode":
let codeStream = newCodeStream("\x01\x02\x30") let codeStream = CodeStream.init("\x01\x02\x30")
check(codeStream[0] == Op.ADD) check(codeStream[0] == Op.ADD)
check(codeStream[1] == Op.MUL) check(codeStream[1] == Op.MUL)
check(codeStream[2] == Op.ADDRESS) check(codeStream[2] == Op.ADDRESS)
test "isValidOpcode invalidates after PUSHXX": test "isValidOpcode invalidates after PUSHXX":
var codeStream = newCodeStream("\x02\x60\x02\x04") var codeStream = CodeStream.init("\x02\x60\x02\x04")
check(codeStream.isValidOpcode(0)) check(codeStream.isValidOpcode(0))
check(codeStream.isValidOpcode(1)) check(codeStream.isValidOpcode(1))
check(not codeStream.isValidOpcode(2)) check(not codeStream.isValidOpcode(2))
@ -71,7 +71,7 @@ proc codeStreamMain*() =
test "isValidOpcode 0": test "isValidOpcode 0":
var codeStream = newCodeStream(@[2.byte, 3.byte, 0x72.byte].concat(repeat(4.byte, 32)).concat(@[5.byte])) var codeStream = CodeStream.init(@[2.byte, 3.byte, 0x72.byte].concat(repeat(4.byte, 32)).concat(@[5.byte]))
# valid: 0 - 2 :: 22 - 35 # valid: 0 - 2 :: 22 - 35
# invalid: 3-21 (PUSH19) :: 36+ (too long) # invalid: 3-21 (PUSH19) :: 36+ (too long)
check(codeStream.isValidOpcode(0)) check(codeStream.isValidOpcode(0))
@ -86,7 +86,7 @@ proc codeStreamMain*() =
test "isValidOpcode 1": test "isValidOpcode 1":
let test = @[2.byte, 3.byte, 0x7d.byte].concat(repeat(4.byte, 32)).concat(@[5.byte, 0x7e.byte]).concat(repeat(4.byte, 35)).concat(@[1.byte, 0x61.byte, 1.byte, 1.byte, 1.byte]) let test = @[2.byte, 3.byte, 0x7d.byte].concat(repeat(4.byte, 32)).concat(@[5.byte, 0x7e.byte]).concat(repeat(4.byte, 35)).concat(@[1.byte, 0x61.byte, 1.byte, 1.byte, 1.byte])
var codeStream = newCodeStream(test) var codeStream = CodeStream.init(test)
# valid: 0 - 2 :: 33 - 36 :: 68 - 73 :: 76 # valid: 0 - 2 :: 33 - 36 :: 68 - 73 :: 76
# invalid: 3 - 32 (PUSH30) :: 37 - 67 (PUSH31) :: 74, 75 (PUSH2) :: 77+ (too long) # invalid: 3 - 32 (PUSH30) :: 37 - 67 (PUSH31) :: 74, 75 (PUSH2) :: 77+ (too long)
check(codeStream.isValidOpcode(0)) check(codeStream.isValidOpcode(0))
@ -109,7 +109,7 @@ proc codeStreamMain*() =
test "right number of bytes invalidates": test "right number of bytes invalidates":
var codeStream = newCodeStream("\x02\x03\x60\x02\x02") var codeStream = CodeStream.init("\x02\x03\x60\x02\x02")
check(codeStream.isValidOpcode(0)) check(codeStream.isValidOpcode(0))
check(codeStream.isValidOpcode(1)) check(codeStream.isValidOpcode(1))
check(codeStream.isValidOpcode(2)) check(codeStream.isValidOpcode(2))

View File

@ -128,7 +128,7 @@ proc verifyStateDB*(wantedState: JsonNode, stateDB: ReadOnlyStateDB) =
wantedBalance = UInt256.fromHex accountData{"balance"}.getStr wantedBalance = UInt256.fromHex accountData{"balance"}.getStr
wantedNonce = accountData{"nonce"}.getHexadecimalInt.AccountNonce wantedNonce = accountData{"nonce"}.getHexadecimalInt.AccountNonce
actualCode = stateDB.getCode(account) actualCode = stateDB.getCode(account).bytes()
actualBalance = stateDB.getBalance(account) actualBalance = stateDB.getBalance(account)
actualNonce = stateDB.getNonce(account) actualNonce = stateDB.getNonce(account)

View File

@ -106,7 +106,7 @@ proc envToHeader(env: EnvStruct): BlockHeader =
proc postState(db: LedgerRef, alloc: var GenesisAlloc) = proc postState(db: LedgerRef, alloc: var GenesisAlloc) =
for accAddr in db.addresses(): for accAddr in db.addresses():
var acc = GenesisAccount( var acc = GenesisAccount(
code: db.getCode(accAddr), code: db.getCode(accAddr).bytes(),
balance: db.getBalance(accAddr), balance: db.getBalance(accAddr),
nonce: db.getNonce(accAddr) nonce: db.getNonce(accAddr)
) )

2
vendor/nim-evmc vendored

@ -1 +1 @@
Subproject commit 86d22a026b0aa07c07b3afd7d91ca475e0eae12a Subproject commit 6e261148565a311536b1a29f1568e8c4470baf9d