Skip to content

Commit

Permalink
transaction signing helpers (#742)
Browse files Browse the repository at this point in the history
Transaction signing is something that happens in a lot of places - this
PR introduces primitives for transaction signing in `transaction_utils`
such that we can use the same logic across web3/eth1/etc for this simple
operation.

`transaction_utils` also contains a few more "spec-derived" helpers for
working with transactions, such as the computation of a contract address
etc that cannot easily be introduced in `transactions` itself without
bringing in dependencies like secp and rlp, so they end up in a separate
module.

Finally, since these modules collect "versions" of these transaction
types across different eips, some tests are moved to follow the same
structure.
  • Loading branch information
arnetheduck authored Oct 4, 2024
1 parent 84664b0 commit 4ea11b9
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 168 deletions.
3 changes: 0 additions & 3 deletions eth/common/eth_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ type
EthReceipt* = Receipt
EthWithdrawapRequest* = WithdrawalRequest

template contractCreation*(tx: Transaction): bool =
tx.to.isNone

func init*(T: type BlockHashOrNumber, str: string): T {.raises: [ValueError].} =
if str.startsWith "0x":
if str.len != sizeof(default(T).hash.data) * 2 + 2:
Expand Down
77 changes: 77 additions & 0 deletions eth/common/transaction_utils.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# eth
# Copyright (c) 2024 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

import ./[keys, transactions, transactions_rlp]

export keys, transactions

proc signature*(tx: Transaction): Opt[Signature] =
var bytes {.noinit.}: array[65, byte]
bytes[0 .. 31] = tx.R.toBytesBE()
bytes[32 .. 63] = tx.S.toBytesBE()

bytes[64] =
if tx.txType != TxLegacy:
tx.V.byte
elif tx.V >= EIP155_CHAIN_ID_OFFSET:
byte(1 - (tx.V and 1))
elif tx.V == 27 or tx.V == 28:
byte(tx.V - 27)
else:
return Opt.none(Signature)

Signature.fromRaw(bytes).mapConvertErr(void)

proc `signature=`*(tx: var Transaction, param: tuple[sig: Signature, eip155: bool]) =
let raw = param.sig.toRaw()

tx.R = UInt256.fromBytesBE(raw.toOpenArray(0, 31))
tx.S = UInt256.fromBytesBE(raw.toOpenArray(32, 63))

let v = raw[64].uint64
tx.V =
case tx.txType
of TxLegacy:
if param.eip155:
v + uint64(tx.chainId) * 2 + 35
else:
v + 27'u64
else:
v

proc sign*(tx: Transaction, pk: PrivateKey, eip155: bool): (Signature, bool) =
let hash = tx.rlpHashForSigning(eip155)

(sign(pk, SkMessage(hash.data)), eip155)

proc recoverKey*(tx: Transaction): Opt[PublicKey] =
## Recovering key / sender is a costly operation - make sure to reuse the
## outcome!
##
## Returns `none` if the signature is invalid with respect to the rest of
## the transaction data.
let
sig = ?tx.signature()
txHash = tx.rlpHashForSigning(tx.isEip155())

recover(sig, SkMessage(txHash.data)).mapConvertErr(void)

proc recoverSender*(tx: Transaction): Opt[Address] =
## Recovering key / sender is a costly operation - make sure to reuse the
## outcome!
##
## Returns `none` if the signature is invalid with respect to the rest of
## the transaction data.
let key = ?tx.recoverKey()
ok key.to(Address)

proc creationAddress*(tx: Transaction, sender: Address): Address =
let hash = keccak256(rlp.encodeList(sender, tx.nonce))
hash.to(Address)

proc getRecipient*(tx: Transaction, sender: Address): Address =
tx.to.valueOr(tx.creationAddress(sender))
8 changes: 8 additions & 0 deletions eth/common/transactions.nim
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import "."/[addresses, base, hashes]

export addresses, base, hashes

const EIP155_CHAIN_ID_OFFSET* = 35'u64

type
AccessPair* = object
address* : Address
Expand Down Expand Up @@ -71,3 +73,9 @@ func destination*(tx: Transaction): Address =
# use getRecipient if you also want to get
# the contract address
tx.to.valueOr(default(Address))

func isEip155*(tx: Transaction): bool =
tx.V >= EIP155_CHAIN_ID_OFFSET

func contractCreation*(tx: Transaction): bool =
tx.to.isNone
28 changes: 18 additions & 10 deletions eth/common/transactions_rlp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ proc append*(w: var RlpWriter, tx: PooledTransaction) =
if tx.networkPayload != nil:
w.append(tx.networkPayload)

const EIP155_CHAIN_ID_OFFSET* = 35'u64

proc rlpEncodeLegacy(tx: Transaction): seq[byte] =
var w = initRlpWriter()
w.startList(6)
Expand All @@ -142,7 +140,6 @@ proc rlpEncodeLegacy(tx: Transaction): seq[byte] =
w.finish()

proc rlpEncodeEip155(tx: Transaction): seq[byte] =
let chainId = (tx.V - EIP155_CHAIN_ID_OFFSET) div 2
var w = initRlpWriter()
w.startList(9)
w.append(tx.nonce)
Expand All @@ -151,7 +148,7 @@ proc rlpEncodeEip155(tx: Transaction): seq[byte] =
w.append(tx.to)
w.append(tx.value)
w.append(tx.payload)
w.append(chainId)
w.append(tx.chainId)
w.append(0'u8)
w.append(0'u8)
w.finish()
Expand Down Expand Up @@ -218,10 +215,12 @@ proc rlpEncodeEip7702(tx: Transaction): seq[byte] =
w.append(tx.authorizationList)
w.finish()

proc rlpEncode*(tx: Transaction): seq[byte] =
proc encodeForSigning*(tx: Transaction, eip155: bool): seq[byte] =
## Encode transaction data in preparation for signing or signature checking.
## For signature checking, set `eip155 = tx.isEip155`
case tx.txType
of TxLegacy:
if tx.V >= EIP155_CHAIN_ID_OFFSET: tx.rlpEncodeEip155 else: tx.rlpEncodeLegacy
if eip155: tx.rlpEncodeEip155 else: tx.rlpEncodeLegacy
of TxEip2930:
tx.rlpEncodeEip2930
of TxEip1559:
Expand All @@ -231,11 +230,17 @@ proc rlpEncode*(tx: Transaction): seq[byte] =
of TxEip7702:
tx.rlpEncodeEip7702

func txHashNoSignature*(tx: Transaction): Hash32 =
template rlpEncode*(tx: Transaction): seq[byte] {.deprecated.} =
encodeForSigning(tx, tx.isEip155())

func rlpHashForSigning*(tx: Transaction, eip155: bool): Hash32 =
# Hash transaction without signature
keccak256(rlpEncode(tx))
keccak256(encodeForSigning(tx, eip155))

proc readTxLegacy*(rlp: var Rlp, tx: var Transaction) {.raises: [RlpError].} =
template txHashNoSignature*(tx: Transaction): Hash32 {.deprecated.} =
rlpHashForSigning(tx, tx.isEip155())

proc readTxLegacy(rlp: var Rlp, tx: var Transaction) {.raises: [RlpError].} =
tx.txType = TxLegacy
rlp.tryEnterList()
rlp.read(tx.nonce)
Expand All @@ -248,6 +253,9 @@ proc readTxLegacy*(rlp: var Rlp, tx: var Transaction) {.raises: [RlpError].} =
rlp.read(tx.R)
rlp.read(tx.S)

if tx.V >= EIP155_CHAIN_ID_OFFSET:
tx.chainId = ChainId((tx.V - EIP155_CHAIN_ID_OFFSET) div 2)

proc readTxEip2930(rlp: var Rlp, tx: var Transaction) {.raises: [RlpError].} =
tx.txType = TxEip2930
rlp.tryEnterList()
Expand Down Expand Up @@ -375,7 +383,7 @@ proc readTxPayload(
of TxEip7702:
rlp.readTxEip7702(tx)

proc readTxTyped*(rlp: var Rlp, tx: var Transaction) {.raises: [RlpError].} =
proc readTxTyped(rlp: var Rlp, tx: var Transaction) {.raises: [RlpError].} =
let txType = rlp.readTxType()
rlp.readTxPayload(tx, txType)

Expand Down
6 changes: 3 additions & 3 deletions tests/common/all_tests.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

import
test_common,
test_eip4844,
test_eip7702,
test_eth_types,
test_eth_types_rlp,
test_keys
test_keys,
test_receipts,
test_transactions
2 changes: 1 addition & 1 deletion tests/common/test_common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type
header: Header

proc loadFile(x: int) =
let fileName = "tests" / "common" / "eip2718" / "acl_block_" & $x & ".json"
let fileName = currentSourcePath.parentDir / "eip2718" / "acl_block_" & $x & ".json"
test fileName:
let n = json.parseFile(fileName)
let data = n["rlp"].getStr()
Expand Down
134 changes: 0 additions & 134 deletions tests/common/test_eip7702.nim

This file was deleted.

1 change: 0 additions & 1 deletion tests/common/test_eth_types_rlp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,3 @@ suite "EIP-7865 tests":

check decodedBody == body
check decodedBlk == blk

39 changes: 39 additions & 0 deletions tests/common/test_receipts.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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.
{.used.}

import
unittest2,
../../eth/common/[receipts_rlp]

template roundTrip(v: untyped) =
let bytes = rlp.encode(v)
let v2 = rlp.decode(bytes, Receipt)
let bytes2 = rlp.encode(v2)
check bytes == bytes2

suite "Receipts":
test "EIP-4844":
let rec = Receipt(
receiptType: Eip4844Receipt,
isHash: false,
status: false,
cumulativeGasUsed: 100.GasInt)

roundTrip(rec)

test "EIP-7702":
let rec = Receipt(
receiptType: Eip7702Receipt,
isHash: false,
status: false,
cumulativeGasUsed: 100.GasInt)

roundTrip(rec)
Loading

0 comments on commit 4ea11b9

Please sign in to comment.