diff --git a/.gitignore b/.gitignore index 79857072..433f118d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ nimble.paths #OS specific files **/.DS_Store + +.claude/ diff --git a/eth.nimble b/eth.nimble index ea596ce2..dd84a25c 100644 --- a/eth.nimble +++ b/eth.nimble @@ -21,7 +21,8 @@ requires "nim >= 2.0.10", "unittest2", "results", "minilru", - "snappy" + "snappy", + "ssz_serialization" let nimc = getEnv("NIMC", "nim") # Which nim compiler to use let lang = getEnv("NIMLANG", "c") # Which backend (c/cpp/js) @@ -64,6 +65,9 @@ task test_utp, "Run utp tests": task test_common, "Run common tests": run "tests/common/all_tests", "common" +task test_ssz, "Run SSZ tests": + run "tests/ssz/all_tests", "ssz_suite" + task test, "Run all tests": run "tests/test_bloom", "" @@ -75,6 +79,7 @@ task test, "Run all tests": test_utp_task() test_common_task() + task test_discv5_full, "Run discovery v5 and its dependencies tests": test_rlp_task() test_discv5_task() diff --git a/eth/common/addresses.nim b/eth/common/addresses.nim index db4f27dc..5c87624c 100644 --- a/eth/common/addresses.nim +++ b/eth/common/addresses.nim @@ -11,6 +11,8 @@ ## https://ethereum.org/en/developers/docs/accounts/#account-creation import std/[typetraits, hashes as std_hashes], "."/[base, hashes], stew/assign2 +import ssz_serialization/codec +import ssz_serialization/merkleization export hashes diff --git a/eth/common/hashes.nim b/eth/common/hashes.nim index 0575ab68..007b21c8 100644 --- a/eth/common/hashes.nim +++ b/eth/common/hashes.nim @@ -15,6 +15,8 @@ ## replaced with proof-of-stake. import std/[typetraits, hashes], nimcrypto/keccak, ./base, stew/assign2 +import ssz_serialization/codec +import ssz_serialization/merkleization export hashes, keccak.update, keccak.finish @@ -102,3 +104,11 @@ template withKeccak256*(body: untyped): Hash32 = var h {.inject.}: keccak.keccak256 body h.finish().to(Hash32) + +# template toSszType*(T: type Hash32): auto = +# T.data() + +# proc fromSszBytes*(T: type Hash32, bytes: openArray[byte]): T {.raises: [SszError].} = +# if bytes.len != sizeof(result.data()): +# raiseIncorrectSize T +# copyMem(addr result.data()[0], unsafeAddr bytes[0], sizeof(result.data())) diff --git a/eth/common/headers.nim b/eth/common/headers.nim index 9baacce4..81ab27e9 100644 --- a/eth/common/headers.nim +++ b/eth/common/headers.nim @@ -40,6 +40,7 @@ type parentBeaconBlockRoot*: Opt[Hash32] # EIP-4788 requestsHash*: Opt[Hash32] # EIP-7685 blockAccessListHash*: Opt[Hash32] # EIP-7928 + systemLogsRoot*: Opt[Hash32] # EIP-7685 # starting from EIP-4399, `mixDigest` field is called `prevRandao` template prevRandao*(h: Header): Bytes32 = diff --git a/eth/common/receipts.nim b/eth/common/receipts.nim index 659f9c12..27735de3 100644 --- a/eth/common/receipts.nim +++ b/eth/common/receipts.nim @@ -7,7 +7,7 @@ {.push raises: [].} -import +import ./[addresses, base, hashes, transactions], ../bloom @@ -30,6 +30,7 @@ type # Eip1559Receipt = TxEip1559 # Eip4844Receipt = TxEip4844 # Eip7702Receipt = TxEip7702 + # Eip7807Receipt = TxEip7807 Receipt* = object receiptType* : ReceiptType @@ -39,6 +40,10 @@ type cumulativeGasUsed*: GasInt logsBloom* : Bloom logs* : seq[Log] + # authorities* : seq[Address] + # txGasUsed* : uint64 # Gas used by THIS transaction only + # contactAddress* : Address # Address of the contract being called/created + # origin* : Address #sender address of the transaction StoredReceipt* = object receiptType* : ReceiptType @@ -47,6 +52,17 @@ type hash* : Hash32 cumulativeGasUsed*: GasInt logs* : seq[Log] + eip7807ReceiptType*: Eip7807ReceiptType + authorities* : seq[Address] + txGasUsed* : uint64 # Gas used by THIS transaction only + contactAddress* : Address # Address of the contract being called/created + origin* : Address #sender address of the transaction + + Eip7807ReceiptType* = enum + Eip7807Create + Eip7807Basic + Eip7807SetCode + const LegacyReceipt* = TxLegacy @@ -54,6 +70,7 @@ const Eip1559Receipt* = TxEip1559 Eip4844Receipt* = TxEip4844 Eip7702Receipt* = TxEip7702 + Eip7807Receipt* = TxEip7807 func hasStatus*(rec: Receipt): bool {.inline.} = rec.isHash == false @@ -75,13 +92,19 @@ func logsBloom(logs: openArray[Log]): BloomFilter = res func to*(rec: Receipt, _: type StoredReceipt): StoredReceipt = + # fill in default values for the new fields StoredReceipt( receiptType : rec.receiptType, isHash : rec.isHash, status : rec.status, hash : rec.hash, cumulativeGasUsed : rec.cumulativeGasUsed, - logs : rec.logs + logs : rec.logs, + authorities : @[], + # https://github.com/ethereum/EIPs/blob/676604927b316a44195008e632778d4ca1101deb/EIPS/eip-6466.md?plain=1#L138 + txGasUsed : 0'u64, + contactAddress : default(Address), # default address + origin : default(Address) # default address ) func to*(rec: StoredReceipt, _: type Receipt): Receipt = diff --git a/eth/common/receipts_rlp.nim b/eth/common/receipts_rlp.nim index dae5724a..cb2ab218 100644 --- a/eth/common/receipts_rlp.nim +++ b/eth/common/receipts_rlp.nim @@ -15,7 +15,7 @@ export addresses_rlp, base_rlp, hashes_rlp, receipts, rlp # RLP encoding for Receipt (eth/68) proc append*(w: var RlpWriter, rec: Receipt) = - if rec.receiptType in {Eip2930Receipt, Eip1559Receipt, Eip4844Receipt, Eip7702Receipt}: + if rec.receiptType in {Eip2930Receipt, Eip1559Receipt, Eip4844Receipt, Eip7702Receipt, Eip7807Receipt}: w.appendDetached(rec.receiptType.uint8) w.startList(4) @@ -29,7 +29,15 @@ proc append*(w: var RlpWriter, rec: Receipt) = # RLP encoding for StoredReceipt (eth/69) proc append*(w: var RlpWriter, rec: StoredReceipt) = - w.startList(4) + + let tailLen = + if rec.receiptType == Eip7807Receipt: + case rec.eip7807ReceiptType + of Eip7807Basic: 3 # subtype, origin, txGasUsed + of Eip7807Create: 4 # + contractAddress + of Eip7807SetCode: 4 # + authorities + else: 0 + w.startList(4 + tailLen) w.append(rec.receiptType.uint) if rec.isHash: w.append(rec.hash) @@ -38,6 +46,15 @@ proc append*(w: var RlpWriter, rec: StoredReceipt) = w.append(rec.cumulativeGasUsed) w.append(rec.logs) + if rec.receiptType == Eip7807Receipt: + w.append(rec.eip7807ReceiptType.uint) + w.append(rec.origin) + w.append(rec.txGasUsed) + case rec.eip7807ReceiptType + of Eip7807Create: w.append(rec.contactAddress) + of Eip7807SetCode: w.append(rec.authorities) + of Eip7807Basic: discard + # Decode legacy receipt (eth/68) proc readReceiptLegacy(rlp: var Rlp, receipt: var Receipt) {.raises: [RlpError].} = receipt.receiptType = LegacyReceipt @@ -71,7 +88,7 @@ proc readReceiptTyped(rlp: var Rlp, receipt: var Receipt) {.raises: [RlpError].} var txVal: ReceiptType if checkedEnumAssign(txVal, recType): case txVal - of Eip2930Receipt, Eip1559Receipt, Eip4844Receipt, Eip7702Receipt: + of Eip2930Receipt, Eip1559Receipt, Eip4844Receipt, Eip7702Receipt, Eip7807Receipt: receipt.receiptType = txVal of LegacyReceipt: raise newException(MalformedRlpError, "Invalid ReceiptType: " & $recType) @@ -116,6 +133,20 @@ proc read*(rlp: var Rlp, T: type StoredReceipt): StoredReceipt {.raises: [RlpErr rlp.read(rec.cumulativeGasUsed) rlp.read(rec.logs) + if rec.receiptType == Eip7807Receipt: + let st = rlp.read(uint) + if not checkedEnumAssign(rec.eip7807ReceiptType, st): + raise newException(UnsupportedRlpError, "Bad Eip7807ReceiptType: " & $st) + rlp.read(rec.origin) + rlp.read(rec.txGasUsed) + case rec.eip7807ReceiptType + of Eip7807Create: rlp.read(rec.contactAddress) + of Eip7807SetCode: rlp.read(rec.authorities) + of Eip7807Basic: discard + + if rlp.hasData: + raise newException(MalformedRlpError, "Trailing fields in StoredReceipt") + rec # Decode eth/68 Receipt diff --git a/eth/common/transactions.nim b/eth/common/transactions.nim index 3d0e0e8b..0a986f80 100644 --- a/eth/common/transactions.nim +++ b/eth/common/transactions.nim @@ -34,6 +34,7 @@ type TxEip1559 # 2 TxEip4844 # 3 TxEip7702 # 4 + TxEip7807 # 5 Transaction* = object txType* : TxType # EIP-2718 diff --git a/eth/common/transactions_rlp.nim b/eth/common/transactions_rlp.nim index 5e4be63e..46f3b807 100644 --- a/eth/common/transactions_rlp.nim +++ b/eth/common/transactions_rlp.nim @@ -7,8 +7,8 @@ {.push raises: [].} -import - "."/[addresses_rlp, base_rlp, hashes_rlp, transactions], +import + "."/[addresses_rlp, base_rlp, hashes_rlp, transactions], ../rlp, ../rlp/[length_writer, two_pass_writer, hash_writer] @@ -99,7 +99,7 @@ proc appendTxEip7702(w: var RlpWriter, tx: Transaction) = w.append(tx.R) w.append(tx.S) -proc appendTxPayload(w: var RlpWriter, tx: Transaction) = +proc appendTxPayload(w: var RlpWriter, tx: Transaction) {.raises: [UnsupportedRlpError].}= case tx.txType of TxLegacy: w.appendTxLegacy(tx) @@ -111,8 +111,12 @@ proc appendTxPayload(w: var RlpWriter, tx: Transaction) = w.appendTxEip4844(tx) of TxEip7702: w.appendTxEip7702(tx) + of TxEip7807: + raise newException(UnsupportedRlpError, "TxEip7807 has no RLP encoding; use SSZ") -proc append*(w: var RlpWriter, tx: Transaction) = +proc append*(w: var RlpWriter, tx: Transaction) {.raises: [UnsupportedRlpError].} = + if tx.txType == TxEip7807: + raise newException(UnsupportedRlpError, "TxEip7807 has no RLP encoding; use SSZ") if tx.txType != TxLegacy: # since the tx type is encoded outside the transaction type its encoding # cannot be considered a part of the rlp list that encodes the type. Hence @@ -195,7 +199,7 @@ proc rlpEncodeEip7702(w: var RlpWriter, tx: Transaction) = w.append(tx.accessList) w.append(tx.authorizationList) -proc encodeUnsignedTransaction*(w: var RlpWriter, tx: Transaction, eip155: bool) = +proc encodeUnsignedTransaction*(w: var RlpWriter, tx: Transaction, eip155: bool){.raises: [UnsupportedRlpError].} = ## Encode transaction data in preparation for signing or signature checking. ## For signature checking, set `eip155 = tx.isEip155` case tx.txType @@ -209,8 +213,10 @@ proc encodeUnsignedTransaction*(w: var RlpWriter, tx: Transaction, eip155: bool) w.rlpEncodeEip4844(tx) of TxEip7702: w.rlpEncodeEip7702(tx) + of TxEip7807: + raise newException(UnsupportedRlpError, "TxEip7807 has no RLP encoding; use SSZ") -proc encodeForSigning*(tx: Transaction, eip155: bool): seq[byte] = +proc encodeForSigning*(tx: Transaction, eip155: bool): seq[byte] {.raises: [UnsupportedRlpError].} = ## Encode transaction data in preparation for signing or signature checking. ## For signature checking, set `eip155 = tx.isEip155` var tracker: DynamicRlpLengthTracker @@ -223,7 +229,7 @@ proc encodeForSigning*(tx: Transaction, eip155: bool): seq[byte] = template rlpEncode*(tx: Transaction): seq[byte] {.deprecated.} = encodeForSigning(tx, tx.isEip155()) -func rlpHashForSigning*(tx: Transaction, eip155: bool): Hash32 = +func rlpHashForSigning*(tx: Transaction, eip155: bool): Hash32 {.raises: [UnsupportedRlpError].} = var tracker: DynamicRlpLengthTracker tracker.initLengthTracker() tracker.encodeUnsignedTransaction(tx, eip155) @@ -403,6 +409,8 @@ proc readTxPayload( rlp.readTxEip4844(tx) of TxEip7702: rlp.readTxEip7702(tx) + of TxEip7807: + raise newException(UnsupportedRlpError, "TxEip7807 has no RLP decoding; use SSZ") proc readTxTyped(rlp: var Rlp, tx: var Transaction) {.raises: [RlpError].} = let txType = rlp.readTxType() @@ -446,7 +454,7 @@ proc read*( rr.readTxTyped(tx) result.add tx -proc append*(rlpWriter: var RlpWriter, txs: seq[Transaction] | openArray[Transaction]) = +proc append*(rlpWriter: var RlpWriter, txs: seq[Transaction] | openArray[Transaction]){.raises: [UnsupportedRlpError].} = # See above about encoding arrays/sequences of transactions. rlpWriter.startList(txs.len) for tx in txs: diff --git a/eth/ssz/adapter.nim b/eth/ssz/adapter.nim new file mode 100644 index 00000000..f3e2dd72 --- /dev/null +++ b/eth/ssz/adapter.nim @@ -0,0 +1,34 @@ +{.push raises: [].} + +import + ../common/[addresses, hashes, base], + std/[typetraits], + ssz_serialization, + ssz_serialization/codec, + ssz_serialization/merkleization + +# This follows how +# https://github.com/status-im/nimbus-eth2/blob/9839f140628ae0e2e8aa7eb055da5c4bb08171d0/beacon_chain/spec/ssz_codec.nim#L29 +# does it for addresses in eth 2 +export ssz_serialization, codec, base, typetraits + +# SSZ for Address +template toSszType*(T: Address): auto = + distinctBase(T) + +func fromSszBytes*(T: type Address, bytes: openArray[byte]): T {.raises: [SszError].} = + readSszValue(bytes, distinctBase(result)) + +# SSZ for Hash32 +template toSszType*(T: Hash32): auto = + distinctBase(T) + +func fromSszBytes*(T: type Hash32, bytes: openArray[byte]): T {.raises: [SszError].} = + readSszValue(bytes, distinctBase(result)) + +# SSZ for Bytes32 +template toSszType*(T: Bytes32): auto = + distinctBase(T) + +func fromSszBytes*(T: type Bytes32, bytes: openArray[byte]): T {.raises: [SszError].} = + readSszValue(bytes, distinctBase(result)) diff --git a/eth/ssz/blocks_ssz.nim b/eth/ssz/blocks_ssz.nim new file mode 100644 index 00000000..7efa0da2 --- /dev/null +++ b/eth/ssz/blocks_ssz.nim @@ -0,0 +1,67 @@ +import ssz_serialization, ./adapter, ../common/[addresses, hashes], ./transaction_ssz + +const + # Post-merge constants + EMPTY_OMMERS_HASH* = + hash32"1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + MAX_BLOB_GAS_PER_BLOCK* = 786432'u64 + +type Withdrawal* {.sszActiveFields: [1, 1, 1, 1].} = object + index*: uint64 + validatorIndex*: uint64 + address*: Address + amount*: uint64 + +type GasAmounts* {.sszActiveFields: [1, 1].} = object + regular*: uint64 + blob*: uint64 + +type BlobFeesPerGas* {.sszActiveFields: [1, 1].} = object + regular*: uint64 + blob*: uint64 + +# EIP-7807: Execution Block Header +type + Header* {.sszActiveFields: [1, 1,1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1].} = object + parent_hash*: Root + miner*: Address + state_root*: Bytes32 + transactions_root*: Root + receipts_root*: Root + number*: uint64 + gas_limits*: GasAmounts + gas_used*: GasAmounts + timestamp*: uint64 + extra_data*: seq[uint8] + mix_hash*: Bytes32 + base_fees_per_gas*: BlobFeesPerGas + withdrawals_root*: Root # EIP-6465 hash_tree_root + excess_gas*: GasAmounts + parent_beacon_block_root*: Root + requests_hash*: Bytes32 # EIP-6110 hash_tree_root + system_logs_root*: Root + + BlockBody* = object + transactions*: seq[Transaction] + uncles*: seq[Header] + withdrawals*: Opt[seq[Withdrawal]] # EIP-4895 + + Block* = object + header*: Header + transactions*: seq[Transaction] + uncles*: seq[Header] + withdrawals*: Opt[seq[Withdrawal]] # EIP-4895 + +const EMPTY_UNCLE_HASH* = + hash32"1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + +func init*(T: type Block, header: Header, body: BlockBody): T = + T( + header: header, + transactions: body.transactions, + uncles: body.uncles, + withdrawals: body.withdrawals, + ) + +template txs*(blk: Block): seq[Transaction] = + blk.transactions diff --git a/eth/ssz/receipts_ssz.nim b/eth/ssz/receipts_ssz.nim new file mode 100644 index 00000000..2ee213ab --- /dev/null +++ b/eth/ssz/receipts_ssz.nim @@ -0,0 +1,99 @@ +import + sequtils, + ssz_serialization, + ./adapter, + ../common/[addresses, hashes], + ../common/receipts as rlp_receipts + +const MAX_TOPICS_PER_LOG* = 4 + +type + GasAmount* = uint64 + + Log* = object + address*: Address + topics*: List[Bytes32, MAX_TOPICS_PER_LOG] + data*: seq[byte] + + BasicReceipt* {.sszActiveFields: [1, 1, 0, 1, 1].} = object + `from`*: Address + gas_used*: GasAmount + logs*: seq[Log] + status*: bool + + CreateReceipt* {.sszActiveFields: [1, 1, 1, 1, 1].} = object + `from`*: Address + gas_used*: GasAmount + contract_address*: Address + logs*: seq[Log] + status*: bool + + SetCodeReceipt* {.sszActiveFields: [1, 1, 0, 1, 1, 1].} = object + `from`*: Address + gas_used*: GasAmount + logs*: seq[Log] + status*: bool + authorities*: seq[Address] + + ReceiptKind* = enum + rBasic = 0 + rCreate = 1 + rSetCode = 2 + + Receipt* = object + case kind*: ReceiptKind + of rBasic: basic*: BasicReceipt + of rCreate: create*: CreateReceipt + of rSetCode: setcode*: SetCodeReceipt + +converter toReceipt*(r: BasicReceipt): Receipt = + Receipt(kind: rBasic, basic: r) + +converter toReceipt*(r: CreateReceipt): Receipt = + Receipt(kind: rCreate, create: r) + +converter toReceipt*(r: SetCodeReceipt): Receipt = + Receipt(kind: rSetCode, setcode: r) + +proc toSszReceipt*( + rec: rlp_receipts.StoredReceipt, + sender: Address, + gasUsed: uint64, + contractAddress: Address, + authorities: seq[Address], +): Receipt = + # Convert logs from rlp_receipts.Log to ssz_receipts.Log + # the problem is its seq[log] in common/receipts but ssz_receipts.Log + # has a fixed size array for topics,which decides how the ssz serialization will happen + var sszLogs: seq[Log] = @[] + for log in rec.logs: + let topicsList = List[Bytes32, MAX_TOPICS_PER_LOG].init( + log.topics[0 ..< min(log.topics.len, MAX_TOPICS_PER_LOG)].mapIt(Bytes32(it)) + ) + sszLogs.add(Log(address: log.address, topics: topicsList, data: log.data)) + if authorities.len > 0: + let sszRec = SetCodeReceipt( + `from`: sender, + gas_used: gasUsed, + logs: sszLogs, + status: rec.status, + authorities: authorities, + ) + return sszRec.toReceipt() + elif contractAddress != default(Address): + let sszRec = CreateReceipt( + `from`: sender, + gas_used: gasUsed, + contract_address: contractAddress, + logs: sszLogs, + status: rec.status, + ) + return sszRec.toReceipt() + else: + let sszRec = BasicReceipt( + `from`: sender, + gas_used: gasUsed, + logs: sszLogs, + status: rec.status, + ) + return sszRec.toReceipt() diff --git a/eth/ssz/signatures.nim b/eth/ssz/signatures.nim new file mode 100644 index 00000000..83bcfc6a --- /dev/null +++ b/eth/ssz/signatures.nim @@ -0,0 +1,101 @@ +import stint, results, ../common/[keys, hashes, addresses, base], ssz_serialization + +const + SECP256K1_SIGNATURE_SIZE* = 32 + 32 + 1 + SECP256K1N* = + UInt256.fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141") + EIP155_CHAIN_ID_OFFSET* = 35'u64 + +type Secp256k1ExecutionSignature* = array[SECP256K1_SIGNATURE_SIZE, byte] + +proc secp256k1Pack*(r, s: UInt256, yParity: uint8): Secp256k1ExecutionSignature = + ## r||s||yParity (65B) + var sig: Secp256k1ExecutionSignature + sig[0 .. 31] = r.toBytesBE() + sig[32 .. 63] = s.toBytesBE() + sig[64] = yParity + sig + +proc secp256k1Unpack*( + signature: Secp256k1ExecutionSignature +): (UInt256, UInt256, uint8) = + ( + UInt256.fromBytesBE(signature.toOpenArray(0, 31)), + UInt256.fromBytesBE(signature.toOpenArray(32, 63)), + signature[64], + ) + +proc secp256k1Validate*(signature: Secp256k1ExecutionSignature): bool = + ## EIP-2 low-s validation: 0 zero) and (r < SECP256K1N) and (s > zero) and (s <= halfN) and + (y == 0'u8 or y == 1'u8) + +proc secp256k1RecoverSigner*( + signature: Secp256k1ExecutionSignature, sigHash: Hash32 +): Address = + ## Recover address from 65B signature over a 32B hash + let sig = Signature.fromRaw(signature).valueOr: + raise newException(ValueError, "invalid signature") + let pk = recover(sig, SkMessage(sigHash.data)).valueOr: + raise newException(ValueError, "recovery failed") + pk.to(Address) + +proc yParityFromLegacyV*(V: uint64, isEip155: bool): uint8 = + ## Legacy: pre-155 => V in {27,28}; EIP-155 => V=2*chainId+35/36 + if isEip155: + uint8((V - EIP155_CHAIN_ID_OFFSET) and 1) + else: + uint8((V - 27'u64) and 1) + +proc legacyVFromParity*(yParity: uint8, chainId: SomeInteger, isEip155: bool): uint64 = + ## Build a legacy V from yParity and (optional) chainId. + if isEip155: + EIP155_CHAIN_ID_OFFSET + (2 * uint64(chainId)) + uint64(yParity) + else: + 27'u64 + uint64(yParity) + +# ------------------------------------------------------------------------------ +# SSZ-native (EIP-6493-style) +# ------------------------------------------------------------------------------ + +# type +# DomainType* = array[4, byte] +# ExecutionSigningData* = object +# object_root*: Hash32 +# domain_type*: DomainType + +# proc sszObjectRoot*(payload: auto): Hash32 = +# hash_tree_root(payload) + +# proc sszSigningRoot*(payload: auto; domain: DomainType): Hash32 = +# hash_tree_root(ExecutionSigningData(object_root: sszObjectRoot(payload), +# domain_type: domain)) + +# proc signSsz6493*(seckey: PrivateKey, payload: auto; domain: DomainType): Secp256k1ExecutionSignature = +# let h = sszSigningRoot(payload, domain) +# let sig = sign(seckey, SkMessage(h.data)).valueOr: +# raise newException(ValueError, "signing failed") +# let raw = sig.toRaw() +# if not secp256k1Validate(raw): +# raise newException(ValueError, "non-canonical secp256k1 signature") +# raw + +# proc recoverSsz6493Signer*(signature: Secp256k1ExecutionSignature, payload: auto; +# domain: DomainType): Address = +# let h = sszSigningRoot(payload, domain) +# let sigObj = Signature.fromRaw(signature).valueOr: +# raise newException(ValueError, "invalid signature") +# let pk = recover(sigObj, SkMessage(h.data)).valueOr: +# raise newException(ValueError, "recovery failed") +# pk.to(Address) + +# proc verifySsz6493*(signature: Secp256k1ExecutionSignature, payload: auto, +# expected: Address; domain: DomainType): bool = +# recoverSsz6493Signer(signature, payload, domain) == expected + +# # SSZ-native id/root helper (no keccak) +# proc sszTxRoot*(tx: auto): Hash32 = +# hash_tree_root(tx) diff --git a/eth/ssz/sszcodec.nim b/eth/ssz/sszcodec.nim new file mode 100644 index 00000000..ae1b729f --- /dev/null +++ b/eth/ssz/sszcodec.nim @@ -0,0 +1,431 @@ +import + std/[sequtils], + stint, + ./[signatures, blocks_ssz, transaction_builder], + ../common/[addresses_rlp, base_rlp, blocks], + ../common/transactions as rlp_tx_mod, + ../common/receipts as rlp_receipts, + ./receipts_ssz as ssz_receipts, + ./transaction_ssz as ssz_tx, + ssz_serialization/merkleization + +type + Withdrawal_SSZ* = blocks_ssz.Withdrawal + Withdrawal_RLP* = blocks.Withdrawal + +proc toSszWithdrawal*(w: Withdrawal_RLP): Withdrawal_SSZ = + Withdrawal_SSZ( + index: w.index, + validatorIndex: w.validatorIndex, + address: w.address, + amount: w.amount, + ) + +proc fromSszWithdrawal*(w: Withdrawal_SSZ): Withdrawal_RLP = + Withdrawal_RLP( + index: w.index, + validatorIndex: w.validatorIndex, + address: w.address, + amount: w.amount, + ) + +# Gas -> FeePerGas +proc feeFromGas(x: rlp_tx_mod.GasInt): ssz_tx.FeePerGas = + when compiles(x.u256): + x.u256 + else: + UInt256.fromInt(int(x)) + +proc toGasInt(x: ssz_tx.FeePerGas): rlp_tx_mod.GasInt = + if x > u256(high(uint64)): + raise newException(ValueError, "FeePerGas too large to fit into GasInt") + # TODO:verify with etan+advaita(advaita say sanity check one is ok) + rlp_tx_mod.GasInt(x.limbs[0]) + +proc accessTupleFrom(pair: rlp_tx_mod.AccessPair): ssz_tx.AccessTuple = + # Old storageKeys: seq[Bytes32]; new seq[Hash32] + result.address = pair.address + result.storage_keys = newSeq[Hash32](pair.storageKeys.len) + for i, k in pair.storageKeys: + result.storage_keys[i] = cast[Hash32](k) + +proc accessListFrom(al: rlp_tx_mod.AccessList): seq[ssz_tx.AccessTuple] = + al.map(accessTupleFrom) + +proc toAuthTuples*(al: seq[rlp_tx_mod.Authorization]): seq[ssz_tx.AuthTuple] = + ## Convert RLP-style authorizations -> AuthTuple expected by builder + result = newSeq[ssz_tx.AuthTuple](al.len) + for i, a in al: + result[i] = ( + chain_id: a.chainId, + address: a.address, + nonce: uint64(a.nonce), + y_parity: a.yParity, + r: a.r, + s: a.s, + ) + +proc toSszSignedAuthList*( + al: seq[rlp_tx_mod.Authorization] +): seq[ssz_tx.Authorization] = + result = newSeq[ssz_tx.Authorization](al.len) + for i, a in al: + let payload = + if a.chainId == ChainId(0.u256): + ssz_tx.AuthorizationPayload( + kind: ssz_tx.authReplayableBasic, + replayable: ssz_tx.RlpReplayableBasicAuthorizationPayload( + magic: ssz_tx.AuthMagic7702, address: a.address, nonce: uint64(a.nonce) + ), + ) + else: + ssz_tx.AuthorizationPayload( + kind: ssz_tx.authBasic, + basic: ssz_tx.RlpBasicAuthorizationPayload( + magic: ssz_tx.AuthMagic7702, + chain_id: a.chainId, + address: a.address, + nonce: uint64(a.nonce), + ), + ) + + let sig = secp256k1Pack(a.r, a.s, a.yParity) + result[i] = ssz_tx.Authorization(payload: payload, signature: sig) + +proc toSszAuthList*(al: seq[rlp_tx_mod.Authorization]): seq[ssz_tx.Authorization] = + toSszSignedAuthList(al) + +proc toRlpAuthList*(al: seq[ssz_tx.Authorization]): seq[rlp_tx_mod.Authorization] = + result = newSeq[rlp_tx_mod.Authorization](al.len) + for i, a in al: + let (R, S, parity) = secp256k1Unpack(a.signature) + case a.payload.kind + of ssz_tx.authReplayableBasic: + let p = a.payload.replayable + result[i] = rlp_tx_mod.Authorization( + chainId: ChainId(0.u256), + address: p.address, + nonce: AccountNonce(p.nonce), + yParity: parity, + r: R, + s: S, + ) + of ssz_tx.authBasic: + let p = a.payload.basic + result[i] = rlp_tx_mod.Authorization( + chainId: p.chain_id, + address: p.address, + nonce: AccountNonce(p.nonce), + yParity: parity, + r: R, + s: S, + ) + +proc packSigFromTx(tx: rlp_tx_mod.Transaction): Secp256k1ExecutionSignature = + let y: uint8 = + case tx.txType + of rlp_tx_mod.TxLegacy: + yParityFromLegacyV(uint64(tx.V), tx.isEip155) + else: + uint8(uint64(tx.V) and 1'u64) + secp256k1Pack(tx.R, tx.S, y) + +proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = + let sig = packSigFromTx(tx) + let legacyChain: ChainId = + if tx.isEip155: + tx.chainId + else: + ChainId(0.u256) + let accessSSZ = accessListFrom(tx.accessList) + + if tx.txType == rlp_tx_mod.TxLegacy: + return transaction_builder.Transaction( + txType = ssz_tx.TxLegacy, + chain_id = legacyChain, + nonce = tx.nonce, + gas = tx.gasLimit, + to = tx.to, + value = tx.value, + input = tx.payload, + max_fees_per_gas = ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.gasPrice)), + signature = sig, + ) + if tx.txType == rlp_tx_mod.TxEip2930: + return transaction_builder.Transaction( + txType = ssz_tx.TxAccessList, + chain_id = tx.chainId, + nonce = tx.nonce, + gas = tx.gasLimit, + to = tx.to, + value = tx.value, + input = tx.payload, + max_fees_per_gas = ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.gasPrice)), + signature = sig, + access_list = accessSSZ, + ) + if tx.txType == rlp_tx_mod.TxEip1559: + return transaction_builder.Transaction( + txType = ssz_tx.TxDynamicFee, + chain_id = tx.chainId, + nonce = tx.nonce, + gas = tx.gasLimit, + to = tx.to, + value = tx.value, + input = tx.payload, + max_fees_per_gas = ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.maxFeePerGas)), + max_priority_fees_per_gas = + ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.maxPriorityFeePerGas)), + signature = sig, + access_list = accessSSZ, + ) + if tx.txType == rlp_tx_mod.TxEip4844: + return transaction_builder.Transaction( + txType = ssz_tx.TxBlob, + chain_id = tx.chainId, + nonce = tx.nonce, + gas = tx.gasLimit, + to = tx.to, + value = tx.value, + input = tx.payload, + max_fees_per_gas = ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.maxFeePerGas)), + max_priority_fees_per_gas = + ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.maxPriorityFeePerGas)), + signature = sig, + access_list = accessSSZ, + blob_versioned_hashes = tx.versionedHashes, + blob_fee = tx.maxFeePerBlobGas, + ) + if tx.txType == rlp_tx_mod.TxEip7702: + if tx.to.isNone: + raise newException(ValueError, "7702 setCode: requires 'to'") + return transaction_builder.Transaction( + txType = ssz_tx.TxSetCode, + chain_id = tx.chainId, + nonce = tx.nonce, + gas = tx.gasLimit, + to = tx.to, + value = tx.value, + input = tx.payload, + max_fees_per_gas = ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.maxFeePerGas)), + max_priority_fees_per_gas = + ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.maxPriorityFeePerGas)), + signature = sig, + access_list = accessSSZ, + authorization_list = toAuthTuples(tx.authorizationList), + ) + raise newException(ValueError, "Unsupported transaction type: " & $tx.txType) + +proc toOldTx*(tx: ssz_tx.Transaction): rlp_tx_mod.Transaction = + if tx.kind != RlpTransaction: + raise newException(ValueError, "only RLP transaction variant supported") + + proc toOldAccessList(al: seq[ssz_tx.AccessTuple]): rlp_tx_mod.AccessList = + result = @[] + for t in al: + result.add rlp_tx_mod.AccessPair( + address: t.address, storageKeys: t.storage_keys.mapIt(cast[Bytes32](it)) + ) + + let r = tx.rlp + + case r.kind + of txLegacyReplayableBasic: + let p = r.legacyReplayableBasic.payload + let sig = r.legacyReplayableBasic.signature + let (R, S, y) = secp256k1Unpack (sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxLegacy, + chainId: ChainId(0.u256), + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.some(p.to), + value: p.value, + payload: p.input, + gasPrice: toGasInt(p.max_fees_per_gas.regular), + V: 27'u64 + uint64(y), + R: R, + S: S, + ) + of txLegacyReplayableCreate: + let p = r.legacyReplayableCreate.payload + let sig = r.legacyReplayableCreate.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxLegacy, + chainId: ChainId(0.u256), + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.none(Address), + value: p.value, + payload: p.input, + gasPrice: toGasInt(p.max_fees_per_gas.regular), + V: 27'u64 + uint64(y), + R: R, + S: S, + ) + of txLegacyBasic: + let p = r.legacyBasic.payload + let sig = r.legacyBasic.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxLegacy, + chainId: p.chain_id, + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.some(p.to), + value: p.value, + payload: p.input, + gasPrice: toGasInt(p.max_fees_per_gas.regular), + V: rlp_tx_mod.EIP155_CHAIN_ID_OFFSET + ((2 * p.chain_id).limbs[0]) + uint64(y), + R: R, + S: S, + ) + of txLegacyCreate: + let p = r.legacyCreate.payload + let sig = r.legacyCreate.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxLegacy, + chainId: p.chain_id, + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.none(Address), + value: p.value, + payload: p.input, + gasPrice: toGasInt(p.max_fees_per_gas.regular), + V: rlp_tx_mod.EIP155_CHAIN_ID_OFFSET + ((2 * p.chain_id).limbs[0]) + uint64(y), + R: R, + S: S, + ) + of txAccessListBasic: + let p = r.accessListBasic.payload + let sig = r.accessListBasic.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxEip2930, + chainId: p.chain_id, + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.some(p.to), + value: p.value, + payload: p.input, + gasPrice: toGasInt(p.max_fees_per_gas.regular), + accessList: toOldAccessList(p.access_list), + V: uint64(y), + R: R, + S: S, + ) + of txAccessListCreate: + let p = r.accessListCreate.payload + let sig = r.accessListCreate.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxEip2930, + chainId: p.chain_id, + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.none(Address), + value: p.value, + payload: p.input, + gasPrice: toGasInt(p.max_fees_per_gas.regular), + accessList: toOldAccessList(p.access_list), + V: uint64(y), + R: R, + S: S, + ) + of txBasic: + let p = r.basic.payload + let sig = r.basic.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxEip1559, + chainId: p.chain_id, + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.some(p.to), + value: p.value, + payload: p.input, + maxPriorityFeePerGas: toGasInt(p.max_priority_fees_per_gas.regular), + maxFeePerGas: toGasInt(p.max_fees_per_gas.regular), + accessList: toOldAccessList(p.access_list), + V: uint64(y), + R: R, + S: S, + ) + of txCreate: + let p = r.create.payload + let sig = r.create.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxEip1559, + chainId: p.chain_id, + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.none(Address), + value: p.value, + payload: p.input, + maxPriorityFeePerGas: toGasInt(p.max_priority_fees_per_gas.regular), + maxFeePerGas: toGasInt(p.max_fees_per_gas.regular), + accessList: toOldAccessList(p.access_list), + V: uint64(y), + R: R, + S: S, + ) + of txBlob: + let p = r.blob.payload + let sig = r.blob.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxEip4844, + chainId: p.chain_id, + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.some(p.to), + value: p.value, + payload: p.input, + maxPriorityFeePerGas: toGasInt(p.max_priority_fees_per_gas.regular), + maxFeePerGas: toGasInt(p.max_fees_per_gas.regular), + # BlobFeesPerGas → BasicFeesPerGas.regular + maxFeePerBlobGas: p.max_fees_per_gas.blob, + versionedHashes: p.blob_versioned_hashes, + accessList: toOldAccessList(p.access_list), + V: uint64(y), + R: R, + S: S, + ) + of txSetCode: + let p = r.setCode.payload + let sig = r.setCode.signature + let (R, S, y) = secp256k1Unpack(sig) + result = rlp_tx_mod.Transaction( + txType: rlp_tx_mod.TxEip7702, + chainId: p.chain_id, + nonce: p.nonce, + gasLimit: p.gas, + to: Opt.some(p.to), + value: p.value, + payload: p.input, + maxPriorityFeePerGas: toGasInt(p.max_priority_fees_per_gas.regular), + maxFeePerGas: toGasInt(p.max_fees_per_gas.regular), + accessList: toOldAccessList(p.access_list), + authorizationList: toRlpAuthList(p.authorization_list), + V: uint64(y), + R: R, + S: S, + ) + +proc computeTransactionsRootSsz*(txs: seq[transactions.Transaction]): Root = + var sszTxs: seq[ssz_tx.Transaction] + for tx in txs: + sszTxs.add(toSszTx(tx)) + Root(sszTxs.hash_tree_root().data) + +proc computeWithdrawalsRootSsz*(withdrawals: Opt[seq[Withdrawal_RLP]]): Root = + if withdrawals.isNone: + return default(Root) + + var sszWds: seq[Withdrawal_SSZ] + for w in withdrawals.get: + sszWds.add(toSszWithdrawal(w)) + Root(sszWds.hash_tree_root().data) diff --git a/eth/ssz/transaction_builder.nim b/eth/ssz/transaction_builder.nim new file mode 100644 index 00000000..c8fe8093 --- /dev/null +++ b/eth/ssz/transaction_builder.nim @@ -0,0 +1,223 @@ +import stint, ./transaction_ssz, ./signatures, ../common/[addresses, base, hashes] + +type TxBuildError* = object of ValueError + +template fail(msg: string): untyped = + raise newException(TxBuildError, msg) + +proc makeAuthorization*(t: AuthTuple): Authorization = + let payload = + if t.chain_id == ChainId(0.u256): + AuthorizationPayload( + kind: authReplayableBasic, + replayable: RlpReplayableBasicAuthorizationPayload( + magic: AuthMagic7702, address: t.address, nonce: t.nonce + ), + ) + else: + AuthorizationPayload( + kind: authBasic, + basic: RlpBasicAuthorizationPayload( + magic: AuthMagic7702, chain_id: t.chain_id, address: t.address, nonce: t.nonce + ), + ) + + Authorization(payload: payload, signature: secp256k1Pack(t.r, t.s, t.y_parity)) + +proc makeAuthorizationList*(xs: openArray[AuthTuple]): seq[Authorization] = + result = newSeqOfCap[Authorization](xs.len) + for x in xs: + result.add makeAuthorization(x) + +template BuildWrap( + PayloadT, WrapperT: typedesc, tag: static[RLPTransactionKind], fieldSym: untyped +) = + proc build*( + payload: PayloadT, signature: Secp256k1ExecutionSignature + ): Transaction {.inline.} = + let inner = WrapperT(payload: payload, signature: signature) + Transaction( + kind: RlpTransaction, rlp: RlpTransactionObject(kind: tag, fieldSym: inner) + ) + +BuildWrap( + RlpLegacyBasicTransactionPayload, RlpLegacyBasicTransaction, txLegacyBasic, + legacyBasic, +) +BuildWrap( + RlpLegacyCreateTransactionPayload, RlpLegacyCreateTransaction, txLegacyCreate, + legacyCreate, +) +BuildWrap( + RlpAccessListBasicTransactionPayload, RlpAccessListBasicTransaction, + txAccessListBasic, accessListBasic, +) +BuildWrap( + RlpAccessListCreateTransactionPayload, RlpAccessListCreateTransaction, + txAccessListCreate, accessListCreate, +) +BuildWrap(RlpBasicTransactionPayload, RlpBasicTransaction, txBasic, basic) +BuildWrap(RlpCreateTransactionPayload, RlpCreateTransaction, txCreate, create) +BuildWrap(RlpBlobTransactionPayload, RlpBlobTransaction, txBlob, blob) +BuildWrap(RlpSetCodeTransactionPayload, RlpSetCodeTransaction, txSetCode, setCode) +BuildWrap( + RlpLegacyReplayableBasicTransactionPayload, RlpLegacyReplayableBasicTransaction, + txLegacyReplayableBasic, legacyReplayableBasic, +) +BuildWrap( + RlpLegacyReplayableCreateTransactionPayload, RlpLegacyReplayableCreateTransaction, + txLegacyReplayableCreate, legacyReplayableCreate, +) + +proc Transaction*( + txType: uint8, + chain_id: ChainId, + nonce: uint64, + gas: GasAmount, + to: Opt[Address], + value: UInt256, + input: openArray[byte], + max_fees_per_gas: BasicFeesPerGas, + signature: Secp256k1ExecutionSignature, + max_priority_fees_per_gas: BasicFeesPerGas = BasicFeesPerGas(regular: 0.u256), + access_list: seq[AccessTuple] = @[], + blob_versioned_hashes: seq[VersionedHash] = @[], + blob_fee: FeePerGas = 0.u256, + authorization_list: seq[AuthTuple] = @[], +): Transaction = + let auths = makeAuthorizationList(authorization_list) + case txType + of TxLegacy: + if to.isSome: + if chain_id == ChainId(0.u256): + let p = RlpLegacyReplayableBasicTransactionPayload( + txType: txType, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + to: to.get, + value: value, + input: @input, + ) + return build(p, signature) + else: + let p = RlpLegacyBasicTransactionPayload( + txType: txType, + chain_id: chain_id, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + to: to.get, + value: value, + input: @input, + ) + return build(p, signature) + else: + if chain_id == ChainId(0.u256): + let p = RlpLegacyReplayableCreateTransactionPayload( + txType: txType, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + value: value, + input: @input, + ) + return build(p, signature) + else: + let p = RlpLegacyCreateTransactionPayload( + txType: txType, + chain_id: chain_id, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + value: value, + input: @input, + ) + return build(p, signature) + of TxAccessList: + if to.isSome: + let p = RlpAccessListBasicTransactionPayload( + txType: txType, + chain_id: chain_id, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + to: to.get, + value: value, + input: @input, + access_list: access_list, + ) + return build(p, signature) + else: + let p = RlpAccessListCreateTransactionPayload( + txType: txType, + chain_id: chain_id, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + value: value, + input: @input, + access_list: access_list, + ) + return build(p, signature) + of TxDynamicFee: + if to.isSome: + let p = RlpBasicTransactionPayload( + txType: txType, + chain_id: chain_id, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + to: to.get, + value: value, + input: @input, + access_list: access_list, + max_priority_fees_per_gas: max_priority_fees_per_gas, + ) + return build(p, signature) + else: + let p = RlpCreateTransactionPayload( + txType: txType, + chain_id: chain_id, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + value: value, + input: @input, + access_list: access_list, + max_priority_fees_per_gas: max_priority_fees_per_gas, + ) + return build(p, signature) + of TxBlob: + let blobFees = BlobFeesPerGas(regular: max_fees_per_gas.regular, blob: blob_fee) + let p = RlpBlobTransactionPayload( + txType: txType, + chain_id: chain_id, + nonce: nonce, + max_fees_per_gas: blobFees, + gas: gas, + to: to.get, + value: value, + input: @input, + access_list: access_list, + max_priority_fees_per_gas: max_priority_fees_per_gas, + blob_versioned_hashes: blob_versioned_hashes, + ) + return build(p, signature) + of TxSetCode: + let p = RlpSetCodeTransactionPayload( + txType: TxSetCode, + chain_id: chain_id, + nonce: nonce, + max_fees_per_gas: max_fees_per_gas, + gas: gas, + to: to.get, + value: value, + input: @input, + access_list: access_list, + max_priority_fees_per_gas: max_priority_fees_per_gas, + authorization_list: auths, + ) + return build(p, signature) + else: + fail("Unsupported txType (expected 0x00..0x04)") diff --git a/eth/ssz/transaction_ssz.nim b/eth/ssz/transaction_ssz.nim new file mode 100644 index 00000000..1448adb9 --- /dev/null +++ b/eth/ssz/transaction_ssz.nim @@ -0,0 +1,260 @@ +import + ssz_serialization, stint, ../common/[addresses, base, hashes], ./signatures, ./adapter + +export adapter + +type SignedTx*[P] = object + payload*: P + signature*: Secp256k1ExecutionSignature + +type + TransactionType* = uint8 + GasAmount* = uint64 + FeePerGas* = UInt256 + ProgressiveByteList* = seq[byte] + +const + TxLegacy*: TransactionType = 00'u8 + TxAccessList*: TransactionType = 01'u8 + TxDynamicFee*: TransactionType = 02'u8 + TxBlob*: TransactionType = 03'u8 + TxSetCode*: TransactionType = 04'u8 + AuthMagic7702*: TransactionType = 05'u8 + +type + BasicFeesPerGas* = object + regular*: FeePerGas + + BlobFeesPerGas* = object + regular*: FeePerGas + blob*: FeePerGas + +type AccessTuple* = object + address*: Address + storage_keys*: seq[Hash32] + +type + RlpLegacyReplayableBasicTransactionPayload* {. + sszActiveFields: [1, 0, 1, 1, 1, 1, 1, 1] + .} = object + txType*: TransactionType + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + to*: Address + value*: UInt256 + input*: ProgressiveByteList + + RlpLegacyReplayableCreateTransactionPayload* {. + sszActiveFields: [1, 0, 1, 1, 1, 0, 1, 1] + .} = object + txType*: TransactionType + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + value*: UInt256 + input*: ProgressiveByteList + +type + RlpLegacyBasicTransactionPayload* {.sszActiveFields: [1, 1, 1, 1, 1, 1, 1, 1].} = object + txType*: TransactionType + chain_id*: ChainId + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + to*: Address + value*: UInt256 + input*: ProgressiveByteList + + RlpLegacyCreateTransactionPayload* {.sszActiveFields: [1, 1, 1, 1, 1, 0, 1, 1].} = object + txType*: TransactionType + chain_id*: ChainId + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + value*: UInt256 + input*: ProgressiveByteList + +type RlpAccessListBasicTransactionPayload* {. + sszActiveFields: [1, 1, 1, 1, 1, 1, 1, 1, 1] +.} = object + txType*: TransactionType + chain_id*: ChainId + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + to*: Address + value*: UInt256 + input*: ProgressiveByteList + access_list*: seq[AccessTuple] + +type RlpAccessListCreateTransactionPayload* {. + sszActiveFields: [1, 1, 1, 1, 1, 0, 1, 1, 1] +.} = object + txType*: TransactionType + chain_id*: ChainId + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + value*: UInt256 + input*: ProgressiveByteList + access_list*: seq[AccessTuple] + +type + RlpBasicTransactionPayload* {.sszActiveFields: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1].} = object + txType*: TransactionType + chain_id*: ChainId + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + to*: Address + value*: UInt256 + input*: ProgressiveByteList + access_list*: seq[AccessTuple] + max_priority_fees_per_gas*: BasicFeesPerGas + + RlpCreateTransactionPayload* {.sszActiveFields: [1, 1, 1, 1, 1, 0, 1, 1, 1, 1].} = object + txType*: TransactionType + chain_id*: ChainId + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + value*: UInt256 + input*: ProgressiveByteList + access_list*: seq[AccessTuple] + max_priority_fees_per_gas*: BasicFeesPerGas + +type RlpBlobTransactionPayload* {.sszActiveFields: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1].} = object + txType*: TransactionType + chain_id*: ChainId + nonce*: uint64 + max_fees_per_gas*: BlobFeesPerGas + gas*: GasAmount + to*: Address + value*: UInt256 + input*: ProgressiveByteList + access_list*: seq[AccessTuple] + max_priority_fees_per_gas*: BasicFeesPerGas + blob_versioned_hashes*: seq[VersionedHash] + +type + RlpReplayableBasicAuthorizationPayload* {.sszActiveFields: [1, 0, 1, 1].} = object + magic*: TransactionType # 0x05 (Auth) + address*: Address + nonce*: uint64 + + RlpBasicAuthorizationPayload* {.sszActiveFields: [1, 1, 1, 1].} = object + magic*: TransactionType # 0x05 (Auth) + chain_id*: ChainId + address*: Address + nonce*: uint64 + + AuthorizationKind* = enum + authReplayableBasic + authBasic + + AuthorizationPayload* = object + case kind*: AuthorizationKind + of authReplayableBasic: + replayable*: RlpReplayableBasicAuthorizationPayload + of authBasic: + basic*: RlpBasicAuthorizationPayload + + Authorization* = object + payload*: AuthorizationPayload + signature*: Secp256k1ExecutionSignature + +type RlpSetCodeTransactionPayload* {. + sszActiveFields: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] +.} = object + txType*: TransactionType + chain_id*: ChainId + nonce*: uint64 + max_fees_per_gas*: BasicFeesPerGas + gas*: GasAmount + to*: Address + value*: UInt256 + input*: ProgressiveByteList + access_list*: seq[AccessTuple] + max_priority_fees_per_gas*: BasicFeesPerGas + authorization_list*: seq[Authorization] + +type + RlpLegacyReplayableBasicTransaction* = + SignedTx[RlpLegacyReplayableBasicTransactionPayload] + RlpLegacyReplayableCreateTransaction* = + SignedTx[RlpLegacyReplayableCreateTransactionPayload] + RlpLegacyBasicTransaction* = SignedTx[RlpLegacyBasicTransactionPayload] + RlpLegacyCreateTransaction* = SignedTx[RlpLegacyCreateTransactionPayload] + RlpAccessListBasicTransaction* = SignedTx[RlpAccessListBasicTransactionPayload] + RlpAccessListCreateTransaction* = SignedTx[RlpAccessListCreateTransactionPayload] + RlpBasicTransaction* = SignedTx[RlpBasicTransactionPayload] + RlpCreateTransaction* = SignedTx[RlpCreateTransactionPayload] + RlpBlobTransaction* = SignedTx[RlpBlobTransactionPayload] + RlpSetCodeTransaction* = SignedTx[RlpSetCodeTransactionPayload] + + # # This doesnt do the ssz encode/decode stuff so we keep it here for now to swap in later + # AnyRlpTransaction* = + # RlpLegacyReplayableBasicTransaction | RlpLegacyReplayableCreateTransaction | + # RlpLegacyBasicTransaction | RlpLegacyCreateTransaction | + # RlpAccessListBasicTransaction | RlpAccessListCreateTransaction | RlpBasicTransaction | + # RlpCreateTransaction | RlpBlobTransaction | RlpSetCodeTransaction + +type + RLPTransactionKind* = enum + txLegacyReplayableBasic = 0 + txLegacyReplayableCreate = 1 + txLegacyBasic = 2 + txLegacyCreate = 3 + txAccessListBasic = 4 + txAccessListCreate = 5 + txBasic = 6 + txCreate = 7 + txBlob = 8 + txSetCode = 9 + + RlpTransactionObject* = object + case kind*: RLPTransactionKind + of txLegacyReplayableBasic: + legacyReplayableBasic*: RlpLegacyReplayableBasicTransaction + of txLegacyReplayableCreate: + legacyReplayableCreate*: RlpLegacyReplayableCreateTransaction + of txLegacyBasic: + legacyBasic*: RlpLegacyBasicTransaction + of txLegacyCreate: + legacyCreate*: RlpLegacyCreateTransaction + of txAccessListBasic: + accessListBasic*: RlpAccessListBasicTransaction + of txAccessListCreate: + accessListCreate*: RlpAccessListCreateTransaction + of txBasic: + basic*: RlpBasicTransaction + of txCreate: + create*: RlpCreateTransaction + of txBlob: + blob*: RlpBlobTransaction + of txSetCode: + setCode*: RlpSetCodeTransaction + +type + TransactionKind* {.pure.} = enum + TxNone = 0 + RlpTransaction = 1 + + Transaction* = object + case kind*: TransactionKind + of TxNone: + discard + of RlpTransaction: + rlp*: RlpTransactionObject + +# Not importing from common/transaction as it would cause problem with the trensaction deffined in common/transactions +type AuthTuple* = + tuple[ + chain_id: ChainId, + address: Address, + nonce: uint64, + y_parity: uint8, + r: UInt256, + s: UInt256, + ] diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 297c029b..139b20fa 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -19,4 +19,5 @@ import ./db/all_tests, ./common/all_tests, ./test_bloom, + ./ssz/all_tests ./test_enode diff --git a/tests/common/test_eth_types_rlp.nim b/tests/common/test_eth_types_rlp.nim index 75854e93..ff0b83e7 100644 --- a/tests/common/test_eth_types_rlp.nim +++ b/tests/common/test_eth_types_rlp.nim @@ -195,6 +195,7 @@ type withdrawalsRoot*: Opt[Hash32] blobGasUsed*: Opt[GasInt] excessBlobGas*: Opt[GasInt] + systemLogsRoot*: Opt[Hash32] BlockBodyOpt* = object transactions*: seq[Transaction] diff --git a/tests/common/test_receipts.nim b/tests/common/test_receipts.nim index b6d8a77f..1e8c96e1 100644 --- a/tests/common/test_receipts.nim +++ b/tests/common/test_receipts.nim @@ -56,3 +56,64 @@ suite "Stored Receipt": cumulativeGasUsed: 100.GasInt) roundTrip(rec) + + test "EIP-7807 StoredReceipt: Basic": + let rec = StoredReceipt( + receiptType: Eip7807Receipt, + isHash: false, + status: true, + cumulativeGasUsed: 777.GasInt, + eip7807ReceiptType: Eip7807Basic, + origin: default(Address), + txGasUsed: 555'u64 + ) + roundTrip(rec) + + test "EIP-7807 StoredReceipt: Create": + let rec = StoredReceipt( + receiptType: Eip7807Receipt, + isHash: false, + status: true, + cumulativeGasUsed: 888.GasInt, + logs: @[], + eip7807ReceiptType: Eip7807Create, + origin: default(Address), + txGasUsed: 666'u64, + contactAddress: default(Address) + ) + roundTrip(rec) + + test "EIP-7807 StoredReceipt: SetCode": + let rec = StoredReceipt( + receiptType: Eip7807Receipt, + isHash: false, + status: true, + cumulativeGasUsed: 999.GasInt, + logs: @[], + eip7807ReceiptType: Eip7807SetCode, + origin: default(Address), + txGasUsed: 333'u64, + authorities: @[default(Address)] + ) + roundTrip(rec) + + test "StoredReceipt seq roundtrip (mixed types)": + let a = StoredReceipt( + receiptType: Eip7702Receipt, + isHash: false, + status: false, + cumulativeGasUsed: 1.GasInt, + logs: @[] + ) + let b = StoredReceipt( + receiptType: Eip7807Receipt, + isHash: false, + status: true, + cumulativeGasUsed: 2.GasInt, + logs: @[], + eip7807ReceiptType: Eip7807Basic, + origin: default(Address), + txGasUsed: 42'u64 + ) + let arr = @[a, b] + roundTrip(arr) diff --git a/tests/common/test_transactions.nim b/tests/common/test_transactions.nim index 64e39666..55ee2d68 100644 --- a/tests/common/test_transactions.nim +++ b/tests/common/test_transactions.nim @@ -26,12 +26,12 @@ const chainID: chainId(1), address: source, nonce: 2.AccountNonce, - yParity: 3, - r: 4.u256, - s: 5.u256 + yParity: 1, + r: 1.u256, + s: 1.u256 )] -func tx0(i: int): Transaction = +proc tx0*(i: int): Transaction = Transaction( txType: TxLegacy, nonce: i.AccountNonce, @@ -40,7 +40,7 @@ func tx0(i: int): Transaction = gasPrice: 2.GasInt, payload: abcdef) -func tx1(i: int): Transaction = +proc tx1*(i: int): Transaction = Transaction( # Legacy tx contract creation. txType: TxLegacy, @@ -49,7 +49,7 @@ func tx1(i: int): Transaction = gasPrice: 2.GasInt, payload: abcdef) -func tx2(i: int): Transaction = +proc tx2*(i: int): Transaction = Transaction( # Tx with non-zero access list. txType: TxEip2930, @@ -61,7 +61,7 @@ func tx2(i: int): Transaction = accessList: accesses, payload: abcdef) -func tx3(i: int): Transaction = +proc tx3*(i: int): Transaction = Transaction( # Tx with empty access list. txType: TxEip2930, @@ -72,7 +72,7 @@ func tx3(i: int): Transaction = gasPrice: 10.GasInt, payload: abcdef) -func tx4(i: int): Transaction = +proc tx4*(i: int): Transaction = Transaction( # Contract creation with access list. txType: TxEip2930, @@ -82,7 +82,7 @@ func tx4(i: int): Transaction = gasPrice: 10.GasInt, accessList: accesses) -func tx5(i: int): Transaction = +proc tx5*(i: int): Transaction = Transaction( txType: TxEip1559, chainId: chainId(1), @@ -92,7 +92,7 @@ func tx5(i: int): Transaction = maxFeePerGas: 10.GasInt, accessList: accesses) -func tx6(i: int): Transaction = +proc tx6*(i: int): Transaction = const digest = hash32"010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014" @@ -106,7 +106,7 @@ func tx6(i: int): Transaction = accessList: accesses, versionedHashes: @[digest]) -func tx7(i: int): Transaction = +proc tx7*(i: int): Transaction = const digest = hash32"01624652859a6e98ffc1608e2af0147ca4e86e1ce27672d8d3f3c9d4ffd6ef7e" @@ -121,7 +121,7 @@ func tx7(i: int): Transaction = versionedHashes: @[digest], maxFeePerBlobGas: 10000000.u256) -func tx8(i: int): Transaction = +proc tx8*(i: int): Transaction = const digest = hash32"01624652859a6e98ffc1608e2af0147ca4e86e1ce27672d8d3f3c9d4ffd6ef7e" @@ -134,10 +134,11 @@ func tx8(i: int): Transaction = maxPriorityFeePerGas:42.GasInt, maxFeePerGas: 10.GasInt, accessList: accesses, + versionedHashes: @[digest], maxFeePerBlobGas: 10000000.u256) -func txEip7702(i: int): Transaction = +proc txEip7702*(i: int): Transaction = Transaction( txType: TxEip7702, chainId: chainId(1), diff --git a/tests/ssz/all_tests.nim b/tests/ssz/all_tests.nim new file mode 100644 index 00000000..1d38abb3 --- /dev/null +++ b/tests/ssz/all_tests.nim @@ -0,0 +1,7 @@ +import + ./receipts, + ./transaction_builder, + ./transaction_ssz, + ./signature, + ./transaction_codec, + ./types diff --git a/tests/ssz/block.nim b/tests/ssz/block.nim new file mode 100644 index 00000000..bf75ef98 --- /dev/null +++ b/tests/ssz/block.nim @@ -0,0 +1,169 @@ +import + std/[os, strutils, json], + stew/[byteutils, io2], + ssz_serialization, + ../../eth/[common, rlp], + ../../eth/ssz/[sszcodec, adapter, blocks_ssz, transaction_ssz], + unittest2 + +type + TxSSZ = transaction_ssz.Transaction + TxRLP = transactions.Transaction + +proc eip2718Dir*(): string = + (currentSourcePath.parentDir / ".." / "common" / "eip2718").normalizedPath + +proc rlpsDir*(): string = + (currentSourcePath.parentDir / ".." / "common" / "rlps").normalizedPath + +proc eip2718FilePath*(index: int): string = + eip2718Dir() / ("acl_block_" & $index & ".json") + +proc loadBlockFromFile*(path: string): EthBlock = + let n = json.parseFile(path) + if not n.hasKey("rlp"): + raise newException(ValueError, "JSON has no 'rlp' key") + let hexRlp = n["rlp"].getStr() + let bytes = hexToSeqByte(hexRlp) + rlp.decode(bytes, EthBlock) + +proc listRlpFiles*(): seq[string] = + let dir = rlpsDir() + if not dir.dirExists: + return @[] + for kind, path in walkDir(dir): + if kind == pcFile and path.endsWith(".rlp"): + result.add path + +proc loadRlpBlocksFromFile*(path: string, limit: int = 0): seq[EthBlock] = + let res = io2.readAllBytes(path) + if res.isErr: + raise newException(IOError, "Failed to read RLP file: " & path) + var r = rlpFromBytes(res.get) + var taken = 0 + while r.hasData and (limit <= 0 or taken < limit): + result.add r.read(EthBlock) + inc taken + +suite "Transaction List Roundtrip": + test "Block 9: RLP → SSZ → RLP preserves all transactions": + let path = eip2718FilePath(9) + if not path.fileExists: + skip() + + let blk = loadBlockFromFile(path) + let originalTxs = blk.transactions + var sszTxs: seq[TxSSZ] + for tx in originalTxs: + sszTxs.add(toSszTx(tx)) + var reconstructed: seq[TxRLP] + for tx in sszTxs: + reconstructed.add(toOldTx(tx)) + check reconstructed.len == originalTxs.len + + # Verify each transaction + for i in 0 ..< originalTxs.len: + check reconstructed[i].txType == originalTxs[i].txType + check reconstructed[i].chainId == originalTxs[i].chainId + check reconstructed[i].nonce == originalTxs[i].nonce + check reconstructed[i].gasLimit == originalTxs[i].gasLimit + check reconstructed[i].to == originalTxs[i].to + check reconstructed[i].value == originalTxs[i].value + check reconstructed[i].payload == originalTxs[i].payload + + test "All blocks 0-9: Transaction count preserved": + for i in 0 .. 9: + let path = eip2718FilePath(i) + if not path.fileExists: + continue + + let blk = loadBlockFromFile(path) + let originalTxs = blk.transactions + + # RLP → SSZ → RLP + var sszTxs: seq[TxSSZ] + for tx in originalTxs: + sszTxs.add(toSszTx(tx)) + + var reconstructed: seq[TxRLP] + for tx in sszTxs: + reconstructed.add(toOldTx(tx)) + + check reconstructed.len == originalTxs.len + + # Spot check first tx + if originalTxs.len > 0: + check reconstructed[0].nonce == originalTxs[0].nonce + check reconstructed[0].chainId == originalTxs[0].chainId + +suite "Transaction Root Computation (EIP-6404)": + test "Block 9: Root is deterministic": + let path = eip2718FilePath(9) + if not path.fileExists: + skip() + + let blk = loadBlockFromFile(path) + + # Compute multiple times + let root1 = computeTransactionsRootSsz(blk.transactions) + let root2 = computeTransactionsRootSsz(blk.transactions) + let root3 = computeTransactionsRootSsz(blk.transactions) + + # All identical + check root1 == root2 + check root2 == root3 + check root1 != default(Root) + + echo "\nTX Root: 0x", root1.data.toHex + + test "All blocks 0-9: Compute transaction roots": + echo "" + for i in 0 .. 9: + let path = eip2718FilePath(i) + if not path.fileExists: + continue + + let blk = loadBlockFromFile(path) + + # Compute root + let root1 = computeTransactionsRootSsz(blk.transactions) + let root2 = computeTransactionsRootSsz(blk.transactions) + + # Verify stable + check root1 == root2 + check root1 != default(Root) + + echo "Block ", + i, " (", blk.transactions.len, " txs): 0x", root1.data.toHex[0 .. 15], "..." + +suite "SSZ Encoding/Decoding": + test "Block 9: Individual transaction SSZ roundtrip": + let path = eip2718FilePath(9) + if not path.fileExists: + skip() + + let blk = loadBlockFromFile(path) + for i, rlpTx in blk.transactions: + let sszTx = toSszTx(rlpTx) + let encoded = SSZ.encode(sszTx) + let decoded = SSZ.decode(encoded, TxSSZ) # ← FIXED: Use type, not variable + let reconstructed = toOldTx(decoded) + + # Verify + check reconstructed.nonce == rlpTx.nonce + check reconstructed.chainId == rlpTx.chainId + check reconstructed.gasLimit == rlpTx.gasLimit + check reconstructed.value == rlpTx.value + + test "Block 9: Transaction list SSZ encoding": + let path = eip2718FilePath(9) + if not path.fileExists: + skip() + + let blk = loadBlockFromFile(path) + var sszTxs: seq[TxSSZ] + for tx in blk.transactions: + sszTxs.add(toSszTx(tx)) + let encoded = SSZ.encode(sszTxs) + let decoded = SSZ.decode(encoded, seq[TxSSZ]) # ← FIXED: Use type + check decoded.len == sszTxs.len diff --git a/tests/ssz/receipts.nim b/tests/ssz/receipts.nim new file mode 100644 index 00000000..9c313717 --- /dev/null +++ b/tests/ssz/receipts.nim @@ -0,0 +1,357 @@ +import + unittest2, + ssz_serialization/merkleization, + ssz_serialization, + macros, + std/sequtils, + ../../eth/common/[addresses, base, hashes], + ../../eth/ssz/[receipts_ssz, adapter] + +proc topicFill*(b: SomeInteger): Bytes32 = + var a: array[32, byte] + let v = byte(b) + for i in 0 ..< 32: + a[i] = v + Bytes32.copyFrom(a) + +template roundTrip*(v: var untyped) = + var bytes = SSZ.encode(v) + var v2 = SSZ.decode(bytes, v.type) + var bytes2 = SSZ.encode(v2) + check bytes == bytes2 + +template topicList(args: varargs[Bytes32]): untyped = + List[Bytes32, MAX_TOPICS_PER_LOG].init(@args) + +macro testRT*(name: static[string], expr: typed): untyped = + ## Roundtrip SSZ + size check. + let valueSym = genSym(nskLet, "rtValue") + let bytesSym = genSym(nskLet, "rtEncoded") + let value2Sym = genSym(nskVar, "rtDecoded") + let bytes2Sym = genSym(nskLet, "rtReencoded") + + result = quote: + test `name`: + let `valueSym` = `expr` + when compiles(encodeReceipt(`valueSym`)): + let `bytesSym` = encodeReceipt(`valueSym`) + var `value2Sym` = decodeReceipt[type(`valueSym`)](`bytesSym`) + let `bytes2Sym` = encodeReceipt(`value2Sym`) + check `bytesSym` == `bytes2Sym` + check sszSize(asTagged(`valueSym`)) == `bytesSym`.len + else: + let `bytesSym` = SSZ.encode(`valueSym`) + var `value2Sym` = SSZ.decode(`bytesSym`, type(`valueSym`)) + let `bytes2Sym` = SSZ.encode(`value2Sym`) + check `bytesSym` == `bytes2Sym` + check sszSize(`valueSym`) == `bytesSym`.len + +macro testRT*(name: static[string], expr: typed, body: untyped): untyped = + ## Same as above, with an extra assertions block. + let valueSym = genSym(nskLet, "rtValue") + let bytesSym = genSym(nskLet, "rtEncoded") + let value2Sym = genSym(nskVar, "rtDecoded") + let bytes2Sym = genSym(nskLet, "rtReencoded") + let userAlias = ident("v") + + result = quote: + test `name`: + let `valueSym` = `expr` + when compiles(encodeReceipt(`valueSym`)): + let `bytesSym` = encodeReceipt(`valueSym`) + var `value2Sym` = decodeReceipt[type(`valueSym`)](`bytesSym`) + let `bytes2Sym` = encodeReceipt(`value2Sym`) + check `bytesSym` == `bytes2Sym` + check sszSize(asTagged(`valueSym`)) == `bytesSym`.len + else: + let `bytesSym` = SSZ.encode(`valueSym`) + var `value2Sym` = SSZ.decode(`bytesSym`, type(`valueSym`)) + let `bytes2Sym` = SSZ.encode(`value2Sym`) + check `bytesSym` == `bytes2Sym` + check sszSize(`valueSym`) == `bytesSym`.len + block: + let `userAlias` = `valueSym` + `body` + +suite "Log Construction (SSZ)": + testRT "Log: empty topics", + Log(address: addresses.zeroAddress, topics: topicList(), data: @[]) + + testRT "Log: max topics", + ( + block: + let addrAA = Address.copyFrom(newSeqWith(20, byte 0xAA)) + Log( + address: addrAA, + topics: topicList( + topicFill(0x10), topicFill(0x11), topicFill(0x12), topicFill(0x13) + ), + data: @[byte 0xDE, 0xAD, 0xBE, 0xEF], + ) + ) + + testRT "Log: 4 topics, some data", + ( + block: + let addr22 = Address.copyFrom(newSeqWith(20, byte 0x22)) + var a0, a1, a2, a3: array[32, byte] + for i in 0 ..< 32: + a0[i] = 0xA0'u8 + a1[i] = 0xA1'u8 + a2[i] = 0xA2'u8 + a3[i] = 0xA3'u8 + Log( + address: addr22, + topics: topicList( + Bytes32.copyFrom(a0), + Bytes32.copyFrom(a1), + Bytes32.copyFrom(a2), + Bytes32.copyFrom(a3), + ), + data: @[byte 0xDE, 0xAD, 0xBE, 0xEF], + ) + ) + + testRT "Log decode sanity", + ( + block: + let addr33 = Address.copyFrom(newSeqWith(20, byte 0x33)) + var t1, t2: array[32, byte] + for i in 0 ..< 32: + t1[i] = 1 + t2[i] = 2 + Log( + address: addr33, + topics: topicList(Bytes32.copyFrom(t1), Bytes32.copyFrom(t2)), + data: @[byte 1, 2, 3, 4], + ) + ): + let d = SSZ.decode(SSZ.encode(v), Log) + check d.address == v.address + check d.topics[0] == v.topics[0] + check d.topics[1] == v.topics[1] + check d.data == v.data + + testRT "Log: large progressive data (128 KiB)", + ( + block: + let a77 = Address.copyFrom(newSeqWith(20, byte 0x77)) + var t1, t2: array[32, byte] + for i in 0 ..< 32: + t1[i] = 1 + t2[i] = 2 + var big = newSeq[byte](128 * 1024) + for i in 0 ..< big.len: + big[i] = byte(i and 0xFF) + Log( + address: a77, + topics: topicList(Bytes32.copyFrom(t1), Bytes32.copyFrom(t2)), + data: big, + ) + ): + check v.data.len == 128 * 1024 + +suite "Receipts Construction (SSZ)": + testRT "Basic Receipt empty", + BasicReceipt( + `from`: addresses.zeroAddress, + gas_used: 100'u64, + logs: @[], + status: true, + ) + + testRT "Basic receipt data", + ( + block: + let log0 = Log(address: default(Address), topics: topicList(), data: @[]) + BasicReceipt( + `from`: default(Address), + gas_used: 100'u64, + logs: @[log0], + status: true, + ) + ): + check v.gas_used == 100'u64 + check v.status == true + check v.logs.len == 1 + + testRT "CreateReceipt: no logs", + ( + block: + let fromBB = Address.copyFrom(newSeqWith(20, byte 0xBB)) + let addrCC = Address.copyFrom(newSeqWith(20, byte 0xCC)) + CreateReceipt( + `from`: fromBB, + gas_used: 42'u64, + contract_address: addrCC, + logs: @[], + status: false, + ) + ): + check v.logs.len == 0 + + testRT "Create receipt: logs 1", + ( + block: + let log1 = Log( + address: default(Address), topics: topicList(), data: @[byte 0x01, 0x02, 0x03] + ) + let createdAddr = address"0x00000000000000000000000000000000000000aa" + CreateReceipt( + `from`: address"0x00000000000000000000000000000000000000bb", + gas_used: 21000'u64, + contract_address: createdAddr, + logs: @[log1], + status: false, + ) + ): + check v.gas_used == 21000'u64 + check v.status == false + check v.logs.len == 1 + + testRT "SetCode receipt", + ( + block: + let log2 = Log( + address: address"0x00000000000000000000000000000000000000cc", + topics: topicList( + Bytes32.default, Bytes32.default, Bytes32.default, Bytes32.default + ), + data: @[], + ) + SetCodeReceipt( + `from`: address"0x00000000000000000000000000000000000000dd", + gas_used: 42000'u64, + logs: @[log2], + status: true, + authorities: + @[ + address"0x00000000000000000000000000000000000000f1", + address"0x00000000000000000000000000000000000000f2", + ], + ) + ): + check v.gas_used == 42000'u64 + check v.status == true + check v.authorities.len == 2 + check v.logs.len == 1 + +suite "Block receipts root (SSZ)": + test "receipts root for 3 receipts: non-zero and stable": + let r0 = toReceipt( + BasicReceipt( + `from`: addresses.zeroAddress, + gas_used: 21_000'u64, + logs: @[], + status: true, + ) + ) + let r1 = toReceipt( + CreateReceipt( + `from`: address"0x0000000000000000000000000000000000000001", + gas_used: 42_000'u64, + contract_address: address"0x00000000000000000000000000000000000000aa", + logs: @[], + status: false, + ) + ) + let r2 = toReceipt( + SetCodeReceipt( + `from`: address"0x00000000000000000000000000000000000000bb", + gas_used: 63_000'u64, + logs: @[], + status: true, + authorities: @[address"0x00000000000000000000000000000000000000f1"], + ) + ) + var receipts: seq[Receipt] = @[r0, r1, r2] + let root1 = hash_tree_root(receipts) + +test "receipts root changes when a receipt changes": + var receipts = + @[ + BasicReceipt( + `from`: default(Address), + gas_used: 1'u64, + logs: @[], + status: true, + ), + BasicReceipt( + `from`: default(Address), + gas_used: 2'u64, + logs: @[], + status: true, + ), + ] + let rootA = hash_tree_root(receipts) + receipts[0].gas_used = 3'u64 + let rootB = hash_tree_root(receipts) + check rootA != rootB + +test "receipts root is order-sensitive": + let a = BasicReceipt( + `from`: default(Address), + gas_used: 1'u64, + logs: @[], + status: true, + ) + let b = BasicReceipt( + `from`: default(Address), + gas_used: 2'u64, + logs: @[], + status: true, + ) + let list1 = @[a, b] + let list2 = @[b, a] + let r1 = hash_tree_root(list1) + let r2 = hash_tree_root(list2) + check r1 != r2 + +suite "SSZ root": + test "hash_tree_root for Log": + let log = Log( + address: addresses.zeroAddress, + topics: + topicList(Bytes32.default, Bytes32.default, Bytes32.default, Bytes32.default), + data: @[], + ) + let root = hash_tree_root(log) + + test "hash_tree_root for list of Log": + let log = Log( + address: addresses.zeroAddress, + topics: + topicList(Bytes32.default, Bytes32.default, Bytes32.default, Bytes32.default), + data: @[], + ) + let logs = @[log] + let root = hash_tree_root(logs) + + test "hash_tree_root for BasicReceipt": + let r = BasicReceipt( + `from`: addresses.zeroAddress, + gas_used: 100'u64, + logs: @[], + status: true, + ) + let root = hash_tree_root(r) + + test "hash_tree_root for CreateReceipt": + let r = CreateReceipt( + `from`: address"0x0000000000000000000000000000000000000001", + gas_used: 42_000'u64, + contract_address: address"0x00000000000000000000000000000000000000aa", + logs: @[], + status: false, + ) + let root = hash_tree_root(r) + + test "hash_tree_root for SetCodeReceipt": + let r = SetCodeReceipt( + `from`: address"0x00000000000000000000000000000000000000bb", + gas_used: 63_000'u64, + logs: @[], + status: true, + authorities: @[address"0x00000000000000000000000000000000000000f1"], + ) + let root = hash_tree_root(r) diff --git a/tests/ssz/signature.nim b/tests/ssz/signature.nim new file mode 100644 index 00000000..584062f0 --- /dev/null +++ b/tests/ssz/signature.nim @@ -0,0 +1,83 @@ +import unittest2, stew/byteutils, stint, ../../eth/ssz/[signatures] + +suite "secp256k1 execution signatures": + test "pack/unpack roundtrip": + let r = UInt256.fromHex( + "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20" + ) + let s = (SECP256K1N div 2'u256) # low-s boundary (valid) + let y: uint8 = 1 + let sig = secp256k1Pack(r, s, y) + let (rr, ss, yy) = secp256k1Unpack(sig) + check rr == r + check ss == s + check yy == y + check secp256k1Validate(sig) + + test "validate rejects high-s and bad parity": + let r = 1.u256 + let sHigh = (SECP256K1N div 2'u256) + 1'u256 + let sigHigh = secp256k1Pack(r, sHigh, 0) + check not secp256k1Validate(sigHigh) + + let sigBadParity = secp256k1Pack(r, (SECP256K1N div 2'u256), 2'u8) + check not secp256k1Validate(sigBadParity) + + test "legacy V <-> yParity roundtrip (pre-155)": + let y0: uint8 = 0 + let y1: uint8 = 1 + let v0 = legacyVFromParity(y0, 0'u64, false) + let v1 = legacyVFromParity(y1, 0'u64, false) + check yParityFromLegacyV(v0, false) == y0 + check yParityFromLegacyV(v1, false) == y1 + check v0 in {27'u64, 28'u64} + check v1 in {27'u64, 28'u64} + + test "legacy V <-> yParity roundtrip (EIP-155)": + let chainId = 1'u64 + for y in [0'u8, 1'u8]: + let v = legacyVFromParity(y, chainId, true) + check yParityFromLegacyV(v, true) == y + check v == 35'u64 + (2 * chainId) + uint64(y) or + v == 36'u64 + (2 * chainId) + uint64(y) + +test "keccak32 sign & recover": + when compiles(PrivateKey.fromHex): + let sk = PrivateKey.fromHex( + "0x46c5d3e7b0f4d01caa9c2025a3d49b9d2a8d3e4edb2f7a1b6c3e2d1f0a9b8c7d" + ).valueOr: + raise newException(ValueError, "could not parse test seckey") + let msg = "deadbeefcafebabe".toBytes + let h = keccak256(msg) + + let sig = signKeccak32(sk, h) + check secp256k1Validate(sig) + + let recAddr = secp256k1RecoverSigner(sig, h) + + when compiles(sk.toPublicKey): + let pk = sk.toPublicKey() + when compiles(pk.to(Address)): + let expected = pk.to(Address) + check recAddr == expected + else: + let rec2 = secp256k1RecoverSigner(sig, h) + check rec2 == recAddr + else: + let rec2 = secp256k1RecoverSigner(sig, h) + check rec2 == recAddr + else: + skip() + + test "signKeccak32 r/s not zero and y in {0,1}": + when compiles(PrivateKey.fromHex): + let sk = PrivateKey.fromHex("0x1".repeat(32)).valueOr: + raise newException(ValueError, "could not parse test seckey") + let h = keccak256("hello".toBytes) + let sig = signKeccak32(sk, h) + let (r, s, y) = secp256k1Unpack(sig) + check r != 0.u256 + check s != 0.u256 + check y == 0'u8 or y == 1'u8 + else: + skip() diff --git a/tests/ssz/transaction_builder.nim b/tests/ssz/transaction_builder.nim new file mode 100644 index 00000000..8976b16e --- /dev/null +++ b/tests/ssz/transaction_builder.nim @@ -0,0 +1,204 @@ +import + unittest2, + stew/byteutils, + stint, + ../../eth/ssz/[transaction_ssz, transaction_builder, signatures], + ../../eth/common/[addresses, base, hashes] + +const + recipient = address"095e7baea6a6c7c4c2dfeb977efac326af552d87" + source = address"0x0000000000000000000000000000000000000001" + storageKey = default(Bytes32) + abcdef = hexToSeqByte("abcdef") + +let accesses: seq[AccessTuple] = + @[AccessTuple(address: source, storage_keys: @[Hash32(storageKey)])] + +proc dummySig(): Secp256k1ExecutionSignature = + secp256k1Pack(1.u256, 1.u256, 0'u8) + +suite "SSZ Transactions (constructor)": + test "Legacy Call": + let tx = Transaction( + txType = 0x00'u8, + chain_id = ChainId(1.u256), + nonce = 1'u64, + gas = 21_000'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txLegacyBasic + check tx.rlp.legacyBasic.payload.to == recipient + + test "Legacy Create": + let tx = Transaction( + txType = 0x00'u8, + chain_id = ChainId(1.u256), + nonce = 2'u64, + gas = 50_000'u64, + to = Opt.none(Address), # create + value = 0.u256, + input = abcdef, # initcode must be non-empty + max_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txLegacyCreate + check tx.rlp.legacyCreate.payload.input.len == abcdef.len + + test "2930 Call (non-empty access list)": + let tx = Transaction( + txType = 0x01'u8, + chain_id = ChainId(1.u256), + nonce = 3'u64, + gas = 123_457'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + access_list = accesses, + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txAccessListBasic + check tx.rlp.accessListBasic.payload.access_list.len == 1 + + test "2930 Create (empty access list)": + let tx = Transaction( + txType = 0x01'u8, + chain_id = ChainId(1.u256), + nonce = 4'u64, + gas = 123_457'u64, + to = Opt.none(Address), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + # access_list defaults to @[] + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txAccessListCreate + check tx.rlp.accessListCreate.payload.access_list.len == 0 + + test "1559 Call": + let tx = Transaction( + txType = 0x02'u8, + chain_id = ChainId(1.u256), + nonce = 5'u64, + gas = 123_457'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + access_list = accesses, + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txBasic + check tx.rlp.basic.payload.max_priority_fees_per_gas.regular == 2.u256 + + test "1559 Create": + let tx = Transaction( + txType = 0x02'u8, + chain_id = ChainId(1.u256), + nonce = 6'u64, + gas = 123_457'u64, + to = Opt.none(Address), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txCreate + check tx.rlp.create.payload.input.len == abcdef.len + + test "4844 Blob Tx": + const d = hash32"010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014" + let tx = Transaction( + txType = 0x03'u8, + chain_id = ChainId(1.u256), + nonce = 7'u64, + gas = 123_457'u64, + to = Opt.some(recipient), + value = 0.u256, + input = @[], + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 1.u256), + access_list = accesses, + blob_versioned_hashes = @[VersionedHash(d)], + blob_fee = 10.u256, + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txBlob + check tx.rlp.blob.payload.blob_versioned_hashes.len == 1 + + test "7702 SetCode with replayable-basic auth": + let auths: seq[AuthTuple] = + @[ + ( + chain_id: ChainId(0.u256), + address: recipient, + nonce: 0'u64, + y_parity: 0'u8, + r: 1.u256, + s: 1.u256, + ) + ] + let tx = Transaction( + txType = 0x04'u8, + chain_id = ChainId(1.u256), + nonce = 8'u64, + gas = 21000'u64, + to = Opt.some(recipient), + value = 0.u256, + input = @[], + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 1.u256), + access_list = @[], + authorization_list = auths, + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txSetCode + check tx.rlp.setCode.payload.authorization_list.len == 1 + check tx.rlp.setCode.payload.authorization_list[0].payload.kind == + authReplayableBasic + + test "7702 SetCode with basic auth": + let auths: seq[AuthTuple] = + @[ + ( + chain_id: ChainId(1.u256), + address: recipient, + nonce: 0'u64, + y_parity: 0'u8, + r: 1.u256, + s: 1.u256, + ) + ] + let tx = Transaction( + txType = 0x04'u8, + chain_id = ChainId(1.u256), + nonce = 9'u64, + gas = 21001'u64, + to = Opt.some(recipient), + value = 0.u256, + input = @[], + max_fees_per_gas = BasicFeesPerGas(regular: 11.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 1.u256), + access_list = @[], + authorization_list = auths, + signature = dummySig(), + ) + check tx.kind == RlpTransaction + check tx.rlp.kind == txSetCode + check tx.rlp.setCode.payload.authorization_list.len == 1 + check tx.rlp.setCode.payload.authorization_list[0].payload.kind == authBasic diff --git a/tests/ssz/transaction_codec.nim b/tests/ssz/transaction_codec.nim new file mode 100644 index 00000000..3340363e --- /dev/null +++ b/tests/ssz/transaction_codec.nim @@ -0,0 +1,284 @@ +import + unittest, + stew/byteutils, + std/sequtils, + ../../eth/ssz/[sszcodec, transaction_ssz, transaction_builder, signatures, adapter], + ../../eth/common/[addresses, hashes, base, eth_types_json_serialization, transactions], + ../../eth/rlp, + ssz_serialization, + ../common/test_transactions + +const + recipient = address"095e7baea6a6c7c4c2dfeb977efac326af552d87" + zeroG1 = + bytes48"0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + source = address"0x0000000000000000000000000000000000000001" + storageKey = default(Bytes32) + accesses = @[AccessPair(address: source, storageKeys: @[storageKey])] + abcdef = hexToSeqByte("abcdef") + +template sszRoundTrip(txFunc: untyped, i: int) = + let oldTx = txFunc(i) + let sszTx = toSszTx(oldTx) + check sszTx.kind == RlpTransaction + let rlptxn = toOldTx(sszTx) + +template sszFullRoundTrip(txFunc: untyped, i: int) = + ## RLP -> SSZ -> RLP + let oldTx = txFunc(i) + let sszTx = toSszTx(oldTx) + let back = toOldTx(sszTx) + check back == oldTx + +template sszDoubleRoundTrip(txFunc: untyped, i: int) = + let oldTx = txFunc(i) + let sszTx = toSszTx(oldTx) + let oldBack = toOldTx(sszTx) + let sszBack = toSszTx(oldBack) + # Work around Nim case object comparison limitation https://github.com/nim-lang/Nim/issues/6676 + check oldBack.txType == oldTx.txType + check oldBack.chainId == oldTx.chainId + check oldBack.nonce == oldTx.nonce + check oldBack.gasLimit == oldTx.gasLimit + check oldBack.to == oldTx.to + check oldBack.value == oldTx.value + check oldBack.payload == oldTx.payload + check sszBack.kind == sszTx.kind + check SSZ.encode(sszBack) == SSZ.encode(sszTx) + +suite "SSZ Transactions (full round-trip)": + test "Legacy Tx Call": + sszRoundTrip(tx0, 1) + test "Legacy tx contract creation": + sszRoundTrip(tx1, 2) + + test "Tx with non-zero access list": + sszRoundTrip(tx2, 3) + + test "Tx with empty access list": + sszRoundTrip(tx3, 4) + + test "Contract creation with access list": + sszRoundTrip(tx4, 5) + + test "Dynamic Fee Tx": + sszRoundTrip(tx5, 6) + + # Will never work as blob Txns must have a To + # test "NetworkBlob Tx": + # sszRoundTrip(tx6, 7) + + # test "Minimal Blob Tx": + # sszRoundTrip(tx7, 8) + + # Will never work as blob Txns must have a To + test "Minimal Blob Tx contract creation": + sszRoundTrip(tx8, 9) + +suite "Transactions SSZ Codec: 7702 SetCode (RLP ↔ SSZ)": + test "7702 with auth: RLP -> SSZ": + let oldTx = txEip7702(1) + let sszTx = toSszTx(oldTx) + + check sszTx.kind == RlpTransaction + check sszTx.rlp.kind == txSetCode + + # Verify authorization list was converted + let authList = sszTx.rlp.setCode.payload.authorization_list + check authList.len == 1 + check authList[0].payload.kind == authBasic + check authList[0].payload.basic.chain_id == ChainId(1.u256) + check authList[0].payload.basic.address == source + check authList[0].payload.basic.nonce == 2 + + test "7702 with auth: RLP -> SSZ -> RLP (Full Roundtrip)": + let oldTx = txEip7702(1) + let sszTx = toSszTx(oldTx) + let backTx = toOldTx(sszTx) + + # Verify all fields match + check backTx.txType == oldTx.txType + check backTx.chainId == oldTx.chainId + check backTx.nonce == oldTx.nonce + check backTx.maxPriorityFeePerGas == oldTx.maxPriorityFeePerGas + check backTx.maxFeePerGas == oldTx.maxFeePerGas + check backTx.gasLimit == oldTx.gasLimit + check backTx.to == oldTx.to + check backTx.value == oldTx.value + check backTx.payload == oldTx.payload + check backTx.accessList == oldTx.accessList + + check backTx.authorizationList.len == oldTx.authorizationList.len + check backTx.authorizationList.len == 1 + + let origAuth = oldTx.authorizationList[0] + let backAuth = backTx.authorizationList[0] + check backAuth.chainId == origAuth.chainId + check backAuth.address == origAuth.address + check backAuth.nonce == origAuth.nonce + check backAuth.yParity == origAuth.yParity + check backAuth.r == origAuth.r + check backAuth.s == origAuth.s + + test "7702 with auth: Double Roundtrip (RLP -> SSZ -> RLP -> SSZ)": + sszDoubleRoundTrip(txEip7702, 1) + + test "7702 authorization data integrity": + let oldTx = txEip7702(5) + let sszTx = toSszTx(oldTx) + let backTx = toOldTx(sszTx) + + check backTx == oldTx + + for i, auth in oldTx.authorizationList: + check backTx.authorizationList[i].chainId == auth.chainId + check backTx.authorizationList[i].address == auth.address + check backTx.authorizationList[i].nonce == auth.nonce + check backTx.authorizationList[i].yParity == auth.yParity + check backTx.authorizationList[i].r == auth.r + check backTx.authorizationList[i].s == auth.s + + test "7702 with replayable auth (chainId = 0)": + var tx = txEip7702(1) + tx.authorizationList[0].chainId = ChainId(0.u256) + + let sszTx = toSszTx(tx) + check sszTx.rlp.setCode.payload.authorization_list.len == 1 + check sszTx.rlp.setCode.payload.authorization_list[0].payload.kind == + authReplayableBasic + let backTx = toOldTx(sszTx) + check backTx.authorizationList[0].chainId == ChainId(0.u256) + check backTx == tx + + test "7702 with mixed authorization types": + var tx = txEip7702(1) + + tx.authorizationList.add transactions.Authorization( + chainId: ChainId(0.u256), + address: recipient, + nonce: 5.AccountNonce, + yParity: 1, + r: 999.u256, + s: 888.u256, + ) + + let sszTx = toSszTx(tx) + let authList = sszTx.rlp.setCode.payload.authorization_list + + check authList.len == 2 + check authList[0].payload.kind == authBasic + check authList[1].payload.kind == authReplayableBasic + + let backTx = toOldTx(sszTx) + check backTx.authorizationList.len == 2 + check backTx.authorizationList[0].chainId == ChainId(1.u256) + check backTx.authorizationList[1].chainId == ChainId(0.u256) + check backTx == tx + + test "7702 with multiple authorizations (5 entries)": + var tx = txEip7702(1) + for i in 1 .. 4: + tx.authorizationList.add transactions.Authorization( + chainId: ChainId(u256(i)), + address: Address.copyFrom(newSeqWith(20, byte(i))), + nonce: AccountNonce(i * 10), + yParity: uint8(i mod 2), + r: u256(100 + i), + s: u256(200 + i), + ) + + let sszTx = toSszTx(tx) + check sszTx.rlp.setCode.payload.authorization_list.len == 5 + + let backTx = toOldTx(sszTx) + check backTx.authorizationList.len == 5 + check backTx == tx + + for i in 0 .. 4: + check backTx.authorizationList[i] == tx.authorizationList[i] + + test "7702 authorization with zero address": + var tx = txEip7702(1) + tx.authorizationList[0].address = zeroAddress + + let sszTx = toSszTx(tx) + let backTx = toOldTx(sszTx) + + check backTx.authorizationList[0].address == zeroAddress + check backTx == tx + + test "7702 authorization signature values": + ## Test that signature r, s, yParity values are preserved + var tx = txEip7702(1) + tx.authorizationList[0].r = u256"12345678901234567890123456789012345678901234567890" + tx.authorizationList[0].s = u256"98765432109876543210987654321098765432109876543210" + tx.authorizationList[0].yParity = 1 + + let sszTx = toSszTx(tx) + let backTx = toOldTx(sszTx) + + check backTx.authorizationList[0].r == tx.authorizationList[0].r + check backTx.authorizationList[0].s == tx.authorizationList[0].s + check backTx.authorizationList[0].yParity == 1 + check backTx == tx + + test "7702 with access list and authorization list": + var tx = txEip7702(1) + + let sszTx = toSszTx(tx) + let backTx = toOldTx(sszTx) + + check backTx.accessList.len == tx.accessList.len + check backTx.authorizationList.len == tx.authorizationList.len + check backTx == tx + +suite "Transactions SSZ Codec: Double Roundtrip": + test "Legacy Call: double roundtrip": + sszDoubleRoundTrip(tx0, 1) + + test "Legacy Create: double roundtrip": + sszDoubleRoundTrip(tx1, 2) + + test "AccessList Call: double roundtrip": + sszDoubleRoundTrip(tx2, 3) + + test "AccessList Create: double roundtrip": + sszDoubleRoundTrip(tx4, 5) + + test "DynamicFee: double roundtrip": + sszDoubleRoundTrip(tx5, 6) + + test "Blob Tx: double roundtrip": + sszDoubleRoundTrip(tx8, 9) + + test "7702 SetCode: double roundtrip": + sszDoubleRoundTrip(txEip7702, 1) + +suite "Transactions SSZ Codec: Mixed Transaction Lists": + test "Mixed list including 7702": + let txs = + @[ + tx0(1), # Legacy + tx2(2), # AccessList + tx5(3), # DynamicFee + tx8(4), # Blob + txEip7702(5), # 7702 SetCode + ] + + var sszTxs: seq[typeof(toSszTx(txs[0]))] = @[] + for tx in txs: + sszTxs.add toSszTx(tx) + + check sszTxs.len == 5 + + var backTxs: seq[transactions.Transaction] = @[] + for sszTx in sszTxs: + backTxs.add toOldTx(sszTx) + + check backTxs.len == 5 + + for i in 0 ..< txs.len: + check backTxs[i].txType == txs[i].txType + check backTxs[i].nonce == txs[i].nonce + + check backTxs[4].authorizationList.len == txs[4].authorizationList.len diff --git a/tests/ssz/transaction_ssz.nim b/tests/ssz/transaction_ssz.nim new file mode 100644 index 00000000..f4113b7c --- /dev/null +++ b/tests/ssz/transaction_ssz.nim @@ -0,0 +1,390 @@ +import + unittest2, + ssz_serialization, + ssz_serialization/merkleization, + macros, + std/sequtils, + stew/byteutils, + stint, + ../../eth/common/[addresses, base, hashes], + ../../eth/ssz/[transaction_ssz, transaction_builder, signatures, adapter] + +const + recipient = address"095e7baea6a6c7c4c2dfeb977efac326af552d87" + source = address"0x0000000000000000000000000000000000000001" + storageKey = default(Bytes32) + abcdef = hexToSeqByte("abcdef") + +let accesses: seq[AccessTuple] = + @[AccessTuple(address: source, storage_keys: @[Hash32(storageKey)])] + +proc dummySig(): Secp256k1ExecutionSignature = + secp256k1Pack(1.u256, 1.u256, 0'u8) + +macro txRT*(name: static[string], expr: typed): untyped = + ## SSZ roundtrip + size check for a Transaction-like value. + let v = genSym(nskLet, "txValue") + let b1 = genSym(nskLet, "enc1") + let v2 = genSym(nskVar, "dec") + let b2 = genSym(nskLet, "enc2") + result = quote: + test `name`: + let `v` = `expr` + let `b1` = SSZ.encode(`v`) + # echo "SSZ bytes (", `name`, ") [", `b1`.len, "]: 0x", `b1`.toHex() + var `v2` = SSZ.decode(`b1`, type(`v`)) + let `b2` = SSZ.encode(`v2`) + check `b1` == `b2` + # check sszSize(`v`) == `b1`.len + +macro txRT*(name: static[string], expr: typed, body: untyped): untyped = + # echo "RAW ARG AST:\n", treeRepr(expr) + let v = genSym(nskLet, "txValue") + let b1 = genSym(nskLet, "enc1") + let v2 = genSym(nskVar, "dec") + let b2 = genSym(nskLet, "enc2") + let t = ident("t") + let d = ident("d") + # echo treeRepr(result) + result = quote: + test `name`: + let `v` = `expr` + let `b1` = SSZ.encode(`v`) + # echo "SSZ bytes (", `name`, ") [", `b1`.len, "]: 0x", `b1`.toHex() + var `v2` = SSZ.decode(`b1`, type(`v`)) + let `b2` = SSZ.encode(`v2`) + check `b1` == `b2` + # check sszSize(`v`) == `b1`.len + block: + let `t` = `v` + let `d` = `v2` + `body` + +suite "SSZ Transactions (round-trip)": + txRT "SSZ: Legacy Call", + Transaction( + txType = 0x00'u8, + chain_id = ChainId(1.u256), + nonce = 1'u64, + gas = 21_000'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + signature = dummySig(), + ): + check d.rlp.legacyBasic.payload.txType == 0x00'u8 + check d.rlp.legacyBasic.payload.to == t.rlp.legacyBasic.payload.to + check d.rlp.legacyBasic.payload.nonce == t.rlp.legacyBasic.payload.nonce + check d.rlp.legacyBasic.payload.gas == t.rlp.legacyBasic.payload.gas + check d.rlp.legacyBasic.payload.input == t.rlp.legacyBasic.payload.input + check d.rlp.legacyBasic.signature == t.rlp.legacyBasic.signature + + txRT "SSZ: Legacy Create", + Transaction( + txType = 0x00'u8, + chain_id = ChainId(1.u256), + nonce = 2'u64, + gas = 50_000'u64, + to = Opt.none(Address), + value = 0.u256, + input = abcdef, # initcode present + max_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + signature = dummySig(), + ): + check d.rlp.legacyCreate.payload.txType == 0x00'u8 + check d.rlp.legacyCreate.payload.input.len == abcdef.len + + txRT "SSZ: 2930 Call", + Transaction( + txType = 0x01'u8, + chain_id = ChainId(1.u256), + nonce = 3'u64, + gas = 123_457'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + access_list = accesses, + signature = dummySig(), + ): + check d.rlp.accessListBasic.payload.txType == 0x01'u8 + check d.rlp.accessListBasic.payload.access_list.len == 1 + check d.rlp.accessListBasic.payload.access_list[0].address == source + + txRT "SSZ: 2930 Create", + Transaction( + txType = 0x01'u8, + chain_id = ChainId(1.u256), + nonce = 4'u64, + gas = 123_457'u64, + to = Opt.none(Address), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + signature = dummySig(), + ): + check d.rlp.accessListCreate.payload.txType == 0x01'u8 + + txRT "SSZ: 1559 Call", + Transaction( + txType = 0x02'u8, + chain_id = ChainId(1.u256), + nonce = 5'u64, + gas = 123_457'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + access_list = accesses, + signature = dummySig(), + ): + check d.rlp.basic.payload.txType == 0x02'u8 + check d.rlp.basic.payload.max_priority_fees_per_gas.regular == 2.u256 + + txRT "SSZ: 1559 Create", + Transaction( + txType = 0x02'u8, + chain_id = ChainId(1.u256), + nonce = 6'u64, + gas = 123_457'u64, + to = Opt.none(Address), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + signature = dummySig(), + ): + check d.rlp.create.payload.txType == 0x02'u8 + check d.rlp.create.payload.input.len == abcdef.len + + # # when compiles(BlobFeesPerGas): + txRT "SSZ: 4844 Blob Tx", + ( + block: + const vh = + hash32"010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014" + Transaction( + txType = 0x03'u8, + chain_id = ChainId(1.u256), + nonce = 7'u64, + gas = 123_457'u64, + to = Opt.some(recipient), + value = 0.u256, + input = @[], + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 1.u256), + access_list = accesses, + blob_versioned_hashes = @[VersionedHash(vh)], + blob_fee = 10.u256, + signature = dummySig(), + ) + ): + check d.rlp.blob.payload.txType == 0x03'u8 + check d.rlp.blob.payload.blob_versioned_hashes.len == 1 + +suite "SSZ Transactions: 7702 SetCode (round-trip)": + txRT "SSZ: 7702 SetCode with replayable auth", + ( + block: + let auths: seq[AuthTuple] = + @[ + ( + chain_id: ChainId(0.u256), # Replayable + address: source, + nonce: 0'u64, + y_parity: 0'u8, + r: 1.u256, + s: 1.u256, + ) + ] + Transaction( + txType = 0x04'u8, + chain_id = ChainId(1.u256), + nonce = 8'u64, + gas = 21001'u64, + to = Opt.some(recipient), + value = 0.u256, + input = @[], + max_fees_per_gas = BasicFeesPerGas(regular: 10.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 1.u256), + access_list = @[], + authorization_list = auths, + signature = dummySig(), + ) + ): + check d.rlp.setCode.payload.txType == 0x04'u8 + check d.rlp.setCode.payload.authorization_list.len == 1 + check d.rlp.setCode.payload.authorization_list[0].payload.kind == authReplayableBasic + check d.rlp.setCode.payload.authorization_list[0].payload.replayable.address == + source + check d.rlp.setCode.payload.authorization_list[0].payload.replayable.nonce == 0 + + txRT "SSZ: 7702 SetCode with basic auth", + ( + block: + let auths: seq[AuthTuple] = + @[ + ( + chain_id: ChainId(1.u256), + address: recipient, + nonce: 5'u64, + y_parity: 1'u8, + r: 100.u256, + s: 200.u256, + ) + ] + Transaction( + txType = 0x04'u8, + chain_id = ChainId(1.u256), + nonce = 9'u64, + gas = 21001'u64, + to = Opt.some(recipient), + value = 0.u256, + input = @[], + max_fees_per_gas = BasicFeesPerGas(regular: 11.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 1.u256), + access_list = @[], + authorization_list = auths, + signature = dummySig(), + ) + ): + check d.rlp.setCode.payload.txType == 0x04'u8 + check d.rlp.setCode.payload.authorization_list.len == 1 + check d.rlp.setCode.payload.authorization_list[0].payload.kind == authBasic + check d.rlp.setCode.payload.authorization_list[0].payload.basic.chain_id == + ChainId(1.u256) + check d.rlp.setCode.payload.authorization_list[0].payload.basic.address == recipient + check d.rlp.setCode.payload.authorization_list[0].payload.basic.nonce == 5 + + txRT "SSZ: 7702 SetCode with mixed auth list", + ( + block: + let auths: seq[AuthTuple] = + @[ + ( + chain_id: ChainId(0.u256), # Replayable + address: source, + nonce: 1'u64, + y_parity: 0'u8, + r: 10.u256, + s: 20.u256, + ), + ( + chain_id: ChainId(1.u256), # Basic + address: recipient, + nonce: 2'u64, + y_parity: 1'u8, + r: 30.u256, + s: 40.u256, + ), + ( + chain_id: ChainId(5.u256), # Basic + address: source, + nonce: 3'u64, + y_parity: 0'u8, + r: 50.u256, + s: 60.u256, + ), + ] + Transaction( + txType = 0x04'u8, + chain_id = ChainId(1.u256), + nonce = 10'u64, + gas = 21001'u64, + to = Opt.some(recipient), + value = 100.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 15.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + access_list = accesses, + authorization_list = auths, + signature = dummySig(), + ) + ): + check d.rlp.setCode.payload.txType == 0x04'u8 + check d.rlp.setCode.payload.authorization_list.len == 3 + check d.rlp.setCode.payload.authorization_list[0].payload.kind == authReplayableBasic + check d.rlp.setCode.payload.authorization_list[1].payload.kind == authBasic + check d.rlp.setCode.payload.authorization_list[2].payload.kind == authBasic + check d.rlp.setCode.payload.access_list.len == 1 + +suite "SSZ: Mixed transaction lists ": + test "SSZ: seq[Transaction] ": + let auths: seq[AuthTuple] = + @[ + ( + chain_id: ChainId(1.u256), + address: recipient, + nonce: 0'u64, + y_parity: 0'u8, + r: 1.u256, + s: 1.u256, + ) + ] + + let t0 = Transaction( + txType = 0x00'u8, + chain_id = ChainId(1.u256), + nonce = 1'u64, + gas = 21_000'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + signature = dummySig(), + ) + + let t1 = Transaction( + txType = 0x02'u8, + chain_id = ChainId(1.u256), + nonce = 2'u64, + gas = 44_000'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 5.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 1.u256), + signature = dummySig(), + ) + + let t2 = Transaction( + txType = 0x04'u8, # 7702 + chain_id = ChainId(1.u256), + nonce = 3'u64, + gas = 50_000'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 9.u256), + max_priority_fees_per_gas = BasicFeesPerGas(regular: 1.u256), + authorization_list = auths, + signature = dummySig(), + ) + + let txs = @[t0, t1, t2] + let enc = SSZ.encode(txs) + let dec = SSZ.decode(enc, type(txs)) + + check dec.len == 3 + check dec[2].rlp.setCode.payload.authorization_list.len == 1 + check enc == SSZ.encode(dec) + +suite "Block transactions root (SSZ sanity)": + test "transactions root for 3 txs: non-zero and stable": + var t0 = Transaction( + txType = 0x00'u8, + chain_id = ChainId(1.u256), + nonce = 1'u64, + gas = 21_000'u64, + to = Opt.some(recipient), + value = 0.u256, + input = abcdef, + max_fees_per_gas = BasicFeesPerGas(regular: 2.u256), + signature = dummySig(), + ) + var t1 = t0 + var t2 = t0 + var txn = @[t0, t1, t2] + let root1 = hash_tree_root(txn) diff --git a/tests/ssz/types.nim b/tests/ssz/types.nim new file mode 100644 index 00000000..05505f82 --- /dev/null +++ b/tests/ssz/types.nim @@ -0,0 +1,157 @@ +import + unittest2, + ssz_serialization, + ssz_serialization/merkleization, + ../../eth/common/[addresses, base, hashes], + ../../eth/ssz/[transaction_builder, signatures, adapter, sszcodec], + ../../eth/ssz/transaction_ssz as ssz_tx, + ../../eth/common/transactions as rlp_tx_mod + +suite "SSZ: Hash32 distinct Bytes32 roundtrip": + test "encode/decode parity": + var h: Hash32 + for i in 0 ..< 32: + distinctBase(h)[i] = byte(0xA0 + i) + let enc = SSZ.encode(h) + let dec = SSZ.decode(enc, Hash32) + check distinctBase(h) == distinctBase(dec) + +suite "SSZ: Hash32 merkleization": + test "seq[Hash32] root stable and order-sensitive": + var h1, h2: Hash32 + for i in 0 ..< 32: + distinctBase(h1)[i] = byte(i) + distinctBase(h2)[i] = byte(255 - i) + let r1 = hash_tree_root(@[h1, h2]) + let r2 = hash_tree_root(@[h1, h2]) + let r3 = hash_tree_root(@[h2, h1]) + check r1 == r2 + check r1 != r3 + + test "single vs pair has different root": + var a, b: Hash32 + for i in 0 ..< 32: + distinctBase(a)[i] = byte(i) + distinctBase(b)[i] = byte(i xor 0xFF) + let rs = hash_tree_root(@[a]) + let rp = hash_tree_root(@[a, b]) + check rs != rp + +suite "SSZ: Address encode/decode + merkleization": + test "Address encode/decode parity": + var a: Address + for i in 0 ..< 20: + a.data[i] = byte(i + 1) + let enc = SSZ.encode(a) + let dec = SSZ.decode(enc, Address) + check distinctBase(a) == distinctBase(dec) + + test "merkleization: seq[Address] root stable and order-sensitive": + var a1, a2: Address + for i in 0 ..< 20: + a1.data[i] = byte(i) + a2.data[i] = byte(19 - i) + let r1 = hash_tree_root(a1) + let r2 = hash_tree_root(a2) + let r4 = hash_tree_root(@[a1, a2]) + +suite "Authorization List Conversion": + test "Single replayable auth: RLP -> SSZ -> RLP": + let rlpAuth = rlp_tx_mod.Authorization( + chainId: ChainId(0.u256), + address: address"0x1111111111111111111111111111111111111111", + nonce: AccountNonce(5), + yParity: 0, + r: 123.u256, + s: 456.u256, + ) + let sszAuths = toSszAuthList(@[rlpAuth]) + check sszAuths.len == 1 + check sszAuths[0].payload.kind == authReplayableBasic + check sszAuths[0].payload.replayable.address == rlpAuth.address + check sszAuths[0].payload.replayable.nonce == uint64(rlpAuth.nonce) + + let backRlp = toRlpAuthList(sszAuths) + check backRlp.len == 1 + check backRlp[0].chainId == rlpAuth.chainId + check backRlp[0].address == rlpAuth.address + check backRlp[0].nonce == rlpAuth.nonce + check backRlp[0].yParity == rlpAuth.yParity + check backRlp[0].r == rlpAuth.r + check backRlp[0].s == rlpAuth.s + + test "Single basic auth: RLP -> SSZ -> RLP": + let rlpAuth = rlp_tx_mod.Authorization( + chainId: ChainId(1.u256), + address: address"0x2222222222222222222222222222222222222222", + nonce: AccountNonce(10), + yParity: 1, + r: 789.u256, + s: 101112.u256, + ) + let sszAuths = toSszAuthList(@[rlpAuth]) + check sszAuths.len == 1 + check sszAuths[0].payload.kind == authBasic + check sszAuths[0].payload.basic.chain_id == ChainId(1.u256) + + let backRlp = toRlpAuthList(sszAuths) + check backRlp[0] == rlpAuth + + test "Mixed auth list (2 replayable + 1 basic)": + let auths = + @[ + rlp_tx_mod.Authorization( + chainId: ChainId(0.u256), + address: address"0x1111111111111111111111111111111111111111", + nonce: AccountNonce(1), + yParity: 0, + r: 1.u256, + s: 2.u256, + ), + rlp_tx_mod.Authorization( + chainId: ChainId(0.u256), + address: address"0x2222222222222222222222222222222222222222", + nonce: AccountNonce(2), + yParity: 1, + r: 3.u256, + s: 4.u256, + ), + rlp_tx_mod.Authorization( + chainId: ChainId(5.u256), + address: address"0x3333333333333333333333333333333333333333", + nonce: AccountNonce(3), + yParity: 0, + r: 5.u256, + s: 6.u256, + ), + ] + let sszAuths = toSszAuthList(auths) + check sszAuths.len == 3 + check sszAuths[0].payload.kind == authReplayableBasic + check sszAuths[1].payload.kind == authReplayableBasic + check sszAuths[2].payload.kind == authBasic + + let backRlp = toRlpAuthList(sszAuths) + check backRlp.len == 3 + for i in 0 .. 2: + check backRlp[i] == auths[i] + + test "Authorization with max values": + let auth = rlp_tx_mod.Authorization( + chainId: ChainId(u256(high(uint64))), + address: address"0xffffffffffffffffffffffffffffffffffffffff", + nonce: AccountNonce(high(uint64)), + yParity: 1, + r: u256(high(uint64)), + s: u256(high(uint64)), + ) + let ssz = toSszSignedAuthList(@[auth]) + let back = toRlpAuthList(ssz) + check back[0] == auth + + test "Empty authorization list": + let empty: seq[rlp_tx_mod.Authorization] = @[] + let ssz = toSszSignedAuthList(empty) + check ssz.len == 0 + let back = toRlpAuthList(ssz) + check back.len == 0