243 lines
7.0 KiB
Nim
243 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,
|
|
chronicles,
|
|
".."/[types, memory, stack],
|
|
../interpreter/op_codes,
|
|
../../db/ledger,
|
|
../../errors
|
|
|
|
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: Option[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] = 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": 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.values:
|
|
ctx.stack.add(%(v.encodeHex))
|
|
|
|
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
|
|
|
|
try:
|
|
ctx.captureOpImpl(c, pc, op, 0, 0, [], depth, 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: Option[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()
|