diff --git a/eth.nimble b/eth.nimble index 105ae2e..c5ac81f 100644 --- a/eth.nimble +++ b/eth.nimble @@ -8,8 +8,10 @@ skipDirs = @["tests"] requires "nim >= 1.6.0", "nimcrypto", + "results", "stint", "secp256k1", + "ssz_serialization", "chronos#head", "chronicles", "stew", diff --git a/eth/common/eth_types.nim b/eth/common/eth_types.nim index 160aafc..f6657cd 100644 --- a/eth/common/eth_types.nim +++ b/eth/common/eth_types.nim @@ -11,11 +11,22 @@ import std/[options, strutils], - stew/[byteutils, endians2], stint, + results, stew/[byteutils, endians2], stint, + ssz_serialization, ./eth_hash, ./eth_times export - options, stint, eth_hash, eth_times + options, results, stint, ssz_serialization, eth_hash, eth_times + +const + MAX_CALLDATA_SIZE* = 1 shl 24 # 2^24 + MAX_ACCESS_LIST_STORAGE_KEYS* = 1 shl 24 # 2^24 + MAX_ACCESS_LIST_SIZE* = 1 shl 24 # 2^24 + MAX_BLOB_COMMITMENTS_PER_BLOCK* = 4_096 + ECDSA_SIGNATURE_SIZE* = 65 + MAX_TRANSACTION_PAYLOAD_FIELDS* = 32 + MAX_TRANSACTION_SIGNATURE_FIELDS* = 16 + MAX_POOLED_TRANSACTION_FIELDS* = 8 type Hash256* = MDigest[256] @@ -54,12 +65,6 @@ type storageRoot*: Hash256 codeHash*: Hash256 - AccessPair* = object - address* : EthAddress - storageKeys*: seq[StorageKey] - - AccessList* = seq[AccessPair] - VersionedHash* = Hash256 VersionedHashes* = seq[VersionedHash] KzgCommitment* = array[48, byte] @@ -69,37 +74,265 @@ type # 4096 -> FIELD_ELEMENTS_PER_BLOB NetworkBlob* = array[32*4096, byte] - TxType* = enum + TxType* = enum # EIP-2718 TxLegacy # 0 TxEip2930 # 1 TxEip1559 # 2 TxEip4844 # 3 + StorageKeys* = List[StorageKey, Limit MAX_ACCESS_LIST_STORAGE_KEYS] + + AccessPair* = object + address*: EthAddress + storage_keys*: StorageKeys + + AccessList* = List[AccessPair, Limit MAX_ACCESS_LIST_SIZE] + + TransactionPayload* {. + sszStableContainer: MAX_TRANSACTION_PAYLOAD_FIELDS.} = object + nonce*: uint64 + max_fee_per_gas*: UInt256 + gas*: uint64 + to*: Opt[EthAddress] + value*: UInt256 + input*: List[byte, Limit MAX_CALLDATA_SIZE] + + # EIP-2718 + tx_type* {.serializedFieldName: "type".}: Opt[TxType] + + # EIP-2930 + access_list*: Opt[AccessList] + + # EIP-1559 + max_priority_fee_per_gas*: Opt[UInt256] + + # EIP-4844 + max_fee_per_blob_gas*: Opt[UInt256] + blob_versioned_hashes*: + Opt[List[VersionedHash, Limit MAX_BLOB_COMMITMENTS_PER_BLOCK]] + + TransactionSignature* {. + sszStableContainer: MAX_TRANSACTION_SIGNATURE_FIELDS.} = object + from_address* {.serializedFieldName: "from".}: EthAddress + ecdsa_signature*: array[ECDSA_SIGNATURE_SIZE, byte] + + Transaction* = object # EIP-6493 + payload*: TransactionPayload + signature*: TransactionSignature + + ReplayableTransactionPayload* {.sszVariant: TransactionPayload.} = object + nonce*: uint64 + max_fee_per_gas*: UInt256 + gas*: uint64 + to*: Opt[EthAddress] + value*: UInt256 + input*: List[byte, Limit MAX_CALLDATA_SIZE] + + ReplayableTransaction* {.sszVariant: Transaction.} = object # EIP-6493 + payload*: ReplayableTransactionPayload + signature*: TransactionSignature + + LegacyTransactionPayload* {.sszVariant: TransactionPayload.} = object + nonce*: uint64 + max_fee_per_gas*: UInt256 + gas*: uint64 + to*: Opt[EthAddress] + value*: UInt256 + input*: List[byte, Limit MAX_CALLDATA_SIZE] + tx_type* {.serializedFieldName: "type".}: TxType + + LegacyTransaction* {.sszVariant: Transaction.} = object # EIP-6493 + payload*: LegacyTransactionPayload + signature*: TransactionSignature + + Eip2930TransactionPayload* {.sszVariant: TransactionPayload.} = object + nonce*: uint64 + max_fee_per_gas*: UInt256 + gas*: uint64 + to*: Opt[EthAddress] + value*: UInt256 + input*: List[byte, Limit MAX_CALLDATA_SIZE] + tx_type* {.serializedFieldName: "type".}: TxType + access_list*: AccessList + + Eip2930Transaction* {.sszVariant: Transaction.} = object # EIP-6493 + payload*: Eip2930TransactionPayload + signature*: TransactionSignature + + Eip1559TransactionPayload* {.sszVariant: TransactionPayload.} = object + nonce*: uint64 + max_fee_per_gas*: UInt256 + gas*: uint64 + to*: Opt[EthAddress] + value*: UInt256 + input*: List[byte, Limit MAX_CALLDATA_SIZE] + tx_type* {.serializedFieldName: "type".}: TxType + access_list*: AccessList + max_priority_fee_per_gas*: UInt256 + + Eip1559Transaction* {.sszVariant: Transaction.} = object # EIP-6493 + payload*: Eip1559TransactionPayload + signature*: TransactionSignature + + Eip4844TransactionPayload* {.sszVariant: TransactionPayload.} = object + nonce*: uint64 + max_fee_per_gas*: UInt256 + gas*: uint64 + to*: EthAddress + value*: UInt256 + input*: List[byte, Limit MAX_CALLDATA_SIZE] + tx_type* {.serializedFieldName: "type".}: TxType + access_list*: AccessList + max_priority_fee_per_gas*: UInt256 + max_fee_per_blob_gas*: UInt256 + blob_versioned_hashes*: + List[VersionedHash, Limit MAX_BLOB_COMMITMENTS_PER_BLOCK] + + Eip4844Transaction* {.sszVariant: Transaction.} = object # EIP-6493 + payload*: Eip4844TransactionPayload + signature*: TransactionSignature + + TransactionKind* {.pure.} = enum + Replayable + Legacy + Eip2930 + Eip1559 + Eip4844 + + AnyTransactionPayloadVariant* = + ReplayableTransactionPayload | + LegacyTransactionPayload | + Eip2930TransactionPayload | + Eip1559TransactionPayload | + Eip4844TransactionPayload + + AnyTransactionPayload* {.sszOneOf: TransactionPayload.} = object + case kind*: TransactionKind + of TransactionKind.Eip4844: + eip4844Data*: Eip4844TransactionPayload + of TransactionKind.Eip1559: + eip1559Data*: Eip1559TransactionPayload + of TransactionKind.Eip2930: + eip2930Data*: Eip2930TransactionPayload + of TransactionKind.Legacy: + legacyData*: LegacyTransactionPayload + of TransactionKind.Replayable: + replayableData*: ReplayableTransactionPayload + + AnyTransactionVariant* = + ReplayableTransaction | + LegacyTransaction | + Eip2930Transaction | + Eip1559Transaction | + Eip4844Transaction + + AnyTransaction* {.sszOneOf: Transaction.} = object + case kind*: TransactionKind + of TransactionKind.Eip4844: + eip4844Data*: Eip4844Transaction + of TransactionKind.Eip1559: + eip1559Data*: Eip1559Transaction + of TransactionKind.Eip2930: + eip2930Data*: Eip2930Transaction + of TransactionKind.Legacy: + legacyData*: LegacyTransaction + of TransactionKind.Replayable: + replayableData*: ReplayableTransaction + +template withTxPayloadVariant*( + x: AnyTransactionPayload, body: untyped): untyped = + case x.kind + of TransactionKind.Eip4844: + const txKind {.inject, used.} = TransactionKind.Eip4844 + template txPayloadVariant: untyped {.inject, used.} = x.eip4844Data + body + of TransactionKind.Eip1559: + const txKind {.inject, used.} = TransactionKind.Eip1559 + template txPayloadVariant: untyped {.inject, used.} = x.eip1559Data + body + of TransactionKind.Eip2930: + const txKind {.inject, used.} = TransactionKind.Eip2930 + template txPayloadVariant: untyped {.inject, used.} = x.eip2930Data + body + of TransactionKind.Legacy: + const txKind {.inject, used.} = TransactionKind.Legacy + template txPayloadVariant: untyped {.inject, used.} = x.legacyData + body + of TransactionKind.Replayable: + const txKind {.inject, used.} = TransactionKind.Replayable + template txPayloadVariant: untyped {.inject, used.} = x.replayableData + body + +func init*(T: typedesc[AnyTransaction], tx: Eip4844Transaction): T = + T(kind: TransactionKind.Eip4844, eip4844Data: tx) + +func init*(T: typedesc[AnyTransaction], tx: Eip1559Transaction): T = + T(kind: TransactionKind.Eip1559, eip1559Data: tx) + +func init*(T: typedesc[AnyTransaction], tx: Eip2930Transaction): T = + T(kind: TransactionKind.Eip2930, eip2930Data: tx) + +func init*(T: typedesc[AnyTransaction], tx: LegacyTransaction): T = + T(kind: TransactionKind.Legacy, legacyData: tx) + +func init*(T: typedesc[AnyTransaction], tx: ReplayableTransaction): T = + T(kind: TransactionKind.Replayable, replayableData: tx) + +template withTxVariant*(x: AnyTransaction, body: untyped): untyped = + case x.kind + of TransactionKind.Eip4844: + const txKind {.inject, used.} = TransactionKind.Eip4844 + template txVariant: untyped {.inject, used.} = x.eip4844Data + body + of TransactionKind.Eip1559: + const txKind {.inject, used.} = TransactionKind.Eip1559 + template txVariant: untyped {.inject, used.} = x.eip1559Data + body + of TransactionKind.Eip2930: + const txKind {.inject, used.} = TransactionKind.Eip2930 + template txVariant: untyped {.inject, used.} = x.eip2930Data + body + of TransactionKind.Legacy: + const txKind {.inject, used.} = TransactionKind.Legacy + template txVariant: untyped {.inject, used.} = x.legacyData + body + of TransactionKind.Replayable: + const txKind {.inject, used.} = TransactionKind.Replayable + template txVariant: untyped {.inject, used.} = x.replayableData + body + +# https://eips.ethereum.org/EIPS/eip-6493#ssz-signedtransaction-container +func selectVariant*(value: TransactionPayload): Opt[TransactionKind] = + if value.tx_type == Opt.some TxEip4844: + return Opt.some TransactionKind.Eip4844 + + if value.tx_type == Opt.some TxEip1559: + return Opt.some TransactionKind.Eip1559 + + if value.tx_type == Opt.some TxEip2930: + return Opt.some TransactionKind.Eip2930 + + if value.tx_type == Opt.some TxLegacy: + return Opt.some TransactionKind.Legacy + + if value.tx_type.isNone: + return Opt.some TransactionKind.Replayable + + Opt.none TransactionKind + +func selectVariant*(value: Transaction): Opt[TransactionKind] = + selectVariant(value.payload) + +type NetworkPayload* = ref object - blobs* : seq[NetworkBlob] - commitments* : seq[KzgCommitment] - proofs* : seq[KzgProof] + blobs* : List[NetworkBlob, MAX_BLOB_COMMITMENTS_PER_BLOCK] + commitments* : List[KzgCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + proofs* : List[KzgProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] - Transaction* = object - txType* : TxType # EIP-2718 - chainId* : ChainId # EIP-2930 - nonce* : AccountNonce - gasPrice* : GasInt - maxPriorityFee*: GasInt # EIP-1559 - maxFee* : GasInt # EIP-1559 - gasLimit* : GasInt - to* : Option[EthAddress] - value* : UInt256 - payload* : Blob - accessList* : AccessList # EIP-2930 - maxFeePerBlobGas*: UInt256 # EIP-4844 - versionedHashes*: VersionedHashes # EIP-4844 - V* : int64 - R*, S* : UInt256 - - PooledTransaction* = object + PooledTransaction* {. + sszStableContainer: MAX_POOLED_TRANSACTION_FIELDS.} = object tx*: Transaction - networkPayload*: NetworkPayload # EIP-4844 + blob_data*: Opt[NetworkPayload] # EIP-4844 TransactionStatus* = enum Unknown, @@ -290,14 +523,17 @@ func stateRoot*(rec: Receipt): Hash256 {.inline.} = rec.hash template contractCreation*(tx: Transaction): bool = - tx.to.isNone + tx.payload.to.isNone -func destination*(tx: Transaction): EthAddress = +func destination*(tx: TransactionPayload): EthAddress = # use getRecipient if you also want to get # the contract address if tx.to.isSome: return tx.to.get +func destination*(tx: Transaction): EthAddress = + tx.payload.destination + func init*(T: type BlockHashOrNumber, str: string): T {.raises: [ValueError].} = if str.startsWith "0x": @@ -323,6 +559,8 @@ template deref*(b: Blob): auto = b template deref*(o: Option): auto = o.get template deref*(r: EthResourceRefs): auto = r[] +func `==`*(a, b: ChainId): bool {.borrow.} + func `==`*(a, b: NetworkId): bool = a.uint == b.uint diff --git a/eth/common/eth_types_rlp.nim b/eth/common/eth_types_rlp.nim index 5ed871a..2b87b77 100644 --- a/eth/common/eth_types_rlp.nim +++ b/eth/common/eth_types_rlp.nim @@ -5,8 +5,10 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import + std/[sequtils, typetraits], + ssz_serialization, "."/[eth_types, eth_hash_rlp], - ../rlp + ".."/[keys, rlp] from stew/objects import checkedEnumAssign @@ -18,6 +20,14 @@ export # Rlp serialization: # +type RawRlp* = distinct seq[byte] + +proc append*(w: var RlpWriter, value: RawRlp) = + w.appendRawBytes(distinctBase(value)) + +func read*(rlp: var Rlp, T: type RawRlp): T = + rlp.toBytes().T + proc read*(rlp: var Rlp, T: type StUint): T {.inline.} = if rlp.isBlob: let bytes = rlp.toBytes @@ -64,102 +74,20 @@ proc append*(rlpWriter: var RlpWriter, value: StInt) = {.fatal: "RLP serialization of signed integers is not allowed".} discard -proc append*[T](w: var RlpWriter, val: Option[T]) = +func append*(w: var RlpWriter, val: ChainId) = + w.append(distinctBase(val)) + +func read*(rlp: var Rlp, T: typedesc[ChainId]): T = + rlp.read(distinctBase(T)).T + +proc append*[T](w: var RlpWriter, val: Option[T] | Opt[T]) = if val.isSome: w.append(val.get()) else: w.append("") -proc appendTxLegacy(w: var RlpWriter, tx: Transaction) = - w.startList(9) - w.append(tx.nonce) - w.append(tx.gasPrice) - w.append(tx.gasLimit) - w.append(tx.to) - w.append(tx.value) - w.append(tx.payload) - w.append(tx.V) - w.append(tx.R) - w.append(tx.S) - -proc appendTxEip2930(w: var RlpWriter, tx: Transaction) = - w.startList(11) - w.append(tx.chainId.uint64) - w.append(tx.nonce) - w.append(tx.gasPrice) - w.append(tx.gasLimit) - w.append(tx.to) - w.append(tx.value) - w.append(tx.payload) - w.append(tx.accessList) - w.append(tx.V) - w.append(tx.R) - w.append(tx.S) - -proc appendTxEip1559(w: var RlpWriter, tx: Transaction) = - w.startList(12) - w.append(tx.chainId.uint64) - w.append(tx.nonce) - w.append(tx.maxPriorityFee) - w.append(tx.maxFee) - w.append(tx.gasLimit) - w.append(tx.to) - w.append(tx.value) - w.append(tx.payload) - w.append(tx.accessList) - w.append(tx.V) - w.append(tx.R) - w.append(tx.S) - -proc appendTxEip4844(w: var RlpWriter, tx: Transaction) = - w.startList(14) - w.append(tx.chainId.uint64) - w.append(tx.nonce) - w.append(tx.maxPriorityFee) - w.append(tx.maxFee) - w.append(tx.gasLimit) - w.append(tx.to) - w.append(tx.value) - w.append(tx.payload) - w.append(tx.accessList) - w.append(tx.maxFeePerBlobGas) - w.append(tx.versionedHashes) - w.append(tx.V) - w.append(tx.R) - w.append(tx.S) - -proc appendTxPayload(w: var RlpWriter, tx: Transaction) = - case tx.txType - of TxLegacy: - w.appendTxLegacy(tx) - of TxEip2930: - w.appendTxEip2930(tx) - of TxEip1559: - w.appendTxEip1559(tx) - of TxEip4844: - w.appendTxEip4844(tx) - -proc append*(w: var RlpWriter, tx: Transaction) = - if tx.txType != TxLegacy: - w.append(tx.txType) - w.appendTxPayload(tx) - -proc append(w: var RlpWriter, networkPayload: NetworkPayload) = - w.append(networkPayload.blobs) - w.append(networkPayload.commitments) - w.append(networkPayload.proofs) - -proc append*(w: var RlpWriter, tx: PooledTransaction) = - if tx.tx.txType != TxLegacy: - w.append(tx.tx.txType) - if tx.networkPayload != nil: - w.startList(4) # spec: rlp([tx_payload, blobs, commitments, proofs]) - w.appendTxPayload(tx.tx) - if tx.networkPayload != nil: - w.append(tx.networkPayload) - -template read[T](rlp: var Rlp, val: var T) = - val = rlp.read(type val) +proc append*(w: var RlpWriter, val: List) = + w.append(distinctBase(val)) proc read[T](rlp: var Rlp, val: var Option[T]) = if rlp.blobLen != 0: @@ -167,214 +95,21 @@ proc read[T](rlp: var Rlp, val: var Option[T]) = else: rlp.skipElem -proc readTxLegacy(rlp: var Rlp, tx: var Transaction) = - tx.txType = TxLegacy - rlp.tryEnterList() - rlp.read(tx.nonce) - rlp.read(tx.gasPrice) - rlp.read(tx.gasLimit) - rlp.read(tx.to) - rlp.read(tx.value) - rlp.read(tx.payload) - rlp.read(tx.V) - rlp.read(tx.R) - rlp.read(tx.S) +proc read[T](rlp: var Rlp, val: var Opt[T]) = + if rlp.blobLen != 0: + val.ok rlp.read(T) + else: + rlp.skipElem -proc readTxEip2930(rlp: var Rlp, tx: var Transaction) = - tx.txType = TxEip2930 - rlp.tryEnterList() - tx.chainId = rlp.read(uint64).ChainId - rlp.read(tx.nonce) - rlp.read(tx.gasPrice) - rlp.read(tx.gasLimit) - rlp.read(tx.to) - rlp.read(tx.value) - rlp.read(tx.payload) - rlp.read(tx.accessList) - rlp.read(tx.V) - rlp.read(tx.R) - rlp.read(tx.S) - -proc readTxEip1559(rlp: var Rlp, tx: var Transaction) = - tx.txType = TxEip1559 - rlp.tryEnterList() - tx.chainId = rlp.read(uint64).ChainId - rlp.read(tx.nonce) - rlp.read(tx.maxPriorityFee) - rlp.read(tx.maxFee) - rlp.read(tx.gasLimit) - rlp.read(tx.to) - rlp.read(tx.value) - rlp.read(tx.payload) - rlp.read(tx.accessList) - rlp.read(tx.V) - rlp.read(tx.R) - rlp.read(tx.S) - -proc readTxEip4844(rlp: var Rlp, tx: var Transaction) = - tx.txType = TxEip4844 - rlp.tryEnterList() - tx.chainId = rlp.read(uint64).ChainId - rlp.read(tx.nonce) - rlp.read(tx.maxPriorityFee) - rlp.read(tx.maxFee) - rlp.read(tx.gasLimit) - rlp.read(tx.to) - rlp.read(tx.value) - rlp.read(tx.payload) - rlp.read(tx.accessList) - rlp.read(tx.maxFeePerBlobGas) - rlp.read(tx.versionedHashes) - rlp.read(tx.V) - rlp.read(tx.R) - rlp.read(tx.S) - -proc readTxType(rlp: var Rlp): TxType = - if rlp.isList: - raise newException(RlpTypeMismatch, - "Transaction type expected, but source RLP is a list") - - # EIP-2718: We MUST decode the first byte as a byte, not `rlp.read(int)`. - # If decoded with `rlp.read(int)`, bad transaction data (from the network) - # or even just incorrectly framed data for other reasons fails with - # any of these misleading error messages: - # - "Message too large to fit in memory" - # - "Number encoded with a leading zero" - # - "Read past the end of the RLP stream" - # - "Small number encoded in a non-canonical way" - # - "Attempt to read an Int value past the RLP end" - # - "The RLP contains a larger than expected Int value" - if not rlp.isSingleByte: - if not rlp.hasData: - raise newException(MalformedRlpError, - "Transaction expected but source RLP is empty") +proc read*[E, N](rlp: var Rlp, T: typedesc[List[E, N]]): T = + let v = rlp.read(seq[E]) + if v.len > N: raise newException(MalformedRlpError, - "TypedTransaction type byte is out of range, must be 0x00 to 0x7f") - let txType = rlp.getByteValue - rlp.position += 1 + "List[" & $E & ", Limit " & $N & "] cannot fit " & $v.len & " items") + List[E, N].init(v) - var txVal: TxType - if checkedEnumAssign(txVal, txType): - return txVal - - raise newException(UnsupportedRlpError, - "TypedTransaction type must be 1, 2, or 3 in this version, got " & $txType) - -proc readTxPayload(rlp: var Rlp, tx: var Transaction, txType: TxType) = - case txType - of TxLegacy: - raise newException(RlpTypeMismatch, - "LegacyTransaction should not be wrapped in a list") - of TxEip2930: - rlp.readTxEip2930(tx) - of TxEip1559: - rlp.readTxEip1559(tx) - of TxEip4844: - rlp.readTxEip4844(tx) - -proc readTxTyped(rlp: var Rlp, tx: var Transaction) = - let txType = rlp.readTxType() - rlp.readTxPayload(tx, txType) - -proc read*(rlp: var Rlp, T: type Transaction): T = - # Individual transactions are encoded and stored as either `RLP([fields..])` - # for legacy transactions, or `Type || RLP([fields..])`. Both of these - # encodings are byte sequences. The part after `Type` doesn't have to be - # RLP in theory, but all types so far use RLP. EIP-2718 covers this. - if rlp.isList: - rlp.readTxLegacy(result) - else: - rlp.readTxTyped(result) - -proc read(rlp: var Rlp, T: type NetworkPayload): T = - result = NetworkPayload() - rlp.read(result.blobs) - rlp.read(result.commitments) - rlp.read(result.proofs) - -proc readTxTyped(rlp: var Rlp, tx: var PooledTransaction) = - let - txType = rlp.readTxType() - hasNetworkPayload = - if txType == TxEip4844: - rlp.listLen == 4 - else: - false - if hasNetworkPayload: - rlp.tryEnterList() # spec: rlp([tx_payload, blobs, commitments, proofs]) - rlp.readTxPayload(tx.tx, txType) - if hasNetworkPayload: - rlp.read(tx.networkPayload) - -proc read*(rlp: var Rlp, T: type PooledTransaction): T = - if rlp.isList: - rlp.readTxLegacy(result.tx) - else: - rlp.readTxTyped(result) - -proc read*( - rlp: var Rlp, - T: (type seq[Transaction]) | (type openArray[Transaction]) -): seq[Transaction] = - # In arrays (sequences), transactions are encoded as either `RLP([fields..])` - # for legacy transactions, or `RLP(Type || RLP([fields..]))` for all typed - # transactions to date. Spot the extra `RLP(..)` blob encoding, to make it - # valid RLP inside a larger RLP. EIP-2976 covers this, "Typed Transactions - # over Gossip", although it's not very clear about the blob encoding. - # - # In practice the extra `RLP(..)` applies to all arrays/sequences of - # transactions. In principle, all aggregates (objects etc.), but - # arrays/sequences are enough. In `eth/65` protocol this is essential for - # the correct encoding/decoding of `Transactions`, `NewBlock`, and - # `PooledTransactions` network calls. We need a type match on both - # `openArray[Transaction]` and `seq[Transaction]` to catch all cases. - if not rlp.isList: - raise newException(RlpTypeMismatch, - "Transaction list expected, but source RLP is not a list") - for item in rlp: - var tx: Transaction - if item.isList: - item.readTxLegacy(tx) - else: - var rr = rlpFromBytes(rlp.read(Blob)) - rr.readTxTyped(tx) - result.add tx - -proc read*( - rlp: var Rlp, - T: (type seq[PooledTransaction]) | (type openArray[PooledTransaction]) -): seq[PooledTransaction] = - if not rlp.isList: - raise newException(RlpTypeMismatch, - "PooledTransaction list expected, but source RLP is not a list") - for item in rlp: - var tx: PooledTransaction - if item.isList: - item.readTxLegacy(tx.tx) - else: - var rr = rlpFromBytes(rlp.read(Blob)) - rr.readTxTyped(tx) - result.add tx - -proc append*(rlpWriter: var RlpWriter, - txs: seq[Transaction] | openArray[Transaction]) {.inline.} = - # See above about encoding arrays/sequences of transactions. - rlpWriter.startList(txs.len) - for tx in txs: - if tx.txType == TxLegacy: - rlpWriter.append(tx) - else: - rlpWriter.append(rlp.encode(tx)) - -proc append*( - rlpWriter: var RlpWriter, - txs: seq[PooledTransaction] | openArray[PooledTransaction]) {.inline.} = - rlpWriter.startList(txs.len) - for tx in txs: - if tx.tx.txType == TxLegacy: - rlpWriter.append(tx) - else: - rlpWriter.append(rlp.encode(tx)) +template read*[T](rlp: var Rlp, val: var T) = + val = rlp.read(type val) proc append*(w: var RlpWriter, rec: Receipt) = if rec.receiptType in {Eip2930Receipt, Eip1559Receipt, Eip4844Receipt}: @@ -518,9 +253,6 @@ proc append*(rlpWriter: var RlpWriter, t: EthTime) {.inline.} = proc rlpHash*[T](v: T): Hash256 = keccakHash(rlp.encode(v)) -proc rlpHash*(tx: PooledTransaction): Hash256 = - keccakHash(rlp.encode(tx.tx)) - func blockHash*(h: BlockHeader): KeccakHash {.inline.} = rlpHash(h) proc append*(rlpWriter: var RlpWriter, id: NetworkId) = diff --git a/eth/common/transaction.nim b/eth/common/transaction.nim index a5caaea..8b4bd0e 100644 --- a/eth/common/transaction.nim +++ b/eth/common/transaction.nim @@ -1,5 +1,16 @@ +# Copyright (c) 2018-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. + +{.push raises: [].} + import - ./eth_types_rlp + std/typetraits, + ssz_serialization, + ./eth_types_rlp, + ".."/[keys, rlp] export eth_types_rlp @@ -20,102 +31,839 @@ type proc effectiveGasTip*(tx: Transaction; baseFee: GasPrice): GasPriceEx = ## The effective miner gas tip for the globally argument `baseFee`. The ## result (which is a price per gas) might well be negative. - if tx.txType != TxEip1559: - (tx.gasPrice - baseFee.int64).GasPriceEx - else: - # London, EIP1559 - min(tx.maxPriorityFee, tx.maxFee - baseFee.int64).GasPriceEx + let + maxFee = tx.payload.max_fee_per_gas + maxPriorityFee = tx.payload.max_priority_fee_per_gas.get(maxFee) + min( + maxPriorityFee.truncate(int64), + maxFee.truncate(int64) - baseFee.int64).GasPriceEx proc effectiveGasTip*(tx: Transaction; baseFee: UInt256): GasPriceEx = ## Variant of `effectiveGasTip()` tx.effectiveGasTip(baseFee.truncate(uint64).GasPrice) -func rlpEncodeLegacy(tx: Transaction): auto = +# https://eips.ethereum.org/EIPS/eip-6493#transaction-validation +func ecdsa_pack_signature*( + y_parity: bool, r: UInt256, s: UInt256): array[ECDSA_SIGNATURE_SIZE, byte] = + var res: array[ECDSA_SIGNATURE_SIZE, byte] + res[0 ..< 32] = r.toBytesBE() + res[32 ..< 64] = s.toBytesBE() + res[64] = if y_parity: 0x01 else: 0x00 + res + +func ecdsa_unpack_signature*( + signature: array[ECDSA_SIGNATURE_SIZE, byte] +): tuple[y_parity: bool, r: UInt256, s: UInt256] = + ( + y_parity: signature[64] != 0, + r: UInt256.fromBytesBE(signature[0 ..< 32]), + s: UInt256.fromBytesBE(signature[32 ..< 64])) + +const SECP256K1N* = + 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141.u256 + +func ecdsa_validate_signature( + signature: array[ECDSA_SIGNATURE_SIZE, byte]): Opt[void] = + static: doAssert signature.len == 65 + if signature[64] > 1: return err() + let (_, r, s) = ecdsa_unpack_signature(signature) + if r >= SECP256K1N: return err() + if s < UInt256.one or s >= SECP256K1N: return err() + ok() + +func ecdsa_recover_from_address*( + signature: array[ECDSA_SIGNATURE_SIZE, byte], + sig_hash: Hash256): Opt[EthAddress] = + let + recover_sig = Signature.fromRaw(signature).valueOr: + return Opt.none EthAddress + public_key = recover_sig.recover(sig_hash.data.SkMessage).valueOr: + return Opt.none EthAddress + Opt.some public_key.toCanonicalAddress() + +# https://github.com/ethereum/EIPs/blob/master/assets/eip-6493/tx_hashes.py +func compute_sig_hash*( + tx: ReplayableTransactionPayload, chain_id: ChainId): Hash256 = var w = initRlpWriter() w.startList(6) w.append(tx.nonce) - w.append(tx.gasPrice) - w.append(tx.gasLimit) + w.append(tx.max_fee_per_gas) + w.append(tx.gas) w.append(tx.to) w.append(tx.value) - w.append(tx.payload) - w.finish() + w.append(tx.input) + keccakHash(w.finish()) -func rlpEncodeEip155(tx: Transaction): auto = - let chainId = (tx.V - EIP155_CHAIN_ID_OFFSET) div 2 +func append_tx(w: var RlpWriter, tx: ReplayableTransaction, chain_id: ChainId) = + let + (y_parity, r, s) = ecdsa_unpack_signature(tx.signature.ecdsa_signature) + v = (if y_parity: 28.u256 else: 27.u256) + w.startList(9) + w.append(tx.payload.nonce) + w.append(tx.payload.max_fee_per_gas) + w.append(tx.payload.gas) + w.append(tx.payload.to) + w.append(tx.payload.value) + w.append(tx.payload.input) + w.append(v) + w.append(r) + w.append(s) + +func compute_tx_hash*(tx: ReplayableTransaction, chain_id: ChainId): Hash256 = + var w = initRlpWriter() + w.append_tx(tx, chain_id) + keccakHash(w.finish()) + +func compute_sig_hash*( + tx: LegacyTransactionPayload, chain_id: ChainId): Hash256 = var w = initRlpWriter() w.startList(9) w.append(tx.nonce) - w.append(tx.gasPrice) - w.append(tx.gasLimit) + w.append(tx.max_fee_per_gas) + w.append(tx.gas) w.append(tx.to) w.append(tx.value) - w.append(tx.payload) - w.append(chainId) + w.append(tx.input) + w.append(chain_id) w.append(0) w.append(0) - w.finish() + keccakHash(w.finish()) -func rlpEncodeEip2930(tx: Transaction): auto = +func append_tx(w: var RlpWriter, tx: LegacyTransaction, chain_id: ChainId) = + let + (y_parity, r, s) = ecdsa_unpack_signature(tx.signature.ecdsa_signature) + v = distinctBase(chain_id).u256 * 2 + (if y_parity: 36.u256 else: 35.u256) + w.startList(9) + w.append(tx.payload.nonce) + w.append(tx.payload.max_fee_per_gas) + w.append(tx.payload.gas) + w.append(tx.payload.to) + w.append(tx.payload.value) + w.append(tx.payload.input) + w.append(v) + w.append(r) + w.append(s) + +func compute_tx_hash*(tx: LegacyTransaction, chain_id: ChainId): Hash256 = + var w = initRlpWriter() + w.append_tx(tx, chain_id) + keccakHash(w.finish()) + +func compute_sig_hash*( + tx: Eip2930TransactionPayload, chain_id: ChainId): Hash256 = var w = initRlpWriter() w.append(TxEip2930) w.startList(8) - w.append(tx.chainId.uint64) + w.append(chain_id) w.append(tx.nonce) - w.append(tx.gasPrice) - w.append(tx.gasLimit) + w.append(tx.max_fee_per_gas) + w.append(tx.gas) w.append(tx.to) w.append(tx.value) - w.append(tx.payload) - w.append(tx.accessList) - w.finish() + w.append(tx.input) + w.append(tx.access_list) + keccakHash(w.finish()) -func rlpEncodeEip1559(tx: Transaction): auto = +func append_tx(w: var RlpWriter, tx: Eip2930Transaction, chain_id: ChainId) = + let (y_parity, r, s) = ecdsa_unpack_signature(tx.signature.ecdsa_signature) + w.startList(11) + w.append(chain_id) + w.append(tx.payload.nonce) + w.append(tx.payload.max_fee_per_gas) + w.append(tx.payload.gas) + w.append(tx.payload.to) + w.append(tx.payload.value) + w.append(tx.payload.input) + w.append(tx.payload.access_list) + w.append(y_parity) + w.append(r) + w.append(s) + +func compute_tx_hash*(tx: Eip2930Transaction, chain_id: ChainId): Hash256 = + var w = initRlpWriter() + w.append(TxEip2930) + w.append_tx(tx, chain_id) + keccakHash(w.finish()) + +func compute_sig_hash*( + tx: Eip1559TransactionPayload, chain_id: ChainId): Hash256 = var w = initRlpWriter() w.append(TxEip1559) w.startList(9) - w.append(tx.chainId.uint64) + w.append(chain_id) w.append(tx.nonce) - w.append(tx.maxPriorityFee) - w.append(tx.maxFee) - w.append(tx.gasLimit) + w.append(tx.max_priority_fee_per_gas) + w.append(tx.max_fee_per_gas) + w.append(tx.gas) w.append(tx.to) w.append(tx.value) - w.append(tx.payload) - w.append(tx.accessList) - w.finish() + w.append(tx.input) + w.append(tx.access_list) + keccakHash(w.finish()) -func rlpEncodeEip4844(tx: Transaction): auto = +func append_tx(w: var RlpWriter, tx: Eip1559Transaction, chain_id: ChainId) = + let (y_parity, r, s) = ecdsa_unpack_signature(tx.signature.ecdsa_signature) + w.startList(12) + w.append(chain_id) + w.append(tx.payload.nonce) + w.append(tx.payload.max_priority_fee_per_gas) + w.append(tx.payload.max_fee_per_gas) + w.append(tx.payload.gas) + w.append(tx.payload.to) + w.append(tx.payload.value) + w.append(tx.payload.input) + w.append(tx.payload.access_list) + w.append(y_parity) + w.append(r) + w.append(s) + +func compute_tx_hash*(tx: Eip1559Transaction, chain_id: ChainId): Hash256 = + var w = initRlpWriter() + w.append(TxEip1559) + w.append_tx(tx, chain_id) + keccakHash(w.finish()) + +func compute_sig_hash*( + tx: Eip4844TransactionPayload, chain_id: ChainId): Hash256 = var w = initRlpWriter() w.append(TxEip4844) w.startList(11) - w.append(tx.chainId.uint64) + w.append(chain_id) w.append(tx.nonce) - w.append(tx.maxPriorityFee) - w.append(tx.maxFee) - w.append(tx.gasLimit) + w.append(tx.max_priority_fee_per_gas) + w.append(tx.max_fee_per_gas) + w.append(tx.gas) w.append(tx.to) w.append(tx.value) - w.append(tx.payload) - w.append(tx.accessList) - w.append(tx.maxFeePerBlobGas) - w.append(tx.versionedHashes) - w.finish() + w.append(tx.input) + w.append(tx.access_list) + w.append(tx.max_fee_per_blob_gas) + w.append(tx.blob_versioned_hashes) + keccakHash(w.finish()) -func rlpEncode*(tx: Transaction): auto = - case tx.txType - of TxLegacy: - if tx.V >= EIP155_CHAIN_ID_OFFSET: - tx.rlpEncodeEip155 - else: - tx.rlpEncodeLegacy - of TxEip2930: - tx.rlpEncodeEip2930 - of TxEip1559: - tx.rlpEncodeEip1559 +func append_tx(w: var RlpWriter, tx: Eip4844Transaction, chain_id: ChainId) = + let (y_parity, r, s) = ecdsa_unpack_signature(tx.signature.ecdsa_signature) + w.startList(14) + w.append(tx.payload.nonce) + w.append(tx.payload.max_priority_fee_per_gas) + w.append(tx.payload.max_fee_per_gas) + w.append(tx.payload.gas) + w.append(tx.payload.to) + w.append(tx.payload.value) + w.append(tx.payload.input) + w.append(tx.payload.access_list) + w.append(tx.payload.max_fee_per_blob_gas) + w.append(tx.payload.blob_versioned_hashes) + w.append(y_parity) + w.append(r) + w.append(s) + +func compute_tx_hash*(tx: Eip4844Transaction, chain_id: ChainId): Hash256 = + var w = initRlpWriter() + w.append(TxEip4844) + w.append_tx(tx, chain_id) + keccakHash(w.finish()) + +func compute_sig_hash*(tx: AnyTransactionPayload, chain_id: ChainId): Hash256 = + withTxPayloadVariant(tx): + txPayloadVariant.compute_sig_hash(chain_id) + +func compute_sig_hash*(tx: TransactionPayload, chain_id: ChainId): Hash256 = + let anyTx = AnyTransactionPayload.fromOneOfBase(tx).valueOr: + raiseAssert "Cannot get sig hash for invalid `TransactionPayload`: " & $tx + anyTx.compute_sig_hash(chain_id) + +func compute_tx_hash*(tx: AnyTransaction, chain_id: ChainId): Hash256 = + withTxVariant(tx): + txVariant.compute_tx_hash(chain_id) + +func compute_tx_hash*(tx: Transaction, chain_id: ChainId): Hash256 = + let anyTx = AnyTransaction.fromOneOfBase(tx).valueOr: + raiseAssert "Cannot get tx hash for invalid `Transaction`: " & $tx + anyTx.compute_tx_hash(chain_id) + +func validate_transaction*( + tx: AnyTransactionVariant, chain_id: ChainId): Opt[void] = + ? ecdsa_validate_signature(tx.signature.ecdsa_signature) + let from_address = ? ecdsa_recover_from_address( + tx.signature.ecdsa_signature, + tx.payload.compute_sig_hash(chain_id)) + if tx.signature.from_address != from_address: + return err() + ok() + +func toBytes(tx: AnyTransaction, chain_id: ChainId): seq[byte] = + withTxVariant(tx): + case txKind + of TransactionKind.Eip4844: + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + @[TxEip4844.ord.uint8] & w.finish() + of TransactionKind.Eip1559: + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + @[TxEip1559.ord.uint8] & w.finish() + of TransactionKind.Eip2930: + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + @[TxEip2930.ord.uint8] & w.finish() + of TransactionKind.Legacy: + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + w.finish() + of TransactionKind.Replayable: + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + w.finish() + +func toBytes*(tx: Transaction, chain_id: ChainId): seq[byte] = + let anyTx = AnyTransaction.fromOneOfBase(tx).valueOr: + raiseAssert "Cannot serialize invalid `Transaction`: " & $tx + anyTx.toBytes(chain_id) + +func toBytes*(txs: openArray[Transaction], chain_id: ChainId): seq[byte] = + var writer = initRlpWriter() + for tx in txs: + let anyTx = AnyTransaction.fromOneOfBase(tx).valueOr: + raiseAssert "Cannot serialize an invalid `Transaction`: " & $tx + withTxVariant(anyTx): + case txKind + of TransactionKind.Eip4844: + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + writer.append @[TxEip4844.ord.uint8] & w.finish() + of TransactionKind.Eip1559: + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + writer.append @[TxEip1559.ord.uint8] & w.finish() + of TransactionKind.Eip2930: + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + writer.append @[TxEip2930.ord.uint8] & w.finish() + of TransactionKind.Legacy: + writer.append_tx(txVariant, chain_id) + of TransactionKind.Replayable: + writer.append_tx(txVariant, chain_id) + writer.finish() + +func append_blob_data(w: var RlpWriter, blob_data: NetworkPayload) = + w.append(distinctBase(blob_data.blobs)) + w.append(distinctBase(blob_data.commitments)) + w.append(distinctBase(blob_data.proofs)) + +func toBytes*(tx: PooledTransaction, chain_id: ChainId): seq[byte] = + let anyTx = AnyTransaction.fromOneOfBase(tx.tx).valueOr: + raiseAssert "Cannot serialize an invalid `PooledTransaction`: " & $tx + withTxVariant(anyTx): + case txKind + of TransactionKind.Eip4844: + doAssert tx.blob_data.isSome, "EIP-4844 requires `blob_data`" + var w = initRlpWriter() + w.startList(4) # spec: rlp([tx_payload, blobs, commitments, proofs]) + w.append_tx(txVariant, chain_id) + w.append_blob_data(tx.blob_data.unsafeGet) + @[TxEip4844.ord.uint8] & w.finish() + of TransactionKind.Eip1559: + doAssert tx.blob_data.isNone, "`blob_data` requires EIP-4844" + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + @[TxEip1559.ord.uint8] & w.finish() + of TransactionKind.Eip2930: + doAssert tx.blob_data.isNone, "`blob_data` requires EIP-4844" + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + @[TxEip2930.ord.uint8] & w.finish() + of TransactionKind.Legacy: + doAssert tx.blob_data.isNone, "`blob_data` requires EIP-4844" + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + w.finish() + of TransactionKind.Replayable: + doAssert tx.blob_data.isNone, "`blob_data` requires EIP-4844" + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + w.finish() + +func toBytes*(txs: openArray[PooledTransaction], chain_id: ChainId): seq[byte] = + var writer = initRlpWriter() + for tx in txs: + let anyTx = AnyTransaction.fromOneOfBase(tx.tx).valueOr: + raiseAssert "Cannot serialize an invalid `PooledTransaction`: " & $tx + withTxVariant(anyTx): + case txKind + of TransactionKind.Eip4844: + doAssert tx.blob_data.isSome, "EIP-4844 requires `blob_data`" + var w = initRlpWriter() + w.startList(4) # spec: rlp([tx_payload, blobs, commitments, proofs]) + w.append_tx(txVariant, chain_id) + w.append_blob_data(tx.blob_data.unsafeGet) + writer.append @[TxEip4844.ord.uint8] & w.finish() + of TransactionKind.Eip1559: + doAssert tx.blob_data.isNone, "`blob_data` requires EIP-4844" + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + writer.append @[TxEip1559.ord.uint8] & w.finish() + of TransactionKind.Eip2930: + doAssert tx.blob_data.isNone, "`blob_data` requires EIP-4844" + var w = initRlpWriter() + w.append_tx(txVariant, chain_id) + writer.append @[TxEip2930.ord.uint8] & w.finish() + of TransactionKind.Legacy: + doAssert tx.blob_data.isNone, "`blob_data` requires EIP-4844" + writer.append_tx(txVariant, chain_id) + of TransactionKind.Replayable: + doAssert tx.blob_data.isNone, "`blob_data` requires EIP-4844" + writer.append_tx(txVariant, chain_id) + writer.finish() + +# https://github.com/ethereum/EIPs/blob/master/assets/eip-6493/convert.py +func read_tx[T: LegacyTransactionPayload]( + rlp: var Rlp, t: typedesc[T], chain_id: ChainId +): Opt[tuple[tx: T, y_parity: bool, r: UInt256, s: UInt256, has_eip155: bool]] = + try: + var + tx = LegacyTransactionPayload(tx_type: TxLegacy) + v: UInt256 + r: UInt256 + s: UInt256 + rlp.tryEnterList() + rlp.read(tx.nonce) + rlp.read(tx.max_fee_per_gas) + rlp.read(tx.gas) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.input) + rlp.read(v) + rlp.read(r) + rlp.read(s) + let + y_parity = v.isEven + has_eip155 = (v > 28.u256 or v < 27.u256) + if has_eip155: + let expected_v = + distinctBase(chain_id).u256 * 2 + (if y_parity: 36.u256 else: 35.u256) + if v != expected_v: return err() + if r >= SECP256K1N: return err() + if s < UInt256.one or s >= SECP256K1N: return err() + Opt.some (tx: tx, y_parity: y_parity, r: r, s: s, has_eip155: has_eip155) + except RlpError: + err() + +func read_tx[T: Eip2930TransactionPayload]( + rlp: var Rlp, t: typedesc[T], chain_id: ChainId +): Opt[tuple[tx: T, y_parity: bool, r: UInt256, s: UInt256]] = + try: + var + tx = Eip2930TransactionPayload(tx_type: TxEip2930) + y_parity: uint8 + r: UInt256 + s: UInt256 + rlp.tryEnterList() + if rlp.read(ChainId) != chain_id: return err() + rlp.read(tx.nonce) + rlp.read(tx.max_fee_per_gas) + rlp.read(tx.gas) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.input) + rlp.read(tx.access_list) + rlp.read(y_parity) + rlp.read(r) + rlp.read(s) + if y_parity > 1: return err() + if r >= SECP256K1N: return err() + if s < UInt256.one or s >= SECP256K1N: return err() + Opt.some (tx: tx, y_parity: y_parity != 0, r: r, s: s) + except RlpError: + err() + +func read_tx[T: Eip1559TransactionPayload]( + rlp: var Rlp, t: typedesc[T], chain_id: ChainId +): Opt[tuple[tx: T, y_parity: bool, r: UInt256, s: UInt256]] = + try: + var + tx = Eip1559TransactionPayload(tx_type: TxEip1559) + y_parity: uint8 + r: UInt256 + s: UInt256 + rlp.tryEnterList() + if rlp.read(ChainId) != chain_id: return err() + rlp.read(tx.nonce) + rlp.read(tx.max_priority_fee_per_gas) + rlp.read(tx.max_fee_per_gas) + rlp.read(tx.gas) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.input) + rlp.read(tx.access_list) + rlp.read(y_parity) + rlp.read(r) + rlp.read(s) + if y_parity > 1: return err() + if r >= SECP256K1N: return err() + if s < UInt256.one or s >= SECP256K1N: return err() + Opt.some (tx: tx, y_parity: y_parity != 0, r: r, s: s) + except RlpError: + err() + +func read_tx[T: Eip4844TransactionPayload]( + rlp: var Rlp, t: typedesc[T], chain_id: ChainId +): Opt[tuple[tx: T, y_parity: bool, r: UInt256, s: UInt256]] = + try: + var + tx = Eip4844TransactionPayload(tx_type: TxEip4844) + y_parity: uint8 + r: UInt256 + s: UInt256 + rlp.tryEnterList() + if rlp.read(ChainId) != chain_id: return err() + rlp.read(tx.nonce) + rlp.read(tx.max_priority_fee_per_gas) + rlp.read(tx.max_fee_per_gas) + rlp.read(tx.gas) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.input) + rlp.read(tx.access_list) + rlp.read(tx.max_fee_per_blob_gas) + rlp.read(tx.blob_versioned_hashes) + rlp.read(y_parity) + rlp.read(r) + rlp.read(s) + if y_parity > 1: return err() + if r >= SECP256K1N: return err() + if s < UInt256.one or s >= SECP256K1N: return err() + Opt.some (tx: tx, y_parity: y_parity != 0, r: r, s: s) + except RlpError: + err() + +func read_tx( + rlp: var Rlp, + chain_id: ChainId, + tx_type: TxType): Opt[AnyTransaction] = + case tx_type of TxEip4844: - tx.rlpEncodeEip4844 + let (tx, y_parity, r, s) = + ? rlp.read_tx(Eip4844TransactionPayload, chain_id) + var signature: TransactionSignature + signature.ecdsa_signature = ecdsa_pack_signature(y_parity, r, s) + signature.from_address = ? ecdsa_recover_from_address( + signature.ecdsa_signature, tx.compute_sig_hash(chain_id)) + Opt.some AnyTransaction.init( + Eip4844Transaction(payload: tx, signature: signature)) + of TxEip1559: + let (tx, y_parity, r, s) = + ? rlp.read_tx(Eip1559TransactionPayload, chain_id) + var signature: TransactionSignature + signature.ecdsa_signature = ecdsa_pack_signature(y_parity, r, s) + signature.from_address = ? ecdsa_recover_from_address( + signature.ecdsa_signature, tx.compute_sig_hash(chain_id)) + Opt.some AnyTransaction.init( + Eip1559Transaction(payload: tx, signature: signature)) + of TxEip2930: + let (tx, y_parity, r, s) = + ? rlp.read_tx(Eip2930TransactionPayload, chain_id) + var signature: TransactionSignature + signature.ecdsa_signature = ecdsa_pack_signature(y_parity, r, s) + signature.from_address = ? ecdsa_recover_from_address( + signature.ecdsa_signature, tx.compute_sig_hash(chain_id)) + Opt.some AnyTransaction.init( + Eip2930Transaction(payload: tx, signature: signature)) + of TxLegacy: + let (tx, y_parity, r, s, has_eip155) = + ? rlp.read_tx(LegacyTransactionPayload, chain_id) + if has_eip155: + var signature: TransactionSignature + signature.ecdsa_signature = ecdsa_pack_signature(y_parity, r, s) + signature.from_address = ? ecdsa_recover_from_address( + signature.ecdsa_signature, tx.compute_sig_hash(chain_id)) + Opt.some AnyTransaction.init( + LegacyTransaction(payload: tx, signature: signature)) + else: + let tx = ReplayableTransactionPayload( + nonce: tx.nonce, + max_fee_per_gas: tx.max_fee_per_gas, + gas: tx.gas, + to: tx.to, + value: tx.value, + input: tx.input) + var signature: TransactionSignature + signature.ecdsa_signature = ecdsa_pack_signature(y_parity, r, s) + signature.from_address = ? ecdsa_recover_from_address( + signature.ecdsa_signature, tx.compute_sig_hash(chain_id)) + Opt.some AnyTransaction.init( + ReplayableTransaction(payload: tx, signature: signature)) -func txHashNoSignature*(tx: Transaction): Hash256 = - # Hash transaction without signature - keccakHash(rlpEncode(tx)) +func fromBytes( + T: typedesc[AnyTransaction], + data: openArray[byte], + chain_id: ChainId): Opt[AnyTransaction] = + if data.len < 1: + return Opt.none AnyTransaction + case data[0] + of TxEip4844.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = rlp.read_tx(chain_id, TxEip4844) + if tx.isSome and rlp.hasData: + return Opt.none AnyTransaction + tx + of TxEip1559.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = rlp.read_tx(chain_id, TxEip1559) + if tx.isSome and rlp.hasData: + return Opt.none AnyTransaction + tx + of TxEip2930.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = rlp.read_tx(chain_id, TxEip2930) + if tx.isSome and rlp.hasData: + return Opt.none AnyTransaction + tx + of 0xc0 .. 0xfe: + var rlp = rlpFromBytes(data) + let tx = rlp.read_tx(chain_id, TxLegacy) + if tx.isSome and rlp.hasData: + return Opt.none AnyTransaction + tx + else: + Opt.none AnyTransaction + +func fromBytes*( + T: typedesc[Transaction], + data: openArray[byte], + chain_id: ChainId): Opt[Transaction] = + let tx = ? AnyTransaction.fromBytes(data, chain_id) + Opt.some tx.toOneOfBase() + +func fromBytesWrapped( + T: typedesc[AnyTransaction], + data: openArray[byte], + chain_id: ChainId): Opt[AnyTransaction] = + # In arrays (sequences), transactions are encoded as either `RLP([fields..])` + # for legacy transactions, or `RLP(Type || RLP([fields..]))` for all typed + # transactions to date. Spot the extra `RLP(..)` blob encoding, to make it + # valid RLP inside a larger RLP. EIP-2976 covers this, "Typed Transactions + # over Gossip", although it's not very clear about the blob encoding. + # + # In practice the extra `RLP(..)` applies to all arrays/sequences of + # transactions. In principle, all aggregates (objects etc.), but + # arrays/sequences are enough. In `eth/65` protocol this is essential for + # the correct encoding/decoding of `Transactions`, `NewBlock`, and + # `PooledTransactions` network calls. We need a type match on both + # `openArray[Transaction]` and `seq[Transaction]` to catch all cases. + var rlp = rlpFromBytes(data) + if rlp.isList: + let tx = rlp.read_tx(chain_id, TxLegacy) + if tx.isSome and rlp.hasData: + return Opt.none AnyTransaction + tx + else: + try: + var data = rlp.read(Blob) + if rlp.hasData: + return Opt.none AnyTransaction + if data.len < 1: + return Opt.none AnyTransaction + case data[0] + of TxEip4844.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = rlp.read_tx(chain_id, TxEip4844) + if tx.isSome and rlp.hasData: + return Opt.none AnyTransaction + tx + of TxEip1559.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = rlp.read_tx(chain_id, TxEip1559) + if tx.isSome and rlp.hasData: + return Opt.none AnyTransaction + tx + of TxEip2930.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = rlp.read_tx(chain_id, TxEip2930) + if tx.isSome and rlp.hasData: + return Opt.none AnyTransaction + tx + else: + Opt.none AnyTransaction + except RlpError: + Opt.none AnyTransaction + +func fromBytesWrapped( + T: typedesc[Transaction], + data: openArray[byte], + chain_id: ChainId): Opt[Transaction] = + let tx = ? AnyTransaction.fromBytesWrapped(data, chain_id) + Opt.some tx.toOneOfBase() + +func fromBytes*( + T: typedesc[seq[Transaction]], + data: openArray[byte], + chain_id: ChainId): Opt[seq[Transaction]] = + var rlp = rlpFromBytes(data) + if not rlp.isList: + return Opt.none seq[Transaction] + try: + var res: seq[Transaction] + for item in rlp: + res.add(? Transaction.fromBytesWrapped(item.toBytes(), chain_id)) + if rlp.hasData: + return Opt.none seq[Transaction] + Opt.some res + except RlpError: + Opt.none seq[Transaction] + +func read_blob_data(rlp: var Rlp): Opt[NetworkPayload] = + try: + var + blobs: seq[NetworkBlob] + commitments: seq[KzgCommitment] + proofs: seq[KzgProof] + rlp.read(blobs) + rlp.read(commitments) + rlp.read(proofs) + Opt.some NetworkPayload( + blobs: List[NetworkBlob, MAX_BLOB_COMMITMENTS_PER_BLOCK] + .init blobs, + commitments: List[KzgCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + .init commitments, + proofs: List[KzgProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + .init proofs) + except RlpError: + Opt.none NetworkPayload + +func fromBytes*( + T: typedesc[PooledTransaction], + data: openArray[byte], + chain_id: ChainId): Opt[PooledTransaction] = + if data.len < 1: + return Opt.none PooledTransaction + try: + case data[0] + of TxEip4844.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + rlp.tryEnterList() # spec: rlp([tx_payload, blobs, commitments, proofs]) + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxEip4844)).toOneOfBase(), + blob_data: Opt.some(? rlp.read_blob_data())) + if rlp.hasData: + return Opt.none PooledTransaction + tx + of TxEip1559.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxEip1559)).toOneOfBase()) + if rlp.hasData: + return Opt.none PooledTransaction + tx + of TxEip2930.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxEip2930)).toOneOfBase()) + if rlp.hasData: + return Opt.none PooledTransaction + tx + of 0xc0 .. 0xfe: + var rlp = rlpFromBytes(data) + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxLegacy)).toOneOfBase()) + if rlp.hasData: + return Opt.none PooledTransaction + tx + else: + Opt.none PooledTransaction + except RlpError: + Opt.none PooledTransaction + +func fromBytesWrapped( + T: typedesc[PooledTransaction], + data: openArray[byte], + chain_id: ChainId): Opt[PooledTransaction] = + # In arrays (sequences), transactions are encoded as either `RLP([fields..])` + # for legacy transactions, or `RLP(Type || RLP([fields..]))` for all typed + # transactions to date. Spot the extra `RLP(..)` blob encoding, to make it + # valid RLP inside a larger RLP. EIP-2976 covers this, "Typed Transactions + # over Gossip", although it's not very clear about the blob encoding. + # + # In practice the extra `RLP(..)` applies to all arrays/sequences of + # transactions. In principle, all aggregates (objects etc.), but + # arrays/sequences are enough. In `eth/65` protocol this is essential for + # the correct encoding/decoding of `Transactions`, `NewBlock`, and + # `PooledTransactions` network calls. We need a type match on both + # `openArray[Transaction]` and `seq[Transaction]` to catch all cases. + var rlp = rlpFromBytes(data) + if rlp.isList: + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxLegacy)).toOneOfBase()) + if tx.isSome and rlp.hasData: + return Opt.none PooledTransaction + tx + else: + try: + var data = rlp.read(Blob) + if rlp.hasData: + return Opt.none PooledTransaction + if data.len < 1: + return Opt.none PooledTransaction + case data[0] + of TxEip4844.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + rlp.tryEnterList() + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxEip4844)).toOneOfBase(), + blob_data: Opt.some(? rlp.read_blob_data())) + if rlp.hasData: + return Opt.none PooledTransaction + tx + of TxEip1559.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxEip1559)).toOneOfBase()) + if rlp.hasData: + return Opt.none PooledTransaction + tx + of TxEip2930.ord: + var rlp = rlpFromBytes(data[1 ..< data.len]) + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxEip2930)).toOneOfBase()) + if rlp.hasData: + return Opt.none PooledTransaction + tx + of 0xc0 .. 0xfe: + var rlp = rlpFromBytes(data) + let tx = Opt.some PooledTransaction( + tx: (? rlp.read_tx(chain_id, TxLegacy)).toOneOfBase()) + if rlp.hasData: + return Opt.none PooledTransaction + tx + else: + Opt.none PooledTransaction + except RlpError: + Opt.none PooledTransaction + +func fromBytes*( + T: typedesc[seq[PooledTransaction]], + data: openArray[byte], + chain_id: ChainId): Opt[seq[PooledTransaction]] = + var rlp = rlpFromBytes(data) + if not rlp.isList: + return Opt.none seq[PooledTransaction] + try: + var res: seq[PooledTransaction] + for item in rlp: + res.add(? PooledTransaction.fromBytesWrapped(item.toBytes(), chain_id)) + if rlp.hasData: + return Opt.none seq[PooledTransaction] + Opt.some res + except RlpError: + Opt.none seq[PooledTransaction] + +proc signTransaction*( + tx: TransactionPayload, + privateKey: PrivateKey, + chainId: ChainId): Transaction = + var signature: TransactionSignature + signature.ecdsa_signature = privateKey.sign( + tx.compute_sig_hash(chainId).data.SkMessage).toRaw() + signature.from_address = ecdsa_recover_from_address( + signature.ecdsa_signature, tx.compute_sig_hash(chainId)).expect("Sig OK") + Transaction(payload: tx, signature: signature)