nimbus-eth1/nimbus/evm/tracer/json_tracer.nim

231 lines
6.7 KiB
Nim
Raw Normal View History

# Nimbus
# Copyright (c) 2023 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,
chronicles,
".."/[types, memory, stack],
../interpreter/op_codes,
../../db/accounts_cache,
../../errors
type
JsonTracer* = ref object of TracerRef
stream: Stream
pretty: bool
gas: GasInt
pc: int
stack: JsonNode
storageKeys: seq[HashSet[UInt256]]
index: int
callFamilyNode: JsonNode
const
callFamily = [
Create,
Create2,
Call,
CallCode,
DelegateCall,
StaticCall,
]
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 `%`(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: Option[string]) {.gcsafe.} =
let
gasCost = ctx.gas - gas
var res = %{
"pc": %(ctx.pc),
"op": %(op.int),
"gas": encodeHexInt(ctx.gas),
"gasCost": encodeHexInt(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["0x" & key.dumpHex.stripLeadingZeros] =
%("0x" & value.dumpHex.stripLeadingZeros)
res["storage"] = storage
res["depth"] = %(depth)
res["refund"] = %(refund)
res["opName"] = %(($op).toUpperAscii)
if error.isSome:
res["error"] = %(error.get)
if op in callFamily:
ctx.callFamilyNode = res
else:
ctx.writeJson(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] = initHashSet[UInt256]()
ctx.storageKeys[depth] = initHashSet[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: Option[string]) {.gcsafe.} =
var res = %{
"output": %(output),
"gasUsed": encodeHexInt(gasUsed)
}
if error.isSome:
res["error"] = %(error.get())
ctx.writeJson(res)
# Opcode level
method captureOpStart*(ctx: JsonTracer, c: Computation, 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.values:
ctx.stack.add(%("0x" & v.dumpHex.stripLeadingZeros))
if TracerFlags.DisableStorage notin ctx.flags and op == SSTORE:
try:
if c.stack.values.len > 1:
ctx.rememberStorageKey(c.msg.depth, c.stack[^1, UInt256])
except InsufficientStack as ex:
error "JsonTracer captureOpStart", msg=ex.msg
except ValueError as ex:
error "JsonTracer captureOpStart", msg=ex.msg
if op in callFamily:
try:
ctx.captureOpImpl(c, pc, op, 0, 0, [], depth, none(string))
except RlpError as ex:
error "JsonTracer captureOpEnd", msg=ex.msg
# make sure captureOpEnd get the right opIndex
result = ctx.index
inc ctx.index
method callFamilyGas*(ctx: JsonTracer, comp: Computation,
op: Op, gas: GasInt,
depth: int) {.gcsafe.} =
doAssert(op in callFamily)
doAssert(ctx.callFamilyNode.isNil.not)
let res = ctx.callFamilyNode
res["gasCost"] = encodeHexInt(gas)
ctx.writeJson(res)
method captureOpEnd*(ctx: JsonTracer, comp: Computation, pc: int,
op: Op, gas: GasInt, refund: GasInt,
rData: openArray[byte],
depth: int, opIndex: int) {.gcsafe.} =
if op in callFamily:
# call family opcode is processed in captureOpStart
return
try:
ctx.captureOpImpl(comp, pc, op, gas, refund, rData, depth, none(string))
except RlpError as ex:
error "JsonTracer captureOpEnd", msg=ex.msg
method captureFault*(ctx: JsonTracer, comp: Computation, pc: int,
op: Op, gas: GasInt, refund: GasInt,
rData: openArray[byte],
depth: int, error: Option[string]) {.gcsafe.} =
try:
ctx.captureOpImpl(comp, pc, op, gas, refund, rData, depth, error)
except RlpError as ex:
error "JsonTracer captureOpEnd", msg=ex.msg
proc close*(ctx: JsonTracer) =
ctx.stream.close()