444 lines
13 KiB
Nim
444 lines
13 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],
|
|
stew/byteutils,
|
|
stint,
|
|
eth/[common, rlp, keys],
|
|
../../nimbus/transaction,
|
|
../../nimbus/common/chain_config,
|
|
../common/helpers,
|
|
./types,
|
|
./txpriv
|
|
|
|
export
|
|
helpers
|
|
|
|
proc parseHexOrInt[T](x: string): T =
|
|
when T is UInt256:
|
|
if x.startsWith("0x"):
|
|
UInt256.fromHex(x)
|
|
else:
|
|
parse(x, UInt256, 10)
|
|
else:
|
|
if x.startsWith("0x"):
|
|
fromHex[T](x)
|
|
else:
|
|
parseInt(x).T
|
|
|
|
proc fromJson(T: type EthAddress, n: JsonNode, field: string): EthAddress =
|
|
let x = n[field].getStr()
|
|
var xlen = x.len
|
|
if x.startsWith("0x"):
|
|
xlen = xlen - 2
|
|
if xlen != sizeof(T) * 2:
|
|
raise newError(ErrorJson, "malformed Eth address " & x)
|
|
hexToByteArray(x, sizeof(T))
|
|
|
|
template fromJson(T: type Blob, n: JsonNode, field: string): Blob =
|
|
hexToSeqByte(n[field].getStr())
|
|
|
|
proc fromJson(T: type uint64, n: JsonNode, field: string): uint64 =
|
|
if n[field].kind == JInt:
|
|
n[field].getInt().uint64
|
|
else:
|
|
parseHexOrInt[AccountNonce](n[field].getStr())
|
|
|
|
template fromJson(T: type UInt256, n: JsonNode, field: string): UInt256 =
|
|
parseHexOrInt[UInt256](n[field].getStr())
|
|
|
|
template fromJson(T: type GasInt, n: JsonNode, field: string): GasInt =
|
|
parseHexOrInt[GasInt](n[field].getStr())
|
|
|
|
template fromJson(T: type ChainId, n: JsonNode, field: string): ChainId =
|
|
parseHexOrInt[uint64](n[field].getStr()).ChainId
|
|
|
|
proc fromJson(T: type Hash256, n: JsonNode): Hash256 =
|
|
var num = n.getStr()
|
|
num.removePrefix("0x")
|
|
if num.len < 64:
|
|
num = repeat('0', 64 - num.len) & num
|
|
Hash256(data: hexToByteArray(num, 32))
|
|
|
|
proc fromJson(T: type Hash256, n: JsonNode, field: string): Hash256 =
|
|
fromJson(T, n[field])
|
|
|
|
template fromJson(T: type EthTime, n: JsonNode, field: string): EthTime =
|
|
EthTime(parseHexOrInt[uint64](n[field].getStr()))
|
|
|
|
proc fromJson(T: type AccessList, n: JsonNode, field: string): AccessList =
|
|
let z = n[field]
|
|
if z.kind == JNull:
|
|
return
|
|
|
|
for x in z:
|
|
var ap = AccessPair(
|
|
address: EthAddress.fromJson(x, "address")
|
|
)
|
|
let sks = x["storageKeys"]
|
|
for sk in sks:
|
|
ap.storageKeys.add hexToByteArray(sk.getStr(), 32)
|
|
result.add ap
|
|
|
|
proc fromJson(T: type Ommer, n: JsonNode): Ommer =
|
|
Ommer(
|
|
delta: fromJson(uint64, n, "delta"),
|
|
address: fromJson(EthAddress, n, "address")
|
|
)
|
|
|
|
proc fromJson(T: type Withdrawal, n: JsonNode): Withdrawal =
|
|
Withdrawal(
|
|
index: fromJson(uint64, n, "index"),
|
|
validatorIndex: fromJson(uint64, n, "validatorIndex"),
|
|
address: fromJson(EthAddress, n, "address"),
|
|
amount: fromJson(uint64, n, "amount")
|
|
)
|
|
|
|
proc fromJson(T: type VersionedHashes, n: JsonNode, field: string): VersionedHashes =
|
|
let list = n[field]
|
|
for x in list:
|
|
result.add Hash256.fromJson(x)
|
|
|
|
template `gas=`(tx: var Transaction, x: GasInt) =
|
|
tx.gasLimit = x
|
|
|
|
template `input=`(tx: var Transaction, x: Blob) =
|
|
tx.payload = x
|
|
|
|
template `v=`(tx: var Transaction, x: uint64) =
|
|
tx.V = x
|
|
|
|
template `r=`(tx: var Transaction, x: UInt256) =
|
|
tx.R = x
|
|
|
|
template `s=`(tx: var Transaction, x: UInt256) =
|
|
tx.S = x
|
|
|
|
template `blobVersionedHashes=`(tx: var Transaction, x: VersionedHashes) =
|
|
tx.versionedHashes = x
|
|
|
|
template required(o: untyped, T: type, oField: untyped) =
|
|
const fName = astToStr(oField)
|
|
if not n.hasKey(fName):
|
|
raise newError(ErrorJson, "missing required field '" & fName & "' in transaction")
|
|
o.oField = T.fromJson(n, fName)
|
|
|
|
template omitZero(o: untyped, T: type, oField: untyped) =
|
|
const fName = astToStr(oField)
|
|
if n.hasKey(fName):
|
|
o.oField = T.fromJson(n, fName)
|
|
|
|
template optional(o: untyped, T: type, oField: untyped) =
|
|
const fName = astToStr(oField)
|
|
if n.hasKey(fName) and n[fName].kind != JNull:
|
|
o.oField = Opt.some(T.fromJson(n, fName))
|
|
|
|
proc parseAlloc*(ctx: var TransContext, n: JsonNode) =
|
|
for accAddr, acc in n:
|
|
let address = hexToByteArray[20](accAddr)
|
|
var ga = GenesisAccount()
|
|
if acc.hasKey("code"):
|
|
ga.code = Blob.fromJson(acc, "code")
|
|
if acc.hasKey("nonce"):
|
|
ga.nonce = AccountNonce.fromJson(acc, "nonce")
|
|
if acc.hasKey("balance"):
|
|
ga.balance = UInt256.fromJson(acc, "balance")
|
|
else:
|
|
raise newError(ErrorJson, "GenesisAlloc: balance required")
|
|
if acc.hasKey("storage"):
|
|
let storage = acc["storage"]
|
|
for k, v in storage:
|
|
ga.storage[UInt256.fromHex(k)] = UInt256.fromHex(v.getStr())
|
|
ctx.alloc[address] = ga
|
|
|
|
proc parseEnv*(ctx: var TransContext, n: JsonNode) =
|
|
required(ctx.env, EthAddress, currentCoinbase)
|
|
required(ctx.env, GasInt, currentGasLimit)
|
|
required(ctx.env, BlockNumber, currentNumber)
|
|
required(ctx.env, EthTime, currentTimestamp)
|
|
optional(ctx.env, DifficultyInt, currentDifficulty)
|
|
optional(ctx.env, Hash256, currentRandom)
|
|
optional(ctx.env, DifficultyInt, parentDifficulty)
|
|
omitZero(ctx.env, EthTime, parentTimestamp)
|
|
optional(ctx.env, UInt256, currentBaseFee)
|
|
omitZero(ctx.env, Hash256, parentUncleHash)
|
|
optional(ctx.env, UInt256, parentBaseFee)
|
|
optional(ctx.env, GasInt, parentGasUsed)
|
|
optional(ctx.env, GasInt, parentGasLimit)
|
|
optional(ctx.env, uint64, currentBlobGasUsed)
|
|
optional(ctx.env, uint64, currentExcessBlobGas)
|
|
optional(ctx.env, uint64, parentBlobGasUsed)
|
|
optional(ctx.env, uint64, parentExcessBlobGas)
|
|
optional(ctx.env, Hash256, parentBeaconBlockRoot)
|
|
|
|
if n.hasKey("blockHashes"):
|
|
let w = n["blockHashes"]
|
|
for k, v in w:
|
|
ctx.env.blockHashes[parseHexOrInt[uint64](k)] = Hash256.fromHex(v.getStr())
|
|
|
|
if n.hasKey("ommers"):
|
|
let w = n["ommers"]
|
|
for v in w:
|
|
ctx.env.ommers.add Ommer.fromJson(v)
|
|
|
|
if n.hasKey("withdrawals"):
|
|
let w = n["withdrawals"]
|
|
var withdrawals: seq[Withdrawal]
|
|
for v in w:
|
|
withdrawals.add Withdrawal.fromJson(v)
|
|
ctx.env.withdrawals = Opt.some(withdrawals)
|
|
|
|
proc parseTx(n: JsonNode, chainId: ChainID): Transaction =
|
|
var tx: Transaction
|
|
if not n.hasKey("type"):
|
|
tx.txType = TxLegacy
|
|
else:
|
|
tx.txType = uint64.fromJson(n, "type").TxType
|
|
|
|
required(tx, AccountNonce, nonce)
|
|
required(tx, GasInt, gas)
|
|
required(tx, UInt256, value)
|
|
required(tx, Blob, input)
|
|
|
|
if n.hasKey("to"):
|
|
tx.to = Opt.some(EthAddress.fromJson(n, "to"))
|
|
|
|
case tx.txType
|
|
of TxLegacy:
|
|
required(tx, GasInt, gasPrice)
|
|
of TxEip2930:
|
|
required(tx, GasInt, gasPrice)
|
|
required(tx, ChainId, chainId)
|
|
omitZero(tx, AccessList, accessList)
|
|
of TxEip1559:
|
|
required(tx, ChainId, chainId)
|
|
required(tx, GasInt, maxPriorityFeePerGas)
|
|
required(tx, GasInt, maxFeePerGas)
|
|
omitZero(tx, AccessList, accessList)
|
|
of TxEip4844:
|
|
required(tx, ChainId, chainId)
|
|
required(tx, GasInt, maxPriorityFeePerGas)
|
|
required(tx, GasInt, maxFeePerGas)
|
|
omitZero(tx, AccessList, accessList)
|
|
required(tx, UInt256, maxFeePerBlobGas)
|
|
required(tx, VersionedHashes, blobVersionedHashes)
|
|
|
|
var eip155 = true
|
|
if n.hasKey("protected"):
|
|
eip155 = n["protected"].bval
|
|
|
|
if n.hasKey("secretKey"):
|
|
let data = Blob.fromJson(n, "secretKey")
|
|
let secretKey = PrivateKey.fromRaw(data).tryGet
|
|
signTransaction(tx, secretKey, chainId, eip155)
|
|
else:
|
|
required(tx, uint64, v)
|
|
required(tx, UInt256, r)
|
|
required(tx, UInt256, s)
|
|
tx
|
|
|
|
proc parseTxLegacy(item: var Rlp): Result[Transaction, string] =
|
|
try:
|
|
var tx: Transaction
|
|
item.decodeTxLegacy(tx)
|
|
return ok(tx)
|
|
except RlpError as x:
|
|
return err(x.msg)
|
|
|
|
proc parseTxTyped(item: var Rlp): Result[Transaction, string] =
|
|
try:
|
|
var tx: Transaction
|
|
var rr = rlpFromBytes(item.read(Blob))
|
|
rr.decodeTxTyped(tx)
|
|
return ok(tx)
|
|
except RlpError as x:
|
|
return err(x.msg)
|
|
|
|
proc parseTxJson(ctx: TransContext, i: int, chainId: ChainId): Result[Transaction, string] =
|
|
try:
|
|
let n = ctx.txs.n[i]
|
|
return ok(parseTx(n, chainId))
|
|
except Exception as x:
|
|
return err(x.msg)
|
|
|
|
proc parseTxs*(ctx: TransContext, chainId: ChainId): seq[Result[Transaction, string]] =
|
|
if ctx.txs.txsType == TxsJson:
|
|
let len = ctx.txs.n.len
|
|
result = newSeqOfCap[Result[Transaction, string]](len)
|
|
for i in 0 ..< len:
|
|
result.add ctx.parseTxJson(i, chainId)
|
|
return
|
|
|
|
if ctx.txs.txsType == TxsRlp:
|
|
result = newSeqOfCap[Result[Transaction, string]](ctx.txs.r.listLen)
|
|
var rlp = ctx.txs.r
|
|
for item in rlp:
|
|
if item.isList:
|
|
result.add parseTxLegacy(item)
|
|
else:
|
|
result.add parseTxTyped(item)
|
|
return
|
|
|
|
proc txList*(ctx: TransContext, chainId: ChainId): seq[Transaction] =
|
|
let list = ctx.parseTxs(chainId)
|
|
for txRes in list:
|
|
if txRes.isOk:
|
|
result.add txRes.get
|
|
|
|
proc parseTxs*(ctx: var TransContext, txs: JsonNode) =
|
|
if txs.kind == JNull:
|
|
return
|
|
if txs.kind != JArray:
|
|
raise newError(ErrorJson,
|
|
"Transaction list should be a JSON array, got=" & $txs.kind)
|
|
ctx.txs = TxsList(
|
|
txsType: TxsJson,
|
|
n: txs)
|
|
|
|
proc parseTxsRlp*(ctx: var TransContext, hexData: string) =
|
|
let bytes = hexToSeqByte(hexData)
|
|
ctx.txs = TxsList(
|
|
txsType: TxsRlp,
|
|
r: rlpFromBytes(bytes)
|
|
)
|
|
if ctx.txs.r.isList.not:
|
|
raise newError(ErrorRlp, "RLP Transaction list should be a list")
|
|
|
|
proc parseInputFromStdin*(ctx: var TransContext) =
|
|
let data = stdin.readAll()
|
|
let n = json.parseJson(data)
|
|
if n.hasKey("alloc"): ctx.parseAlloc(n["alloc"])
|
|
if n.hasKey("env"): ctx.parseEnv(n["env"])
|
|
if n.hasKey("txs"): ctx.parseTxs(n["txs"])
|
|
if n.hasKey("txsRlp"): ctx.parseTxsRlp(n["txsRlp"].getStr())
|
|
|
|
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 `@@`*[K, V](x: Table[K, V]): JsonNode
|
|
proc `@@`*[T](x: seq[T]): JsonNode
|
|
|
|
proc to0xHex(x: UInt256): string =
|
|
"0x" & x.toHex
|
|
|
|
proc `@@`(x: uint64 | int64 | int): JsonNode =
|
|
let hex = x.toHex.stripLeadingZeros
|
|
%("0x" & hex.toLowerAscii)
|
|
|
|
proc `@@`(x: UInt256): JsonNode =
|
|
%("0x" & x.toHex)
|
|
|
|
proc `@@`(x: Hash256): JsonNode =
|
|
%("0x" & x.data.toHex)
|
|
|
|
proc `@@`*(x: Blob): JsonNode =
|
|
%("0x" & x.toHex)
|
|
|
|
proc `@@`(x: bool): JsonNode =
|
|
%(if x: "0x1" else: "0x0")
|
|
|
|
proc `@@`(x: EthAddress): JsonNode =
|
|
%("0x" & x.toHex)
|
|
|
|
proc `@@`(x: Topic): JsonNode =
|
|
%("0x" & x.toHex)
|
|
|
|
proc toJson(x: Table[UInt256, UInt256]): JsonNode =
|
|
# special case, we need to convert UInt256 into full 32 bytes
|
|
# and not shorter
|
|
result = newJObject()
|
|
for k, v in x:
|
|
result["0x" & k.dumpHex] = %("0x" & v.dumpHex)
|
|
|
|
proc `@@`(acc: GenesisAccount): JsonNode =
|
|
result = newJObject()
|
|
if acc.code.len > 0:
|
|
result["code"] = @@(acc.code)
|
|
result["balance"] = @@(acc.balance)
|
|
if acc.nonce > 0:
|
|
result["nonce"] = @@(acc.nonce)
|
|
if acc.storage.len > 0:
|
|
result["storage"] = toJson(acc.storage)
|
|
|
|
proc `@@`[K, V](x: Table[K, V]): JsonNode =
|
|
result = newJObject()
|
|
for k, v in x:
|
|
result[k.to0xHex] = @@(v)
|
|
|
|
proc `@@`(x: BloomFilter): JsonNode =
|
|
%("0x" & toHex[256](x))
|
|
|
|
proc `@@`(x: Log): JsonNode =
|
|
result = %{
|
|
"address": @@(x.address),
|
|
"topics" : @@(x.topics),
|
|
"data" : @@(x.data)
|
|
}
|
|
|
|
proc `@@`(x: TxReceipt): JsonNode =
|
|
result = %{
|
|
"root" : if x.root == Hash256(): %("0x") else: @@(x.root),
|
|
"status" : @@(x.status),
|
|
"cumulativeGasUsed": @@(x.cumulativeGasUsed),
|
|
"logsBloom" : @@(x.logsBloom),
|
|
"logs" : if x.logs.len == 0: newJNull() else: @@(x.logs),
|
|
"transactionHash" : @@(x.transactionHash),
|
|
"contractAddress" : @@(x.contractAddress),
|
|
"gasUsed" : @@(x.gasUsed),
|
|
"blockHash" : @@(x.blockHash),
|
|
"transactionIndex" : @@(x.transactionIndex)
|
|
}
|
|
if x.txType > TxLegacy:
|
|
result["type"] = %("0x" & toHex(x.txType.int, 1))
|
|
|
|
proc `@@`(x: RejectedTx): JsonNode =
|
|
result = %{
|
|
"index": %(x.index),
|
|
"error": %(x.error)
|
|
}
|
|
|
|
proc `@@`[T](x: seq[T]): JsonNode =
|
|
result = newJArray()
|
|
for c in x:
|
|
result.add @@(c)
|
|
|
|
proc `@@`[T](x: Opt[T]): JsonNode =
|
|
if x.isNone:
|
|
newJNull()
|
|
else:
|
|
@@(x.get())
|
|
|
|
proc `@@`*(x: ExecutionResult): JsonNode =
|
|
result = %{
|
|
"stateRoot" : @@(x.stateRoot),
|
|
"txRoot" : @@(x.txRoot),
|
|
"receiptsRoot": @@(x.receiptsRoot),
|
|
"logsHash" : @@(x.logsHash),
|
|
"logsBloom" : @@(x.logsBloom),
|
|
"receipts" : @@(x.receipts),
|
|
"currentDifficulty": @@(x.currentDifficulty),
|
|
"gasUsed" : @@(x.gasUsed)
|
|
}
|
|
if x.rejected.len > 0:
|
|
result["rejected"] = @@(x.rejected)
|
|
if x.currentBaseFee.isSome:
|
|
result["currentBaseFee"] = @@(x.currentBaseFee)
|
|
if x.withdrawalsRoot.isSome:
|
|
result["withdrawalsRoot"] = @@(x.withdrawalsRoot)
|
|
if x.currentExcessBlobGas.isSome:
|
|
result["currentExcessBlobGas"] = @@(x.currentExcessBlobGas)
|
|
if x.blobGasUsed.isSome:
|
|
result["blobGasUsed"] = @@(x.blobGasUsed)
|