nimbus-eth1/tools/t8n/transition.nim

515 lines
17 KiB
Nim

# Nimbus
# Copyright (c) 2022-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
std/[json, strutils, tables, os, streams],
eth/[rlp, trie, eip1559],
stint, results,
"."/[config, types, helpers],
../common/state_clearing,
../../nimbus/[evm/types, evm/state, transaction],
../../nimbus/common/common,
../../nimbus/db/ledger,
../../nimbus/utils/utils,
../../nimbus/core/pow/difficulty,
../../nimbus/core/dao,
../../nimbus/core/executor/[process_transaction, executor_helpers],
../../nimbus/core/eip4844,
../../nimbus/evm/tracer/json_tracer
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: Opt[seq[Withdrawal]]): Opt[Hash256] =
if w.isNone:
return Opt.none(Hash256)
Opt.some calcWithdrawalsRoot(w.get)
proc envToHeader(env: EnvStruct): BlockHeader =
BlockHeader(
coinbase : env.currentCoinbase,
difficulty : env.currentDifficulty.get(0.u256),
mixHash : env.currentRandom.get(Hash256()),
number : env.currentNumber,
gasLimit : env.currentGasLimit,
timestamp : env.currentTimestamp,
stateRoot : emptyRlpHash,
baseFeePerGas : env.currentBaseFee,
withdrawalsRoot: env.withdrawals.calcWithdrawalsRoot(),
blobGasUsed : env.currentBlobGasUsed,
excessBlobGas : env.currentExcessBlobGas,
)
proc postState(db: LedgerRef, alloc: var GenesisAlloc) =
for accAddr in db.addresses():
var acc = GenesisAccount(
code: db.getCode(accAddr).bytes(),
balance: db.getBalance(accAddr),
nonce: db.getNonce(accAddr)
)
for k, v in db.storage(accAddr):
acc.storage[k] = v
alloc[accAddr] = acc
proc genAddress(tx: Transaction, sender: EthAddress): EthAddress =
if tx.to.isNone:
result = generateAddress(sender, tx.nonce)
proc toTxReceipt(rec: Receipt,
tx: Transaction,
sender: EthAddress,
txIndex: int,
gasUsed: GasInt): TxReceipt =
let contractAddress = genAddress(tx, sender)
TxReceipt(
txType: tx.txType,
root: if rec.isHash: rec.hash else: Hash256(),
status: rec.status,
cumulativeGasUsed: rec.cumulativeGasUsed,
logsBloom: rec.logsBloom,
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)
proc defaultTraceStreamFilename(conf: T8NConf,
txIndex: int,
txHash: Hash256): (string, string) =
let
txHash = "0x" & toLowerAscii($txHash)
baseDir = if conf.outputBaseDir.len > 0:
conf.outputBaseDir
else:
"."
fName = "$1/trace-$2-$3.jsonl" % [baseDir, $txIndex, txHash]
(baseDir, fName)
proc defaultTraceStream(conf: T8NConf, txIndex: int, txHash: Hash256): Stream =
let (baseDir, fName) = defaultTraceStreamFilename(conf, txIndex, txHash)
createDir(baseDir)
newFileStream(fName, fmWrite)
proc traceToFileStream(path: string, txIndex: int): Stream =
# replace whatever `.ext` to `-${txIndex}.jsonl`
let
file = path.splitFile
fName = "$1/$2-$3.jsonl" % [file.dir, file.name, $txIndex]
createDir(file.dir)
newFileStream(fName, fmWrite)
proc setupTrace(conf: T8NConf, txIndex: int, txHash: Hash256, vmState: BaseVMState) =
var tracerFlags = {
TracerFlags.DisableMemory,
TracerFlags.DisableStorage,
TracerFlags.DisableState,
TracerFlags.DisableStateDiff,
TracerFlags.DisableReturnData
}
if conf.traceMemory: tracerFlags.excl TracerFlags.DisableMemory
if conf.traceNostack: tracerFlags.incl TracerFlags.DisableStack
if conf.traceReturnData: tracerFlags.excl TracerFlags.DisableReturnData
let traceMode = conf.traceEnabled.get
let stream = if traceMode == "stdout":
newFileStream(stdout)
elif traceMode == "stderr":
newFileStream(stderr)
elif traceMode.len > 0:
traceToFileStream(traceMode, txIndex)
else:
defaultTraceStream(conf, txIndex, txHash)
if stream.isNil:
let traceLoc =
if traceMode.len > 0:
traceMode
else:
defaultTraceStreamFilename(conf, txIndex, txHash)[1]
raise newError(ErrorConfig, "Unable to open tracer stream: " & traceLoc)
vmState.tracer = newJsonTracer(stream, tracerFlags, false)
proc closeTrace(vmState: BaseVMState) =
let tracer = JsonTracer(vmState.tracer)
if tracer.isNil.not:
tracer.close()
proc exec(ctx: var TransContext,
vmState: BaseVMState,
stateReward: Option[UInt256],
header: BlockHeader,
conf: T8NConf): 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
if ctx.env.parentBeaconBlockRoot.isSome:
vmState.processBeaconBlockRoot(ctx.env.parentBeaconBlockRoot.get).isOkOr:
raise newError(ErrorConfig, error)
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
if conf.traceEnabled.isSome:
setupTrace(conf, txIndex, rlpHash(tx), vmState)
let rc = vmState.processTransaction(tx, sender, header)
if conf.traceEnabled.isSome:
closeTrace(vmState)
if rc.isErr:
rejected.add RejectedTx(
index: txIndex,
error: rc.error
)
continue
let gasUsed = rc.get()
let rec = vmState.makeReceipt(tx.txType)
vmState.receipts.add rec
receipts.add toTxReceipt(
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.weiAmount)
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: calcReceiptsRoot(vmState.receipts),
logsHash : calcLogsHash(vmState.receipts),
logsBloom : 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
)
if fork >= FkCancun:
result.result.blobGasUsed = Opt.some vmState.blobGasUsed
if ctx.env.currentExcessBlobGas.isSome:
result.result.currentExcessBlobGas = ctx.env.currentExcessBlobGas
elif ctx.env.parentExcessBlobGas.isSome and ctx.env.parentBlobGasUsed.isSome:
result.result.currentExcessBlobGas = Opt.some calcExcessBlobGas(vmState.parent)
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: LedgerRef, 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 =
# we can't raise exception here, it'll mess with EVM exception handler.
# so, store the exception for later using `hashError`
var h = Hash256()
if vmState.blockHashes.len == 0:
vmState.hashError = "getAncestorHash(" &
$blockNumber & ") invoked, no blockhashes provided"
return h
vmState.blockHashes.withValue(blockNumber, val) do:
h = val[]
do:
vmState.hashError = "getAncestorHash(" &
$blockNumber & ") invoked, blockhash for that block not provided"
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:
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(newCoreDbRef DefaultDbMemory, config)
# 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,
number: ctx.env.currentNumber - 1'u64,
blobGasUsed: ctx.env.parentBlobGasUsed,
excessBlobGas: ctx.env.parentExcessBlobGas,
)
# Sanity check, to not `panic` in state_transition
if com.isLondonOrLater(ctx.env.currentNumber):
if ctx.env.currentBaseFee.isSome:
# Already set, currentBaseFee has precedent over parentBaseFee.
discard
elif ctx.env.parentBaseFee.isSome:
ctx.env.currentBaseFee = Opt.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.isCancunOrLater(ctx.env.currentTimestamp):
if ctx.env.parentBeaconBlockRoot.isNone:
raise newError(ErrorConfig, "Cancun config but missing 'parentBeaconBlockRoot' in env section")
let res = loadKzgTrustedSetup()
if res.isErr:
raise newError(ErrorConfig, res.error)
else:
# un-set it if it has been set too early
ctx.env.parentBeaconBlockRoot = Opt.none(Hash256)
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 = Opt.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.BlockNumber:
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 = Opt.some(calcDifficulty(com,
ctx.env.currentTimestamp, parent))
# Calculate the excessBlobGas
if ctx.env.currentExcessBlobGas.isNone:
# If it is not explicitly defined, but we have the parent values, we try
# to calculate it ourselves.
if parent.excessBlobGas.isSome and parent.blobGasUsed.isSome:
ctx.env.currentExcessBlobGas = Opt.some calcExcessBlobGas(parent)
let header = envToHeader(ctx.env)
let vmState = TestVMState(
blockHashes: ctx.env.blockHashes,
hashError: ""
)
vmState.init(
parent = parent,
header = header,
com = com
)
vmState.mutateStateDB:
db.setupAlloc(ctx.alloc)
db.persist(clearEmptyAccount = false)
let res = exec(ctx, vmState, conf.stateReward, header, conf)
if vmState.hashError.len > 0:
raise newError(ErrorMissingBlockhash, vmState.hashError)
ctx.dispatchOutput(conf, res)