mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-10 12:26:02 +00:00
a31db89e0e
t8n: a silly bug contract address generator, should use original tx nonce instead of read the nonce from sender address in state db. Although in EVM contract address generated by reading nonce from state db is correct, outside EVM that nonce value might have been modified, thus generating incorrect contract address. accounts cache: when clearing account storage, the originalValue cache is not cleared, only the storageRoot set to empty storage root, this will cause getStorage and getCommitedStorage return wrong value if the originalValue cache contains old value.
471 lines
15 KiB
Nim
471 lines
15 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2022 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
|
|
std/[json, strutils, times, tables, os, math, streams],
|
|
eth/[rlp, trie, eip1559],
|
|
stint, stew/results,
|
|
"."/[config, types, helpers],
|
|
../common/state_clearing,
|
|
../../nimbus/[vm_types, vm_state, transaction],
|
|
../../nimbus/common/common,
|
|
../../nimbus/db/accounts_cache,
|
|
../../nimbus/utils/utils,
|
|
../../nimbus/core/pow/difficulty,
|
|
../../nimbus/core/dao,
|
|
../../nimbus/core/executor/[process_transaction, executor_helpers]
|
|
|
|
import stew/byteutils
|
|
const
|
|
wrapExceptionEnabled* {.booldefine.} = true
|
|
stdinSelector = "stdin"
|
|
|
|
type
|
|
Dispatch = object
|
|
stdout: JsonNode
|
|
stderr: JsonNode
|
|
|
|
ExecOutput = object
|
|
result: ExecutionResult
|
|
alloc: GenesisAlloc
|
|
|
|
TestVMState = ref object of BaseVMState
|
|
blockHashes: Table[uint64, Hash256]
|
|
hashError: string
|
|
|
|
proc init(_: type Dispatch): Dispatch =
|
|
result.stdout = newJObject()
|
|
result.stderr = newJObject()
|
|
|
|
proc dispatch(dis: var Dispatch, baseDir, fName, name: string, obj: JsonNode) =
|
|
case fName
|
|
of "stdout":
|
|
dis.stdout[name] = obj
|
|
of "stderr":
|
|
dis.stderr[name] = obj
|
|
of "":
|
|
# don't save
|
|
discard
|
|
else:
|
|
# save to file
|
|
let path = if baseDir.len > 0:
|
|
baseDir / fName
|
|
else:
|
|
fName
|
|
writeFile(path, obj.pretty)
|
|
|
|
proc dispatchOutput(ctx: var TransContext, conf: T8NConf, res: ExecOutput) =
|
|
var dis = Dispatch.init()
|
|
createDir(conf.outputBaseDir)
|
|
|
|
dis.dispatch(conf.outputBaseDir, conf.outputAlloc, "alloc", @@(res.alloc))
|
|
dis.dispatch(conf.outputBaseDir, conf.outputResult, "result", @@(res.result))
|
|
|
|
let chainId = conf.stateChainId.ChainId
|
|
let txList = ctx.txList(chainId)
|
|
|
|
let body = @@(rlp.encode(txList))
|
|
dis.dispatch(conf.outputBaseDir, conf.outputBody, "body", body)
|
|
|
|
if dis.stdout.len > 0:
|
|
stdout.write(dis.stdout.pretty)
|
|
stdout.write("\n")
|
|
|
|
if dis.stderr.len > 0:
|
|
stderr.write(dis.stderr.pretty)
|
|
stderr.write("\n")
|
|
|
|
proc calcWithdrawalsRoot(w: Option[seq[Withdrawal]]): Option[Hash256] =
|
|
if w.isNone:
|
|
return none(Hash256)
|
|
calcWithdrawalsRoot(w.get).some()
|
|
|
|
proc envToHeader(env: EnvStruct): BlockHeader =
|
|
BlockHeader(
|
|
coinbase : env.currentCoinbase,
|
|
difficulty : env.currentDifficulty.get(0.u256),
|
|
mixDigest : env.currentRandom.get(Hash256()),
|
|
blockNumber: env.currentNumber,
|
|
gasLimit : env.currentGasLimit,
|
|
timestamp : env.currentTimestamp,
|
|
stateRoot : emptyRlpHash,
|
|
fee : env.currentBaseFee,
|
|
withdrawalsRoot: env.withdrawals.calcWithdrawalsRoot()
|
|
)
|
|
|
|
proc postState(db: AccountsCache, alloc: var GenesisAlloc) =
|
|
for accAddr in db.addresses():
|
|
var acc = GenesisAccount(
|
|
code: db.getCode(accAddr),
|
|
balance: db.getBalance(accAddr),
|
|
nonce: db.getNonce(accAddr)
|
|
)
|
|
|
|
for k, v in db.storage(accAddr):
|
|
acc.storage[k] = v
|
|
alloc[accAddr] = acc
|
|
|
|
proc genAddress(vmState: BaseVMState, tx: Transaction, sender: EthAddress): EthAddress =
|
|
if tx.to.isNone:
|
|
result = generateAddress(sender, tx.nonce)
|
|
|
|
proc toTxReceipt(vmState: BaseVMState,
|
|
rec: Receipt,
|
|
tx: Transaction,
|
|
sender: EthAddress,
|
|
txIndex: int,
|
|
gasUsed: GasInt): TxReceipt =
|
|
|
|
let contractAddress = genAddress(vmState, tx, sender)
|
|
TxReceipt(
|
|
txType: tx.txType,
|
|
root: if rec.isHash: rec.hash else: Hash256(),
|
|
status: rec.status,
|
|
cumulativeGasUsed: rec.cumulativeGasUsed,
|
|
logsBloom: rec.bloom,
|
|
logs: rec.logs,
|
|
transactionHash: rlpHash(tx),
|
|
contractAddress: contractAddress,
|
|
gasUsed: gasUsed,
|
|
blockHash: Hash256(),
|
|
transactionIndex: txIndex
|
|
)
|
|
|
|
proc calcLogsHash(receipts: openArray[Receipt]): Hash256 =
|
|
var logs: seq[Log]
|
|
for rec in receipts:
|
|
logs.add rec.logs
|
|
rlpHash(logs)
|
|
|
|
template stripLeadingZeros(value: string): string =
|
|
var cidx = 0
|
|
# ignore the last character so we retain '0' on zero value
|
|
while cidx < value.len - 1 and value[cidx] == '0':
|
|
cidx.inc
|
|
value[cidx .. ^1]
|
|
|
|
proc encodeHexInt(x: SomeInteger): JsonNode =
|
|
%("0x" & x.toHex.stripLeadingZeros.toLowerAscii)
|
|
|
|
proc toHex(x: Hash256): string =
|
|
"0x" & x.data.toHex
|
|
|
|
proc dumpTrace(txIndex: int, txHash: Hash256, vmState: BaseVMstate) =
|
|
let txHash = "0x" & toLowerAscii($txHash)
|
|
let fName = "trace-$1-$2.jsonl" % [$txIndex, txHash]
|
|
let trace = vmState.getTracingResult()
|
|
var s = newFileStream(fName, fmWrite)
|
|
|
|
trace["gasUsed"] = encodeHexInt(vmState.tracerGasUsed)
|
|
trace.delete("gas")
|
|
let stateRoot = %{
|
|
"stateRoot": %(vmState.readOnlyStateDB.rootHash.toHex)
|
|
}
|
|
|
|
let logs = trace["structLogs"]
|
|
trace.delete("structLogs")
|
|
for x in logs:
|
|
if "error" in x:
|
|
trace["error"] = x["error"]
|
|
x.delete("error")
|
|
s.writeLine($x)
|
|
|
|
s.writeLine($trace)
|
|
s.writeLine($stateRoot)
|
|
s.close()
|
|
|
|
func gwei(n: uint64): UInt256 =
|
|
n.u256 * (10 ^ 9).u256
|
|
|
|
proc exec(ctx: var TransContext,
|
|
vmState: BaseVMState,
|
|
stateReward: Option[UInt256],
|
|
header: BlockHeader): ExecOutput =
|
|
|
|
let txList = ctx.parseTxs(vmState.com.chainId)
|
|
|
|
var
|
|
receipts = newSeqOfCap[TxReceipt](txList.len)
|
|
rejected = newSeq[RejectedTx]()
|
|
includedTx = newSeq[Transaction]()
|
|
|
|
if vmState.com.daoForkSupport and
|
|
vmState.com.daoForkBlock.get == vmState.blockNumber:
|
|
vmState.mutateStateDB:
|
|
db.applyDAOHardFork()
|
|
|
|
vmState.receipts = newSeqOfCap[Receipt](txList.len)
|
|
vmState.cumulativeGasUsed = 0
|
|
|
|
for txIndex, txRes in txList:
|
|
if txRes.isErr:
|
|
rejected.add RejectedTx(
|
|
index: txIndex,
|
|
error: txRes.error
|
|
)
|
|
continue
|
|
|
|
let tx = txRes.get
|
|
var sender: EthAddress
|
|
if not tx.getSender(sender):
|
|
rejected.add RejectedTx(
|
|
index: txIndex,
|
|
error: "Could not get sender"
|
|
)
|
|
continue
|
|
|
|
let rc = vmState.processTransaction(tx, sender, header)
|
|
|
|
if vmState.tracingEnabled:
|
|
dumpTrace(txIndex, rlpHash(tx), vmState)
|
|
|
|
if rc.isErr:
|
|
rejected.add RejectedTx(
|
|
index: txIndex,
|
|
error: "processTransaction failed"
|
|
)
|
|
continue
|
|
|
|
let gasUsed = rc.get()
|
|
let rec = vmState.makeReceipt(tx.txType)
|
|
vmState.receipts.add rec
|
|
receipts.add toTxReceipt(
|
|
vmState, rec,
|
|
tx, sender, txIndex, gasUsed
|
|
)
|
|
includedTx.add tx
|
|
|
|
# Add mining reward? (-1 means rewards are disabled)
|
|
if stateReward.isSome and stateReward.get >= 0:
|
|
# Add mining reward. The mining reward may be `0`, which only makes a difference in the cases
|
|
# where
|
|
# - the coinbase suicided, or
|
|
# - there are only 'bad' transactions, which aren't executed. In those cases,
|
|
# the coinbase gets no txfee, so isn't created, and thus needs to be touched
|
|
let blockReward = stateReward.get()
|
|
var mainReward = blockReward
|
|
for uncle in ctx.env.ommers:
|
|
var uncleReward = 8.u256 - uncle.delta.u256
|
|
uncleReward = uncleReward * blockReward
|
|
uncleReward = uncleReward div 8.u256
|
|
vmState.mutateStateDB:
|
|
db.addBalance(uncle.address, uncleReward)
|
|
mainReward += blockReward div 32.u256
|
|
|
|
vmState.mutateStateDB:
|
|
db.addBalance(ctx.env.currentCoinbase, mainReward)
|
|
|
|
if ctx.env.withdrawals.isSome:
|
|
for withdrawal in ctx.env.withdrawals.get:
|
|
vmState.stateDB.addBalance(withdrawal.address, withdrawal.amount.gwei)
|
|
|
|
let miner = ctx.env.currentCoinbase
|
|
let fork = vmState.com.toEVMFork
|
|
coinbaseStateClearing(vmState, miner, fork, stateReward.isSome())
|
|
|
|
let stateDB = vmState.stateDB
|
|
stateDB.postState(result.alloc)
|
|
result.result = ExecutionResult(
|
|
stateRoot : stateDB.rootHash,
|
|
txRoot : includedTx.calcTxRoot,
|
|
receiptsRoot: calcReceiptRoot(vmState.receipts),
|
|
logsHash : calcLogsHash(vmState.receipts),
|
|
bloom : createBloom(vmState.receipts),
|
|
receipts : system.move(receipts),
|
|
rejected : system.move(rejected),
|
|
# geth using both vmContext.Difficulty and vmContext.Random
|
|
# therefore we cannot use vmState.difficulty
|
|
currentDifficulty: ctx.env.currentDifficulty,
|
|
gasUsed : vmState.cumulativeGasUsed,
|
|
currentBaseFee: ctx.env.currentBaseFee,
|
|
withdrawalsRoot: header.withdrawalsRoot
|
|
)
|
|
|
|
template wrapException(body: untyped) =
|
|
when wrapExceptionEnabled:
|
|
try:
|
|
body
|
|
except IOError as e:
|
|
raise newError(ErrorIO, e.msg)
|
|
except RlpError as e:
|
|
raise newError(ErrorRlp, e.msg)
|
|
except ValueError as e:
|
|
raise newError(ErrorJson, e.msg)
|
|
else:
|
|
body
|
|
|
|
proc setupAlloc(stateDB: AccountsCache, alloc: GenesisAlloc) =
|
|
for accAddr, acc in alloc:
|
|
stateDB.setNonce(accAddr, acc.nonce)
|
|
stateDB.setCode(accAddr, acc.code)
|
|
stateDB.setBalance(accAddr, acc.balance)
|
|
|
|
for slot, value in acc.storage:
|
|
stateDB.setStorage(accAddr, slot, value)
|
|
|
|
method getAncestorHash(vmState: TestVMState; blockNumber: BlockNumber): Hash256 {.gcsafe.} =
|
|
# we can't raise exception here, it'll mess with EVM exception handler.
|
|
# so, store the exception for later using `hashError`
|
|
let num = blockNumber.truncate(uint64)
|
|
var h = Hash256()
|
|
if vmState.blockHashes.len == 0:
|
|
vmState.hashError = "getAncestorHash($1) invoked, no blockhashes provided" % [$num]
|
|
return h
|
|
|
|
vmState.blockHashes.withValue(num, val) do:
|
|
h = val[]
|
|
do:
|
|
vmState.hashError = "getAncestorHash($1) invoked, blockhash for that block not provided" % [$num]
|
|
|
|
return h
|
|
|
|
proc parseChainConfig(network: string): ChainConfig =
|
|
try:
|
|
result = getChainConfig(network)
|
|
except ValueError as e:
|
|
raise newError(ErrorConfig, e.msg)
|
|
|
|
proc calcBaseFee(env: EnvStruct): UInt256 =
|
|
if env.parentGasUsed.isNone:
|
|
raise newError(ErrorConfig,
|
|
"'parentBaseFee' exists but missing 'parentGasUsed' in env section")
|
|
|
|
if env.parentGasLimit.isNone:
|
|
raise newError(ErrorConfig,
|
|
"'parentBaseFee' exists but missing 'parentGasLimit' in env section")
|
|
|
|
calcEip1599BaseFee(
|
|
env.parentGasLimit.get,
|
|
env.parentGasUsed.get,
|
|
env.parentBaseFee.get)
|
|
|
|
proc transitionAction*(ctx: var TransContext, conf: T8NConf) =
|
|
wrapException:
|
|
var tracerFlags = {
|
|
TracerFlags.DisableMemory,
|
|
TracerFlags.DisableStorage,
|
|
TracerFlags.DisableState,
|
|
TracerFlags.DisableStateDiff,
|
|
TracerFlags.DisableReturnData,
|
|
TracerFlags.GethCompatibility
|
|
}
|
|
|
|
if conf.traceEnabled:
|
|
tracerFlags.incl TracerFlags.EnableTracing
|
|
if conf.traceMemory: tracerFlags.excl TracerFlags.DisableMemory
|
|
if conf.traceNostack: tracerFlags.incl TracerFlags.DisableStack
|
|
if conf.traceReturnData: tracerFlags.excl TracerFlags.DisableReturnData
|
|
|
|
if conf.inputAlloc.len == 0 and conf.inputEnv.len == 0 and conf.inputTxs.len == 0:
|
|
raise newError(ErrorConfig, "either one of input is needeed(alloc, txs, or env)")
|
|
|
|
let config = parseChainConfig(conf.stateFork)
|
|
config.chainId = conf.stateChainId.ChainId
|
|
|
|
let com = CommonRef.new(newMemoryDb(), config, pruneTrie = true)
|
|
|
|
# We need to load three things: alloc, env and transactions.
|
|
# May be either in stdin input or in files.
|
|
|
|
if conf.inputAlloc == stdinSelector or
|
|
conf.inputEnv == stdinSelector or
|
|
conf.inputTxs == stdinSelector:
|
|
ctx.parseInputFromStdin()
|
|
|
|
if conf.inputAlloc != stdinSelector and conf.inputAlloc.len > 0:
|
|
let n = json.parseFile(conf.inputAlloc)
|
|
ctx.parseAlloc(n)
|
|
|
|
if conf.inputEnv != stdinSelector and conf.inputEnv.len > 0:
|
|
let n = json.parseFile(conf.inputEnv)
|
|
ctx.parseEnv(n)
|
|
|
|
if conf.inputTxs != stdinSelector and conf.inputTxs.len > 0:
|
|
if conf.inputTxs.endsWith(".rlp"):
|
|
let data = readFile(conf.inputTxs)
|
|
ctx.parseTxsRlp(data.strip(chars={'"'}))
|
|
else:
|
|
let n = json.parseFile(conf.inputTxs)
|
|
ctx.parseTxs(n)
|
|
|
|
let uncleHash = if ctx.env.parentUncleHash == Hash256():
|
|
EMPTY_UNCLE_HASH
|
|
else:
|
|
ctx.env.parentUncleHash
|
|
|
|
let parent = BlockHeader(
|
|
stateRoot: emptyRlpHash,
|
|
timestamp: ctx.env.parentTimestamp,
|
|
difficulty: ctx.env.parentDifficulty.get(0.u256),
|
|
ommersHash: uncleHash,
|
|
blockNumber: ctx.env.currentNumber - 1.toBlockNumber
|
|
)
|
|
|
|
# Sanity check, to not `panic` in state_transition
|
|
if com.isLondon(ctx.env.currentNumber):
|
|
if ctx.env.currentBaseFee.isSome:
|
|
# Already set, currentBaseFee has precedent over parentBaseFee.
|
|
discard
|
|
elif ctx.env.parentBaseFee.isSome:
|
|
ctx.env.currentBaseFee = some(calcBaseFee(ctx.env))
|
|
else:
|
|
raise newError(ErrorConfig, "EIP-1559 config but missing 'currentBaseFee' in env section")
|
|
|
|
if com.isShanghaiOrLater(ctx.env.currentTimestamp) and ctx.env.withdrawals.isNone:
|
|
raise newError(ErrorConfig, "Shanghai config but missing 'withdrawals' in env section")
|
|
|
|
if com.forkGTE(MergeFork):
|
|
if ctx.env.currentRandom.isNone:
|
|
raise newError(ErrorConfig, "post-merge requires currentRandom to be defined in env")
|
|
|
|
if ctx.env.currentDifficulty.isSome and ctx.env.currentDifficulty.get() != 0:
|
|
raise newError(ErrorConfig, "post-merge difficulty must be zero (or omitted) in env")
|
|
ctx.env.currentDifficulty = none(DifficultyInt)
|
|
|
|
elif ctx.env.currentDifficulty.isNone:
|
|
if ctx.env.parentDifficulty.isNone:
|
|
raise newError(ErrorConfig, "currentDifficulty was not provided, and cannot be calculated due to missing parentDifficulty")
|
|
|
|
if ctx.env.currentNumber == 0.toBlockNumber:
|
|
raise newError(ErrorConfig, "currentDifficulty needs to be provided for block number 0")
|
|
|
|
if ctx.env.currentTimestamp <= ctx.env.parentTimestamp:
|
|
raise newError(ErrorConfig,
|
|
"currentDifficulty cannot be calculated -- currentTime ($1) needs to be after parent time ($2)" %
|
|
[$ctx.env.currentTimestamp, $ctx.env.parentTimestamp])
|
|
|
|
ctx.env.currentDifficulty = some(calcDifficulty(com,
|
|
ctx.env.currentTimestamp, parent))
|
|
|
|
let header = envToHeader(ctx.env)
|
|
|
|
let vmState = TestVMState(
|
|
blockHashes: ctx.env.blockHashes,
|
|
hashError: ""
|
|
)
|
|
|
|
vmState.init(
|
|
parent = parent,
|
|
header = header,
|
|
com = com,
|
|
tracerFlags = (if conf.traceEnabled: tracerFlags else: {})
|
|
)
|
|
|
|
vmState.mutateStateDB:
|
|
db.setupAlloc(ctx.alloc)
|
|
db.persist(clearEmptyAccount = false, clearCache = false)
|
|
|
|
let res = exec(ctx, vmState, conf.stateReward, header)
|
|
|
|
if vmState.hashError.len > 0:
|
|
raise newError(ErrorMissingBlockhash, vmState.hashError)
|
|
|
|
ctx.dispatchOutput(conf, res)
|