nimbus-eth1/nimbus/evm/tracer/json_tracer.nim
Jacek Sieka f6be4bd0ec
avoid initTable (#2328)
`initTable` is obsolete since nim 0.19 and can introduce significant
memory overhead while providing no benefit (since the table will be
grown to the default initial size on first use anyway).

In particular, aristo layers will not necessarily use all tables they
initialize, for exampe when many empty accounts are being created.
2024-06-10 11:05:30 +02:00

242 lines
7.0 KiB
Nim

# Nimbus
# Copyright (c) 2023-2024 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
import
std/[json, sets, streams, strutils],
eth/common/eth_types,
eth/rlp,
stew/byteutils,
results,
chronicles,
".."/[types, memory, stack],
../interpreter/op_codes,
../../db/ledger
type
JsonTracer* = ref object of TracerRef
stream: Stream
pretty: bool
gas: GasInt
pc: int
stack: JsonNode
storageKeys: seq[HashSet[UInt256]]
index: int
node: JsonNode
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 encodeHex(x: SomeInteger): JsonNode =
%("0x" & x.toHex.stripLeadingZeros.toLowerAscii)
proc encodeHex(x: UInt256): string =
"0x" & x.dumpHex.stripLeadingZeros
proc `%`(x: openArray[byte]): JsonNode =
if x.len == 0:
%("")
else:
%("0x" & x.toHex)
proc writeJson(ctx: JsonTracer, res: JsonNode) =
try:
if ctx.pretty:
ctx.stream.writeLine(res.pretty)
else:
ctx.stream.writeLine($res)
except IOError as ex:
error "JsonTracer writeJson", msg=ex.msg
except OSError as ex:
error "JsonTracer writeJson", msg=ex.msg
proc rememberStorageKey(ctx: JsonTracer, compDepth: int, key: UInt256) =
ctx.storageKeys[compDepth].incl key
iterator storage(ctx: JsonTracer, compDepth: int): UInt256 =
doAssert compDepth >= 0 and compDepth < ctx.storageKeys.len
for key in ctx.storageKeys[compDepth]:
yield key
proc captureOpImpl(ctx: JsonTracer, c: Computation, pc: int,
op: Op, gas: GasInt, refund: GasInt,
rData: openArray[byte], depth: int, error: Opt[string]) {.gcsafe.} =
let
gasCost = ctx.gas - gas
var res = %{
"pc": %(ctx.pc),
"op": %(op.int),
"gas": encodeHex(ctx.gas),
"gasCost": encodeHex(gasCost),
"memSize": %(c.memory.len)
}
if TracerFlags.DisableMemory notin ctx.flags:
let mem = newJArray()
const chunkLen = 32
let numChunks = c.memory.len div chunkLen
for i in 0 ..< numChunks:
let memHex = c.memory.bytes.toOpenArray(i * chunkLen, (i + 1) * chunkLen - 1).toHex()
mem.add(%("0x" & memHex.toLowerAscii))
res["memory"] = mem
if TracerFlags.DisableStack notin ctx.flags:
if ctx.stack.isNil:
res["stack"] = newJArray()
else:
res["stack"] = ctx.stack
if TracerFlags.DisableReturnData notin ctx.flags:
res["returnData"] = %(rData)
if TracerFlags.DisableStorage notin ctx.flags:
var storage = newJObject()
if c.msg.depth < ctx.storageKeys.len:
var stateDB = c.vmState.stateDB
for key in ctx.storage(c.msg.depth):
let value = stateDB.getStorage(c.msg.contractAddress, key)
storage[key.encodeHex] = %(value.encodeHex)
res["storage"] = storage
res["depth"] = %(depth)
res["refund"] = %(refund)
res["opName"] = %(($op).toUpperAscii)
if error.isSome:
res["error"] = %(error.get)
ctx.node = res
proc newJsonTracer*(stream: Stream, flags: set[TracerFlags], pretty: bool): JsonTracer =
JsonTracer(
flags: flags,
stream: stream,
pretty: pretty
)
method capturePrepare*(ctx: JsonTracer, comp: Computation, depth: int) {.gcsafe.} =
if depth >= ctx.storageKeys.len:
let prevLen = ctx.storageKeys.len
ctx.storageKeys.setLen(depth + 1)
for i in prevLen ..< ctx.storageKeys.len - 1:
ctx.storageKeys[i] = HashSet[UInt256]()
ctx.storageKeys[depth] = HashSet[UInt256]()
# Top call frame
method captureStart*(ctx: JsonTracer, comp: Computation,
sender: EthAddress, to: EthAddress,
create: bool, input: openArray[byte],
gasLimit: GasInt, value: UInt256) {.gcsafe.} =
discard
method captureEnd*(ctx: JsonTracer, comp: Computation, output: openArray[byte],
gasUsed: GasInt, error: Opt[string]) {.gcsafe.} =
var res = %{
"output": %(output),
"gasUsed": encodeHex(gasUsed)
}
if error.isSome:
res["error"] = %(error.get())
ctx.writeJson(res)
# Opcode level
method captureOpStart*(ctx: JsonTracer, c: Computation,
fixed: bool, pc: int, op: Op, gas: GasInt,
depth: int): int {.gcsafe.} =
ctx.gas = gas
ctx.pc = pc
if TracerFlags.DisableStack notin ctx.flags:
ctx.stack = newJArray()
for v in c.stack:
ctx.stack.add(%(v.encodeHex))
if TracerFlags.DisableStorage notin ctx.flags and op == Sstore:
try:
if c.stack.len > 1:
ctx.rememberStorageKey(c.msg.depth,
c.stack[^1, UInt256].expect("stack constains more than 2 elements"))
except ValueError as ex:
error "JsonTracer captureOpStart", msg=ex.msg
try:
ctx.captureOpImpl(c, pc, op, 0, 0, [], depth, Opt.none(string))
except RlpError as ex:
error "JsonTracer captureOpStart", msg=ex.msg
# make sure captureOpEnd get the right opIndex
result = ctx.index
inc ctx.index
method captureGasCost*(ctx: JsonTracer, comp: Computation,
fixed: bool, op: Op, gasCost: GasInt, gasRemaining: GasInt,
depth: int) {.gcsafe.} =
doAssert(ctx.node.isNil.not)
let res = ctx.node
res["gasCost"] = encodeHex(gasCost)
if gasCost <= gasRemaining and not fixed:
ctx.writeJson(res)
ctx.node = nil
# else:
# OOG will be handled by captureFault
# opcode with fixed gasCost will be handled by captureOpEnd
method captureOpEnd*(ctx: JsonTracer, comp: Computation,
fixed: bool, pc: int, op: Op, gas: GasInt, refund: GasInt,
rData: openArray[byte],
depth: int, opIndex: int) {.gcsafe.} =
if fixed:
doAssert(ctx.node.isNil.not)
let res = ctx.node
res["refund"] = %(refund)
if TracerFlags.DisableReturnData notin ctx.flags:
res["returnData"] = %(rData)
ctx.writeJson(res)
ctx.node = nil
return
method captureFault*(ctx: JsonTracer, comp: Computation,
fixed: bool, pc: int, op: Op, gas: GasInt, refund: GasInt,
rData: openArray[byte],
depth: int, error: Opt[string]) {.gcsafe.} =
if ctx.node.isNil.not:
let res = ctx.node
res["refund"] = %(refund)
if TracerFlags.DisableReturnData notin ctx.flags:
res["returnData"] = %(rData)
if error.isSome:
res["error"] = %(error.get)
ctx.writeJson(res)
ctx.node = nil
return
try:
ctx.captureOpImpl(comp, pc, op, gas, refund, rData, depth, error)
doAssert(ctx.node.isNil.not)
ctx.writeJson(ctx.node)
ctx.node = nil
except RlpError as ex:
error "JsonTracer captureOpFault", msg=ex.msg
proc close*(ctx: JsonTracer) =
ctx.stream.close()