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

283 lines
6.9 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: int64,
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: int64,
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: int64,
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()