From 3eb23793dba89fc53289a3c476b20366b0667033 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Wed, 24 Sep 2025 22:38:39 +0530 Subject: [PATCH 01/10] Add Pureth types and tests RT stanity --- eth.nimble | 7 +- eth/common/addresses.nim | 10 + eth/common/hashes.nim | 10 + eth/ssz/adapter.nim | 79 ++++ eth/ssz/receipts.nim | 57 +++ eth/ssz/signatures.nim | 101 +++++ eth/ssz/sszcodec.nim | 402 ++++++++++++++++++++ eth/ssz/transaction_builder.nim | 286 ++++++++++++++ eth/ssz/transaction_ssz.nim | 253 +++++++++++++ tests/all_tests.nim | 3 +- tests/common/test_receipts.nim | 2 +- tests/common/test_transactions.nim | 27 +- tests/ssz/all_tests.nim | 2 + tests/ssz/block.nim | 192 ++++++++++ tests/ssz/receipts.nim | 580 +++++++++++++++++++++++++++++ tests/ssz/signature.nim | 83 +++++ tests/ssz/transaction_builder.nim | 219 +++++++++++ tests/ssz/transaction_codec.nim | 77 ++++ tests/ssz/transaction_ssz.nim | 287 ++++++++++++++ 19 files changed, 2661 insertions(+), 16 deletions(-) create mode 100644 eth/ssz/adapter.nim create mode 100644 eth/ssz/receipts.nim create mode 100644 eth/ssz/signatures.nim create mode 100644 eth/ssz/sszcodec.nim create mode 100644 eth/ssz/transaction_builder.nim create mode 100644 eth/ssz/transaction_ssz.nim create mode 100644 tests/ssz/all_tests.nim create mode 100644 tests/ssz/block.nim create mode 100644 tests/ssz/receipts.nim create mode 100644 tests/ssz/signature.nim create mode 100644 tests/ssz/transaction_builder.nim create mode 100644 tests/ssz/transaction_codec.nim create mode 100644 tests/ssz/transaction_ssz.nim 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..277f117b 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 @@ -109,3 +111,11 @@ func hasValidChecksum*(_: type Address, a: string): bool = except ValueError: return false a == address.toChecksum0xHex() + +# template toSszType*(T: Address): auto = +# T.data() + +# func fromSszBytes*( T: type Address, 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/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/ssz/adapter.nim b/eth/ssz/adapter.nim new file mode 100644 index 00000000..b7fd42e5 --- /dev/null +++ b/eth/ssz/adapter.nim @@ -0,0 +1,79 @@ +{.push raises: [].} + +import + ../common/[addresses, hashes, base], + std/[typetraits], + ssz_serialization, + ssz_serialization/codec, + ssz_serialization/merkleization, + unittest2 + +# 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)) + + +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]) + # check r1 == r2 + # check r1 != r3 diff --git a/eth/ssz/receipts.nim b/eth/ssz/receipts.nim new file mode 100644 index 00000000..cc82eae2 --- /dev/null +++ b/eth/ssz/receipts.nim @@ -0,0 +1,57 @@ +import + ssz_serialization, + ./adapter, + ../common/[addresses, hashes] + +const MAX_TOPICS_PER_LOG* = 4 + +type + GasAmount* = uint64 + + Log* = object + address*: Address + topics*: List[Hash32, MAX_TOPICS_PER_LOG] + data*: seq[byte] + + BasicReceipt* = object + `from`*: Address + gas_used*: GasAmount + contract_address*: Address + logs*: seq[Log] + status*: bool + + CreateReceipt* = object + `from`*: Address + gas_used*: GasAmount + contract_address*: Address + logs*: seq[Log] + status*: bool + + SetCodeReceipt* = object + `from`*: Address + gas_used*: GasAmount + contract_address*: Address + logs*: seq[Log] + status*: bool + authorities*: seq[Address] + + #Run time ->ssz + collections + ReceiptKind* {.pure.} = 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) 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..17669fec --- /dev/null +++ b/eth/ssz/sszcodec.nim @@ -0,0 +1,402 @@ +import + std/[strutils, sequtils, options], + stint, + ssz_serialization/merkleization, + ./[signatures, receipts, transaction_builder], + ./transaction_ssz as ssz_tx, + ../common/[addresses_rlp, base_rlp], + ../common/transactions as rlp_tx_mod, + ../rlp/[length_writer, two_pass_writer, hash_writer] + +# 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]) + + # Normalize any legacy V into 0/1 +func vToParity(v: uint8): uint8 = + if v == 27'u8: 0'u8 + elif v == 28'u8: 1'u8 + else: v and 1'u8 + + +proc accessTupleFrom(pair: rlp_tx_mod.AccessPair): ssz_tx.AccessTuple = + # Old storageKeys: seq[Bytes32]; new seq[Hash32].( bruh ) + 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 ensureAuthMagic(m: TransactionType) = + if m != AuthMagic7702: + raise newException(ValueError, "authorization.magic must be 0x05") + + +proc toSszSignedAuthList*(al: seq[rlp_tx_mod.Authorization]): + seq[ssz_tx.SignedTx[ssz_tx.Authorization]] = + result = newSeq[ssz_tx.SignedTx[ssz_tx.Authorization]](al.len) + for i, a in al: + let payload = + if a.chainId == ChainId(0.u256): + ssz_tx.Authorization( + kind: authReplayableBasic, + replayable: ssz_tx.RlpReplayableBasicAuthorizationPayload( + magic: AuthMagic7702, + address: a.address, + nonce: uint64(a.nonce), + ) + ) + else: + ssz_tx.Authorization( + kind: authBasic, + basic: ssz_tx.RlpBasicAuthorizationPayload( + magic: AuthMagic7702, + chain_id: a.chainId, + address: a.address, + nonce: uint64(a.nonce), + ) + ) + + let sig = secp256k1Pack(a.R, a.S, vToParity(a.yParity)) + result[i] = ssz_tx.SignedTx[ssz_tx.Authorization](payload: payload, signature: sig) + +# sszcodec.nim +proc toRlpAuthList*(al: seq[ssz_tx.SignedTx[ssz_tx.Authorization]]): + seq[rlp_tx_mod.Authorization] = + result = newSeq[rlp_tx_mod.Authorization](al.len) + for i, sa in al: + let (R, S, parity) = secp256k1Unpack(sa.signature) + case sa.payload.kind + of authReplayableBasic: + let p = sa.payload.replayable + ensureAuthMagic(p.magic) + result[i] = rlp_tx_mod.Authorization( + chainId: ChainId(0.u256), + address: p.address, + nonce: AccountNonce(p.nonce), + yParity: parity, + r: R, + s: S, + ) + of authBasic: + let p = sa.payload.basic + ensureAuthMagic(p.magic) + 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) + + case tx.txType + of rlp_tx_mod.TxLegacy: + return 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, + ) + of rlp_tx_mod.TxEip2930: + return 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, + ) + of rlp_tx_mod.TxEip1559: + return 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, + ) + of rlp_tx_mod.TxEip4844: + return 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, + ) + of rlp_tx_mod.TxEip7702: + if tx.to.isNone: + raise newException(ValueError, "7702 setCode: requires 'to'") + return 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 = toAuthList() + #TODO + authorization_list = @[], + ) + +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), + # Similar TODO for typecast + 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), + # Similar TODO for typecast + 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: @[], + V: uint64(y), + R: R, + S: S, + ) diff --git a/eth/ssz/transaction_builder.nim b/eth/ssz/transaction_builder.nim new file mode 100644 index 00000000..49d14e1a --- /dev/null +++ b/eth/ssz/transaction_builder.nim @@ -0,0 +1,286 @@ +import stint, ./transaction_ssz, ./signatures, ../common/[addresses, base, hashes] + +type TxBuildError* = object of ValueError + +template fail(msg: string): untyped = + raise newException(TxBuildError, msg) + + +#This should be placed in nimbus-eth1 +# template validateCommonFields( +# payload: untyped, +# expectedTxType: static[uint8], +# contextName: static[string], +# txTypeErrorMsg: static[string], +# ) = +# if payload.txType != expectedTxType: +# fail(txTypeErrorMsg) +# when compiles(payload.chain_id): +# if payload.chain_id == ChainId(0.u256): +# fail(contextName & ": chain_id must be non-zero") + +# # Per-payload validations +# proc validate*(p: RlpLegacyBasicTransactionPayload) = +# validateCommonFields( +# p, 0x00'u8, "legacy basic", "legacy basic: txType must be 0x00 (TxLegacy)" +# ) + +# proc validate*(p: RlpLegacyCreateTransactionPayload) = +# validateCommonFields( +# p, 0x00'u8, "legacy create", "legacy create: txType must be 0x00 (TxLegacy)" +# ) +# if p.input.len == 0: +# fail("legacy create: initcode (input) must be non-empty") + +# proc validate*(p: RlpAccessListBasicTransactionPayload) = +# validateCommonFields( +# p, 0x01'u8, "2930 basic", "2930 basic: txType must be 0x01 (TxAccessList)" +# ) + +# proc validate*(p: RlpAccessListCreateTransactionPayload) = +# validateCommonFields( +# p, 0x01'u8, "2930 create", "2930 create: txType must be 0x01 (TxAccessList)" +# ) +# if p.input.len == 0: +# fail("2930 create: initcode (input) must be non-empty") + +# proc validate*(p: RlpBasicTransactionPayload) = +# validateCommonFields( +# p, 0x02'u8, "1559 basic", "1559 basic: txType must be 0x02 (TxDynamicFee)" +# ) + +# proc validate*(p: RlpCreateTransactionPayload) = +# validateCommonFields( +# p, 0x02'u8, "1559 create", "1559 create: txType must be 0x02 (TxDynamicFee)" +# ) +# if p.input.len == 0: +# fail("1559 create: initcode (input) must be non-empty") + +# proc validate*(p: RlpBlobTransactionPayload) = +# validateCommonFields( +# p, 0x03'u8, "4844 blob", "4844 blob: txType must be 0x03 (TxBlob)" +# ) +# if p.blob_versioned_hashes.len == 0: +# fail("4844 blob: blob_versioned_hashes must be non-empty") + +# proc validate*(p: RlpSetCodeTransactionPayload) = +# validateCommonFields(p, 0x04'u8, "7702", "7702: txType must be 0x04 (SetCode)") +# if p.authorization_list.len == 0: +# fail("7702: authorization_list must be non-empty") + +# proc validate*(p: RlpLegacyReplayableBasicTransactionPayload) = +# validateCommonFields( +# p, 0x00'u8, +# "legacy replayable basic", +# "legacy replayable basic: txType must be 0x00 (TxLegacy)" +# ) + +# proc validate*(p: RlpLegacyReplayableCreateTransactionPayload) = +# validateCommonFields( +# p, 0x00'u8, +# "legacy replayable create", +# "legacy replayable create: txType must be 0x00 (TxLegacy)" +# ) +# if p.input.len == 0: +# fail("legacy create: initcode (input) must be non-empty") + +# proc validate*(sig: Secp256k1ExecutionSignature) = +# if not secp256k1Validate(sig): +# fail("invalid secp256k1 signature") + +# BuildWrap: generates build(payload, signature) -> Transaction using the payload-specific validate* +template BuildWrap( + PayloadT, WrapperT: typedesc, tag: static[RLPTransactionKind], fieldSym: untyped +) = + proc build*( + payload: PayloadT, signature: Secp256k1ExecutionSignature + ): Transaction {.inline.} = + # validate(payload) + # validate signature + # validate(signature) + 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], # some(addr) => call, none => create + 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[transaction_ssz.Authorization] = @[], +): Transaction = + 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: + # if to.isNone: + # fail("4844 blob: create-style not supported") + when compiles(BlobFeesPerGas): + 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) + else: + fail("4844 blob: BlobFeesPerGas type not available in this build") + of TxSetCode: + if to.isNone: + fail("7702 setCode: requires 'to'") + if authorization_list.len == 0: + fail("7702 setCode: authorization_list must be non-empty") + # Minimal validation: ensure auth magic is set correctly. + for i, a in authorization_list: + case a.kind + of transaction_ssz.AuthorizationKind.authReplayableBasic: + if a.replayable.magic != transaction_ssz.AuthMagic7702: + fail("7702 setCode: auth[" & $i & "] replayable.magic must be 0x05") + of transaction_ssz.AuthorizationKind.authBasic: + if a.basic.magic != transaction_ssz.AuthMagic7702: + fail("7702 setCode: auth[" & $i & "] basic.magic must be 0x05") + 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: authorization_list, + ) + 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..269a808e --- /dev/null +++ b/eth/ssz/transaction_ssz.nim @@ -0,0 +1,253 @@ +import ssz_serialization +import stint +import ../common/[addresses, base, hashes] +import ./signatures +import ./adapter +import serialization/case_objects + +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 # ExecutionAddress + nonce*: uint64 + + RlpBasicAuthorizationPayload* {. + sszActiveFields: [1, 1, 1, 1] + .} = object + magic*: TransactionType # 0x05 (Auth) + chain_id*: ChainId + address*: Address # ExecutionAddress + nonce*: uint64 + + AuthorizationKind* = enum + authReplayableBasic + authBasic + + Authorization* = object + case kind*: AuthorizationKind + of authReplayableBasic: + replayable*: RlpReplayableBasicAuthorizationPayload + of authBasic: + basic*: RlpBasicAuthorizationPayload + +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 diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 7e8967e2..4bbea951 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -18,4 +18,5 @@ import ./trie/all_tests, ./db/all_tests, ./common/all_tests, - ./test_bloom + ./test_bloom, + ./ssz/all_tests diff --git a/tests/common/test_receipts.nim b/tests/common/test_receipts.nim index 409ae231..e8416cf0 100644 --- a/tests/common/test_receipts.nim +++ b/tests/common/test_receipts.nim @@ -54,5 +54,5 @@ suite "Stored Receipt": isHash: false, status: false, cumulativeGasUsed: 100.GasInt) - + roundTrip(rec) diff --git a/tests/common/test_transactions.nim b/tests/common/test_transactions.nim index ff5be9c6..23b82458 100644 --- a/tests/common/test_transactions.nim +++ b/tests/common/test_transactions.nim @@ -25,12 +25,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 )] -proc tx0(i: int): Transaction = +proc tx0*(i: int): Transaction = Transaction( txType: TxLegacy, nonce: i.AccountNonce, @@ -39,7 +39,7 @@ proc tx0(i: int): Transaction = gasPrice: 2.GasInt, payload: abcdef) -proc tx1(i: int): Transaction = +proc tx1*(i: int): Transaction = Transaction( # Legacy tx contract creation. txType: TxLegacy, @@ -48,7 +48,7 @@ proc tx1(i: int): Transaction = gasPrice: 2.GasInt, payload: abcdef) -proc tx2(i: int): Transaction = +proc tx2*(i: int): Transaction = Transaction( # Tx with non-zero access list. txType: TxEip2930, @@ -60,7 +60,7 @@ proc tx2(i: int): Transaction = accessList: accesses, payload: abcdef) -proc tx3(i: int): Transaction = +proc tx3*(i: int): Transaction = Transaction( # Tx with empty access list. txType: TxEip2930, @@ -71,7 +71,7 @@ proc tx3(i: int): Transaction = gasPrice: 10.GasInt, payload: abcdef) -proc tx4(i: int): Transaction = +proc tx4*(i: int): Transaction = Transaction( # Contract creation with access list. txType: TxEip2930, @@ -81,7 +81,7 @@ proc tx4(i: int): Transaction = gasPrice: 10.GasInt, accessList: accesses) -proc tx5(i: int): Transaction = +proc tx5*(i: int): Transaction = Transaction( txType: TxEip1559, chainId: chainId(1), @@ -91,7 +91,7 @@ proc tx5(i: int): Transaction = maxFeePerGas: 10.GasInt, accessList: accesses) -proc tx6(i: int): Transaction = +proc tx6*(i: int): Transaction = const digest = hash32"010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014" @@ -105,7 +105,7 @@ proc tx6(i: int): Transaction = accessList: accesses, versionedHashes: @[digest]) -proc tx7(i: int): Transaction = +proc tx7*(i: int): Transaction = const digest = hash32"01624652859a6e98ffc1608e2af0147ca4e86e1ce27672d8d3f3c9d4ffd6ef7e" @@ -120,7 +120,7 @@ proc tx7(i: int): Transaction = versionedHashes: @[digest], maxFeePerBlobGas: 10000000.u256) -proc tx8(i: int): Transaction = +proc tx8*(i: int): Transaction = const digest = hash32"01624652859a6e98ffc1608e2af0147ca4e86e1ce27672d8d3f3c9d4ffd6ef7e" @@ -133,10 +133,11 @@ proc tx8(i: int): Transaction = maxPriorityFeePerGas:42.GasInt, maxFeePerGas: 10.GasInt, accessList: accesses, + versionedHashes: @[digest], maxFeePerBlobGas: 10000000.u256) -proc 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..c0417a04 --- /dev/null +++ b/tests/ssz/all_tests.nim @@ -0,0 +1,2 @@ +import + ./receipts, ./transaction_builder, ./transaction_ssz, ./signature, ./transaction_codec diff --git a/tests/ssz/block.nim b/tests/ssz/block.nim new file mode 100644 index 00000000..94325eac --- /dev/null +++ b/tests/ssz/block.nim @@ -0,0 +1,192 @@ +import + std/[os, strutils, json], + stew/[byteutils, io2], + ssz_serialization, + ../../eth/[common, rlp], + ../../eth/common/transactions as rlp_tx_mod, + ../../eth/ssz/[sszcodec,adapter], + ../../eth/ssz/transaction_ssz as ssz_tx, + unittest2 + +proc eip2718Dir*(): string = + (currentSourcePath.parentDir / ".." / "common" / "eip2718").normalizedPath + +proc rlpsDir*(): string = + (currentSourcePath.parentDir / ".." / "common" / "rlps").normalizedPath + +proc listEip2718Files*(): seq[string] = + let dir = eip2718Dir() + if not dir.dirExists: return @[] + for kind, path in walkDir(dir): + if kind == pcFile and path.endsWith(".json"): + result.add path + +proc eip2718FilePath*(index: int): string = + eip2718Dir() / ("acl_block_" & $index & ".json") + +proc listSelectedEip2718Files*(indices: openArray[int]): seq[string] = + for i in indices: + let p = eip2718FilePath(i) + if p.fileExists: + result.add p + +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 + +# Loaders from a specific file +proc loadEip2718BlockFromFile*(path: string): EthBlock = + let n = json.parseFile(path) + if not n.hasKey("rlp"): + raise newException(ValueError, "JSON has no 'rlp' key: " & path) + let hexRlp = n["rlp"].getStr() + let bytes = hexToSeqByte(hexRlp) + rlp.decode(bytes, EthBlock) + +# Extract eth/common transactions from a JSON fixture +proc loadEip2718TransactionsFromFile*(path: string): seq[rlp_tx_mod.Transaction] = + let blk = loadEip2718BlockFromFile(path) + blk.transactions + +# Extract transactions and their RLP bytes from a JSON fixture +proc loadEip2718TransactionsWithRlp*(path: string): seq[tuple[tx: rlp_tx_mod.Transaction, rlp: seq[byte]]] = + for tx in loadEip2718TransactionsFromFile(path): + result.add (tx: tx, rlp: rlp.encode(tx)) + +# Convert eth/common transactions to SSZ transactions +proc toSszTransactions*(txs: seq[rlp_tx_mod.Transaction]): seq[ssz_tx.Transaction] = + for tx in txs: + result.add toSszTx(tx) + +# From a JSON fixture file, produce SSZ txs and their SSZ encodings +proc loadEip2718SszTransactionsWithSsz*(path: string): seq[tuple[tx: ssz_tx.Transaction, ssz: seq[byte]]] = + let rlpTxs = loadEip2718TransactionsFromFile(path) + for stx in toSszTransactions(rlpTxs): + result.add (tx: stx, ssz: SSZ.encode(stx)) + +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 + +# Load all EIP-2718 JSON fixtures (acl_block_*.json) and decode their RLP into EthBlock objects +proc loadEip2718Blocks*(): seq[EthBlock] = + let dir = eip2718Dir() + if not dir.dirExists: return @[] + for kind, path in walkDir(dir): + if kind == pcFile and path.endsWith(".json"): + try: + result.add loadEip2718BlockFromFile(path) + except CatchableError: + discard + +# Load blocks from binary .rlp fixtures; supports multi-block streams +# limitPerFile controls how many blocks to extract from each file (default 1 for speed) +proc loadRlpBlocks*(limitPerFile: int = 1): seq[EthBlock] = + let dir = rlpsDir() + if not dir.dirExists: return @[] + for kind, path in walkDir(dir): + if kind == pcFile and path.endsWith(".rlp"): + try: + result.add loadRlpBlocksFromFile(path, limitPerFile) + except CatchableError: + discard + +# --- Simple CLI printing helpers --- +proc summarize*(b: EthBlock): string = + let txCount = b.transactions.len + let uncles = b.uncles.len + let w = (if b.withdrawals.isSome: $b.withdrawals.get.len else: "none") + # compute a quick content hash for reference + let h = rlp.computeRlpHash(b) + "txs=" & $txCount & ", uncles=" & $uncles & ", withdrawals=" & w & + ", rlpHash=0x" & h.data.toHex + +proc printPicked*() = + echo "== EIP-2718 JSON fixtures ==" + let jsonFiles = listEip2718Files() + if jsonFiles.len == 0: + echo "(none)" + else: + for f in jsonFiles: + try: + let blk = loadEip2718BlockFromFile(f) + echo f, " -> ", summarize(blk) + # Also print transactions and their RLP bytes (hex) + let txsWithRlp = loadEip2718TransactionsWithRlp(f) + echo " txs: ", txsWithRlp.len + var idx = 0 + for item in txsWithRlp: + let hex = item.rlp.toHex() + echo " [", idx, "] type=", $item.tx.txType, ", nonce=", $item.tx.nonce + echo " rlp=0x", hex + inc idx + except CatchableError as e: + echo f, " -> ERROR: ", e.msg + + # echo "\n== RLP fixtures ==" + # let rlpFiles = listRlpFiles() + # if rlpFiles.len == 0: + # echo "(none)" + # else: + # for f in rlpFiles: + # try: + # let blks = loadRlpBlocksFromFile(f, 1) # first block per file + # if blks.len == 0: + # echo f, " -> (no blocks)" + # else: + # echo f, " -> ", summarize(blks[0]) + # except CatchableError as e: + # echo f, " -> ERROR: ", e.msg + +when isMainModule: + # Print only EIP-2718 blocks 9 and 8, with their transactions and full RLP + echo "== EIP-2718 JSON fixtures (selected: 9, 8) ==" + let selected = listSelectedEip2718Files([9,]) + if selected.len == 0: + echo "(none)" + else: + for f in selected: + try: + let blk = loadEip2718BlockFromFile(f) + echo f, " -> ", summarize(blk) + let txsWithRlp = loadEip2718TransactionsWithRlp(f) + echo " txs: ", txsWithRlp.len + var idx = 0 + for item in txsWithRlp: + let hex = item.rlp.toHex() + echo " [", idx, "] type=", $item.tx.txType, ", nonce=", $item.tx.nonce + echo " rlp=0x", hex + inc idx + # Also show SSZ-converted transactions and their SSZ bytes + let sszTxs = loadEip2718SszTransactionsWithSsz(f) + var sidx = 0 + for it in sszTxs: + let sszHex = it.ssz.toHex() + echo " (ssz)[", sidx, "] kind=", $it.tx.kind + echo " ssz=0x", sszHex + inc sidx + except CatchableError as e: + echo f, " -> ERROR: ", e.msg + +# Unit tests: RLP -> SSZ -> SSZ bytes are stable +suite "EIP-2718 tx RLP->SSZ->SSZ round-trip": + for idx in [9, 8]: + test "block " & $idx & ": tx SSZ bytes stable after decode": + let path = eip2718FilePath(idx) + let sszTuples = loadEip2718SszTransactionsWithSsz(path) + check sszTuples.len > 0 + for it in sszTuples: + let dec = SSZ.decode(it.ssz, ssz_tx.Transaction) + let enc2 = SSZ.encode(dec) + # check enc2 == it.ssz + + diff --git a/tests/ssz/receipts.nim b/tests/ssz/receipts.nim new file mode 100644 index 00000000..3471bc66 --- /dev/null +++ b/tests/ssz/receipts.nim @@ -0,0 +1,580 @@ +import + unittest2, + ssz_serialization/merkleization, + ssz_serialization, + macros, + std/sequtils, + ../../eth/common/[addresses, base, hashes], + ../../eth/ssz/[receipts, adapter] + +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 topicFill(b: byte): untyped = + ( + block: + var buf: array[32, byte] + for i in 0 ..< 32: + buf[i] = b + Hash32.copyFrom(buf) + ) + +# Idea- pass only l values to this + +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: List[Hash32, MAX_TOPICS_PER_LOG](@[]), + # data: @[], + # ) + + # testRT "Log: max topics", + # ( + # block: + # let addrAA = Address.copyFrom(newSeqWith(20, byte 0xAA)) + # Log( + # address: addrAA, + # topics: List[Hash32, MAX_TOPICS_PER_LOG]( + # @[topicFill(0x10), topicFill(0x11), topicFill(0x12), topicFill(0x13)] + # ), + # data: @[byte 0xDE, 0xAD, 0xBE, 0xEF], + # ) + # ): + # check v.topics.len == 4 + # 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: List[Hash32, MAX_TOPICS_PER_LOG]( + # @[ + # Hash32.copyFrom(a0), + # Hash32.copyFrom(a1), + # Hash32.copyFrom(a2), + # Hash32.copyFrom(a3), + # ] + # ), + # data: @[byte 0xDE, 0xAD, 0xBE, 0xEF], + # ) + # ): + # check v.topics.len == 4 + + # 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: List[Hash32, MAX_TOPICS_PER_LOG]( + # @[Hash32.copyFrom(t1), Hash32.copyFrom(t2)] + # ), + # data: @[byte 1, 2, 3, 4], + # ) + # ): + # let d = SSZ.decode(SSZ.encode(v), Log) + # check d.address == v.address + # check d.topics.len == 2 + # 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: List[Hash32, MAX_TOPICS_PER_LOG]( + # @[Hash32.copyFrom(t1), Hash32.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, +# contract_address: addresses.zeroAddress, +# logs: @[], +# status: true, +# ) + + # testRT "Basic receipt data", + # ( + # block: + # let log0 = Log( + # address: default(Address), + # topics: default(List[Hash32, MAX_TOPICS_PER_LOG]), + # data: @[], + # ) + # BasicReceipt( + # `from`: default(Address), + # gas_used: 100'u64, + # contract_address: default(Address), + # logs: @[log0], + # status: true, + # ) + # ): + # check v.gas_used == 100'u64 + # check v.status == true + # check v.contract_address == default(Address) + # check v.logs.len == 1 + + # testRT "BasicReceipt: reserved contract_address is zero", + # ( + # block: + # let fromAA = Address.copyFrom(newSeqWith(20, byte 0xAA)) + # BasicReceipt( + # `from`: fromAA, + # gas_used: 1'u64, + # contract_address: addresses.zeroAddress, + # logs: @[], + # status: true, + # ) + # ) + # let dec = decodeReceipt[BasicReceipt](encodeReceipt(v)) + # check dec.contract_address == addresses.zeroAddress + + # 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: default(List[Hash32, MAX_TOPICS_PER_LOG]), + # 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: default(List[Hash32, MAX_TOPICS_PER_LOG]), + # data: @[], + # ) + # SetCodeReceipt( + # `from`: address"0x00000000000000000000000000000000000000dd", + # gas_used: 42000'u64, + # contract_address: address"0x00000000000000000000000000000000000000ee", + # 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 + +# # #TODO -> rlp to receipts ssz + +# suite "SSZ Debug Tests": +# test "Test individual receipt components": +# echo "=== Testing individual SSZ components ===" + +# echo "Testing Address SSZ..." +# try: +# let address = addresses.zeroAddress +# let addrHash = hash_tree_root(address) +# # echo "Address hash_tree_root successful: ", addrHash.to0xHex() +# except Exception as e: +# echo "ERROR with Address SSZ: ", e.msg +# echo "Exception type: ", $e.name + + # echo "Testing Log SSZ..." + # try: + # let log = Log( + # address: addresses.zeroAddress, + # topics: List[Hash32, MAX_TOPICS_PER_LOG](@[]), + # data: @[], + # ) + # let logHash = hash_tree_root(log) + # # echo "Log hash_tree_root successful: ", logHash.to0xHex() + # except Exception as e: + # echo "ERROR with Log SSZ: ", e.msg + # echo "Exception type: ", $e.name + + # echo "Testing BasicReceipt SSZ..." + # try: + # let basicReceipt = BasicReceipt( + # `from`: addresses.zeroAddress, + # gas_used: 21_000'u64, + # contract_address: addresses.zeroAddress, + # logs: @[], + # status: true, + # ) + # let basicHash = hash_tree_root(basicReceipt) + # echo "BasicReceipt hash_tree_root successful: ", basicHash.to(Hash32).to0xHex() + # except Exception as e: + # echo "ERROR with BasicReceipt SSZ: ", e.msg + # echo "Exception type: ", $e.name + + # echo "Testing Receipt variant SSZ..." + # try: + # let receipt = toReceipt(BasicReceipt( + # `from`: addresses.zeroAddress, + # gas_used: 21_000'u64, + # contract_address: addresses.zeroAddress, + # logs: @[], + # status: true, + # )) + # # echo "Receipt created with kind: ", receipt.kind + # # let receiptHash = hash_tree_root(receipt) + # # echo "Receipt hash_tree_root successful: ", receiptHash.to0xHex() + # except Exception as e: + # echo "ERROR with Receipt variant SSZ: ", e.msg + # echo "Exception type: ", $e.name +# suite "Block receipts root (SSZ)": +# test "receipts root for 3 receipts: non-zero and stable": +# echo "Creating BasicReceipt..." +# let r0 = toReceipt( +# BasicReceipt( +# `from`: addresses.zeroAddress, +# gas_used: 21_000'u64, +# contract_address: addresses.zeroAddress, +# logs: @[], +# status: true, +# ) +# ) +# # echo "BasicReceipt created: ", r0.kind + + +# # echo "Creating CreateReceipt..." +# # let r1 = toReceipt( +# # CreateReceipt( +# # `from`: address"0x0000000000000000000000000000000000000001", +# # gas_used: 42_000'u64, +# # contract_address: address"0x00000000000000000000000000000000000000aa", +# # logs: @[], +# # status: false, +# # ) +# # ) +# # echo "CreateReceipt created: ", r1.kind + +# echo "Creating SetCodeReceipt..." +# let r2 = toReceipt( +# SetCodeReceipt( +# `from`: address"0x00000000000000000000000000000000000000bb", +# gas_used: 63_000'u64, +# contract_address: address"0x00000000000000000000000000000000000000cc", +# logs: @[], +# status: true, +# authorities: @[address"0x00000000000000000000000000000000000000f1"], +# ) +# ) +# # echo "SetCodeReceipt created: ", r2.kind + +# var receipts: seq[Receipt] = @[r0, r2] +# # echo "Created receipts sequence with ", receipts.len, " items" + +# echo "Attempting to compute hash_tree_root..." +# let root1 = hash_tree_root(receipts) + # check root1 != hashes.zeroHash32 + # echo "receipts_root: ", root1.to(Hash32).to0xHex() + + # let root2 = hash_tree_root(receipts2) + # check root2 == root1 + +test "receipts root changes when a receipt changes": + var receipts = @[BasicReceipt(`from`: default(Address), gas_used: 1'u64, contract_address: default(Address), logs: @[], status: true), + BasicReceipt(`from`: default(Address), gas_used: 2'u64, contract_address: default(Address), logs: @[], status: true) + ] + let rootA = hash_tree_root(receipts) + # mutate gas_used in first receipt + 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, contract_address: default(Address), logs: @[], status: true) + let b = BasicReceipt(`from`: default(Address), gas_used: 2'u64, contract_address: default(Address), 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 + echo "receipts_root(list1): ", r1.to(Hash32).to0xHex() + echo "receipts_root(list2): ", r2.to(Hash32).to0xHex() + +suite "SSZ root ": + test "hash_tree_root for Log": + let log = Log( + address: addresses.zeroAddress, + topics: List[Hash32, MAX_TOPICS_PER_LOG](@[]), + data: @[] + ) + let root = hash_tree_root(log) + echo "Log root: ", root.to(Hash32).to0xHex() + + + test "hash_tree_root for receipts list (variant)": + let r0 = toReceipt(BasicReceipt( + `from`: addresses.zeroAddress, + gas_used: 21_000'u64, + contract_address: addresses.zeroAddress, + logs: @[], + status: true + )) + let r1 = toReceipt(BasicReceipt( + `from`: address"0x0000000000000000000000000000000000000001", + gas_used: 42_000'u64, + contract_address: address"0x00000000000000000000000000000000000000aa", + logs: @[], + status: false + )) + let r2 = toReceipt(BasicReceipt( + `from`: address"0x00000000000000000000000000000000000000bb", + gas_used: 63_000'u64, + contract_address: address"0x00000000000000000000000000000000000000cc", + logs: @[], + status: true + )) + + hash_tree_root(r1) + let receipts: seq[Receipt] = @[r0, r1, r2] + let root = hash_tree_root(receipts) + echo "Tagged receipts list root: ", root.to(Hash32).to0xHex() + + test "hash_tree_root for list of Log": + let log = Log( + address: addresses.zeroAddress, + topics: List[Hash32, MAX_TOPICS_PER_LOG](@[]), + data: @[] + ) + let logs = @[log] + let root = hash_tree_root(logs) + # echo "Logs list root: ", root.to0xHex() + echo "Logs list root: ", root.to(Hash32).to0xHex() + + test "hash_tree_root for BasicReceipt": + let r = BasicReceipt( + `from`: addresses.zeroAddress, + gas_used: 100'u64, + contract_address: addresses.zeroAddress, + logs: @[], + status: true + ) + let root = hash_tree_root(r) +# echo "BasicReceipt root: ", root.to0xHex() + echo "BasicReceipt root: ", root.to(Hash32).to0xHex() + + 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) + echo "CreateReceipt root: ", root.to(Hash32).to0xHex() + + # test "hash_tree_root for SetCodeReceipt": + # let r = SetCodeReceipt( + # `from`: address"0x00000000000000000000000000000000000000bb", + # gas_used: 63_000'u64, + # contract_address: address"0x00000000000000000000000000000000000000cc", + # logs: @[], + # status: true, + # authorities: @[] + # # authorities: @[address"0x00000000000000000000000000000000000000f1"] + # ) + # let root = hash_tree_root(r) + # echo "SetCodeReceipt root: ", root.to(Hash32).to0xHex() + + test "hash_tree_root for treceipts list (variant)": + # Build concrete receipts and convert to the Receipt variant using toReceipt + let r0 = BasicReceipt( + `from`: addresses.zeroAddress, + gas_used: 21_000'u64, + contract_address: addresses.zeroAddress, + logs: @[], + status: true + ) + let r1 = BasicReceipt( + `from`: addresses.zeroAddress, + gas_used: 21_000'u64, + contract_address: addresses.zeroAddress, + logs: @[], + status: true + ) + let r2 = BasicReceipt( + `from`: addresses.zeroAddress, + gas_used: 21_000'u64, + contract_address: addresses.zeroAddress, + 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, + contract_address: address"0x00000000000000000000000000000000000000cc", + logs: @[], + status: true, + authorities: @[address"0x00000000000000000000000000000000000000f1"] + )) +# TODO make so we can tkae an arbitary amount of receipts with different kind + var receipts = @[r0, r1, r2] + let root = hash_tree_root(receipts) + echo "Tagged receipts list root: ", root.to(Hash32).to0xHex() + +# Focused tests to demonstrate current toReceipt SSZ behavior +# suite "Receipt variant SSZ behavior": +# proc sampleBasic(): BasicReceipt = +# BasicReceipt( +# `from`: addresses.zeroAddress, +# gas_used: 21_000'u64, +# contract_address: addresses.zeroAddress, +# logs: @[], +# status: true +# ) + +# proc sampleVariant(): Receipt = +# toReceipt(sampleBasic()) + +# test "SSZ.encode on BasicReceipt succeeds": +# let r = sampleBasic() +# let bytes = SSZ.encode(r) +# let r2 = SSZ.decode(bytes, BasicReceipt) +# check r == r2 + + # test "SSZ.encode on Receipt (toReceipt) currently unsupported": + # let rv = sampleVariant() + # when compiles(SSZ.encode(rv)): + # let bytes = SSZ.encode(rv) + # let rv2 = SSZ.decode(bytes, Receipt) + # check bytes.len > 0 + # check rv2.kind == rv.kind + # else: + # check true + + # test "hash_tree_root(Receipt) compile check": + # let rv = sampleVariant() + # when compiles(hash_tree_root(rv)): + # let root = hash_tree_root(rv) + # echo "Receipt variant root: ", root.to(Hash32).to0xHex() + # else: + # check true + + # test "SSZ.encode on seq[Receipt] compile check": + # let seqv = @[sampleVariant(), sampleVariant()] + # when compiles(SSZ.encode(seqv)): + # let bytes = SSZ.encode(seqv) + # let seq2 = SSZ.decode(bytes, type(seqv)) + # check seq2.len == seqv.len + # else: + # check true 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..8e19d789 --- /dev/null +++ b/tests/ssz/transaction_builder.nim @@ -0,0 +1,219 @@ +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 + + when compiles(BlobFeesPerGas): + 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 = @[ + Authorization( + kind: authReplayableBasic, + replayable: RlpReplayableBasicAuthorizationPayload( + magic: AuthMagic7702, + address: recipient, + nonce: 0'u64, + ) + ) + ] + 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].kind == authReplayableBasic + + test "7702 SetCode with basic auth": + let auths = @[ + Authorization( + kind: authBasic, + basic: RlpBasicAuthorizationPayload( + magic: AuthMagic7702, + chain_id: ChainId(1.u256), + address: recipient, + nonce: 0'u64, + ) + ) + ] + 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].kind == authBasic + + test "7702 SetCode: fails when auth list empty": + expect(TxBuildError): + discard Transaction( + txType = 0x04'u8, + chain_id = ChainId(1.u256), + nonce = 11'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), + authorization_list = @[], + signature = dummySig(), + ) diff --git a/tests/ssz/transaction_codec.nim b/tests/ssz/transaction_codec.nim new file mode 100644 index 00000000..95260491 --- /dev/null +++ b/tests/ssz/transaction_codec.nim @@ -0,0 +1,77 @@ +import + unittest, + ../../eth/ssz/sszcodec, + ../../eth/common/[addresses, hashes, base, eth_types_json_serialization], + ../../eth/rlp, + ssz_serialization, + ../common/test_transactions + +# const recipient = address"095e7baea6a6c7c4c2dfeb977efac326af552d87" +# const source = address"0x0000000000000000000000000000000000000001" +# let abcdef = hexToSeqByte("abcdef") +# let storageKey = default(Bytes32) +# let accesses = @[rlp_tx.AccessPair(address: source, storageKeys: @[storageKey])] + +# proc someTo(a: Address): Opt[Address] = Opt.some(a) +# proc noneTo(): Opt[Address] = Opt.none(Address) +# let R1 = 1.u256 +# let S1 = 1.u256 + +template sszRoundTrip(txFunc: untyped, i: int) = + 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) + check oldBack == oldTx + check sszBack == sszTx + +suite "Transactions SSZ Roundtrip": + 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) + + # test "NetworkBlob Tx": + # sszRoundTrip(tx6, 7) + + # test "Minimal Blob Tx": + # sszRoundTrip(tx7, 8) + + # test "Minimal Blob Tx contract creation": + # sszRoundTrip(tx8, 9) + + # test "EIP-7702 (currently fail-path only)": + # expect(ValueError): + # discard toSszTx(txEip7702(10)) + +# suite "sszcodec: passing SSZ round-trips (old -> new -> SSZ -> old)": +# sszRoundTripOK("Legacy CALL pre-155", legacyCallPre155(1)) +# sszRoundTripOK("Legacy CALL EIP-155", legacyCall155(2)) +# sszRoundTripOK("Legacy CREATE pre-155", legacyCreatePre155(3, true)) +# sszRoundTripOK("Legacy CREATE EIP-155", legacyCreate155(4, true)) +# sszRoundTripOK("EIP-2930 CALL (with AL)", eip2930Call(5, true)) +# sszRoundTripOK("EIP-2930 CALL (empty AL)", eip2930Call(6, false)) +# sszRoundTripOK("EIP-2930 CREATE", eip2930Create(7, true)) +# sszRoundTripOK("EIP-1559 CALL", eip1559Call(8)) +# sszRoundTripOK("EIP-1559 CREATE", eip1559Create(9, true)) +# sszRoundTripOK("EIP-4844 CALL (with blob fee)", eip4844Call(10, true)) +# sszRoundTripOK("EIP-4844 CALL (blob fee zero)", eip4844Call(11, false)) +# sszRoundTripOK("EIP-7702 setCode (empty auths)", eip7702SetCode(12, false)) diff --git a/tests/ssz/transaction_ssz.nim b/tests/ssz/transaction_ssz.nim new file mode 100644 index 00000000..5f2ec278 --- /dev/null +++ b/tests/ssz/transaction_ssz.nim @@ -0,0 +1,287 @@ +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 + + # txRT "SSZ: SetCode (auth list)", + # Transaction( + # txType = 0x04'u8, + # chain_id = ChainId(1.u256), + # nonce = 8'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), + # authorization_list = + # @[ + # Authorization( + # kind: authReplayableBasic, + # replayable: RlpReplayableBasicAuthorizationPayload( + # magic: AuthMagic7702, + # address: source, + # nonce: 0'u64, + # ), + # ) + # ], + # signature = dummySig(), + # ) + # check d.rlp.setCode.payload.txType == 0x04'u8 + # check d.rlp.setCode.payload.authorization_list.len == 1 + +# suite "SSZ seq[Transaction] round-trip": +# test "SSZ: seq[Transaction] roundtrip (mixed types)": +# 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 = 0x01'u8, +# chain_id = ChainId(1.u256), +# nonce = 2'u64, +# gas = 44_000'u64, +# to = Opt.none(Address), +# value = 0.u256, +# input = abcdef, +# max_fees_per_gas = BasicFeesPerGas(regular: 5.u256), +# signature = dummySig(), +# ) +# let t2 = Transaction( +# txType = 0x02'u8, +# 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), +# access_list = accesses, +# signature = dummySig(), +# ) +# let txs = @[t0, t1, t2] +# let enc = SSZ.encode(txs) +# echo "SSZ bytes (seq[Transaction]) [", enc.len, "]: 0x", enc.toHex() +# let dec = SSZ.decode(enc, type(txs)) +# 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) + +# Failing +# suite "SSZ: Authorization list": +# test "SSZ: Authorization list size parity": +# let al = @[ +# Authorization( +# kind: authReplayableBasic, +# replayable: RlpReplayableBasicAuthorizationPayload( +# magic: AuthMagic7702, +# address: source, +# nonce: 0'u64) +# ) +# ] +# check sszSize(al) == SSZ.encode(al).len From 2024322f2e0f02d95d3fa0033e97e57199e70809 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Fri, 10 Oct 2025 03:18:32 +0530 Subject: [PATCH 02/10] RT Blocks --- eth/common/addresses.nim | 8 - eth/ssz/adapter.nim | 57 +--- eth/ssz/blocks_ssz.nim | 75 +++++ eth/ssz/blocks_ssz_adapter.nim | 229 +++++++++++++++ eth/ssz/sszcodec.nim | 130 +++++---- eth/ssz/transaction_builder.nim | 144 +++------- eth/ssz/transaction_ssz.nim | 32 ++- tests/ssz/all_tests.nim | 7 +- tests/ssz/block.nim | 192 ------------- tests/ssz/block_rt.nim | 325 +++++++++++++++++++++ tests/ssz/transaction_builder.nim | 37 ++- tests/ssz/transaction_codec.nim | 283 +++++++++++++++--- tests/ssz/transaction_ssz.nim | 459 ++++++++++++++++++------------ tests/ssz/types.nim | 149 ++++++++++ 14 files changed, 1460 insertions(+), 667 deletions(-) create mode 100644 eth/ssz/blocks_ssz.nim create mode 100644 eth/ssz/blocks_ssz_adapter.nim delete mode 100644 tests/ssz/block.nim create mode 100644 tests/ssz/block_rt.nim create mode 100644 tests/ssz/types.nim diff --git a/eth/common/addresses.nim b/eth/common/addresses.nim index 277f117b..5c87624c 100644 --- a/eth/common/addresses.nim +++ b/eth/common/addresses.nim @@ -111,11 +111,3 @@ func hasValidChecksum*(_: type Address, a: string): bool = except ValueError: return false a == address.toChecksum0xHex() - -# template toSszType*(T: Address): auto = -# T.data() - -# func fromSszBytes*( T: type Address, 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/ssz/adapter.nim b/eth/ssz/adapter.nim index b7fd42e5..ffba1c68 100644 --- a/eth/ssz/adapter.nim +++ b/eth/ssz/adapter.nim @@ -5,8 +5,7 @@ import std/[typetraits], ssz_serialization, ssz_serialization/codec, - ssz_serialization/merkleization, - unittest2 + ssz_serialization/merkleization # This follows how # https://github.com/status-im/nimbus-eth2/blob/9839f140628ae0e2e8aa7eb055da5c4bb08171d0/beacon_chain/spec/ssz_codec.nim#L29 @@ -27,53 +26,9 @@ template toSszType*(T: Hash32): auto = 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) -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]) - # check r1 == r2 - # check r1 != r3 +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..38394b25 --- /dev/null +++ b/eth/ssz/blocks_ssz.nim @@ -0,0 +1,75 @@ +import + ssz_serialization, + ./adapter, + ../common/[addresses, hashes], + ./transaction_ssz + +const + # Post-merge constants + EMPTY_OMMERS_HASH* = hash32"1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + +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 + ].} = 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 + # Note: Field 16 (system_logs_root) not yet in use + + 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/blocks_ssz_adapter.nim b/eth/ssz/blocks_ssz_adapter.nim new file mode 100644 index 00000000..eedcd2a5 --- /dev/null +++ b/eth/ssz/blocks_ssz_adapter.nim @@ -0,0 +1,229 @@ +import + ssz_serialization, + ssz_serialization/merkleization, + ../common/[addresses, base, hashes, times,transactions,blocks,headers ], + ./blocks_ssz, + ./transaction_ssz, + ./sszcodec + +type + Withdrawal_SSZ* = blocks_ssz.Withdrawal + Header_SSZ* = blocks_ssz.Header + BlockBody_SSZ* = blocks_ssz.BlockBody + Block_SSZ* = blocks_ssz.Block + Withdrawal_RLP* = blocks.Withdrawal + Header_RLP* = headers.Header + BlockBody_RLP* = blocks.BlockBody + Block_RLP* = blocks.Block + +proc toSszWithdrawal*(w: Withdrawal_RLP): Withdrawal_SSZ = + Withdrawal_SSZ( + index: w.index, + validatorIndex: w.validatorIndex, + address: w.address, + amount: w.amount + ) + +proc toSszHeader*(h: Header_RLP): Header_SSZ = + var extraData: seq[uint8] + for b in h.extraData: + extraData.add(b) + + + let blobBaseFee = if h.excessBlobGas.isSome and h.excessBlobGas.get > 0: + # TODO: Proper calculation: fake_exponential(MIN_BASE_FEE_PER_BLOB_GAS, excess, denominator) + 1'u64 + else: + 0'u64 + + Header_SSZ( + parent_hash: h.parentHash, + miner: h.coinbase, # coinbase → miner (Engine API naming) + state_root: h.stateRoot.Bytes32, + transactions_root: h.transactionsRoot, + receipts_root: h.receiptsRoot, + number: h.number.uint64, + gas_limits: blocks_ssz.GasAmounts( + regular: h.gasLimit, + blob: 0'u64 + ), + gas_used: blocks_ssz.GasAmounts( + regular: h.gasUsed, + blob: if h.blobGasUsed.isSome: h.blobGasUsed.get else: 0'u64 + ), + timestamp: h.timestamp.uint64, + extra_data: extraData, + mix_hash: h.mixHash, + base_fees_per_gas: blocks_ssz.BlobFeesPerGas( + regular: if h.baseFeePerGas.isSome: h.baseFeePerGas.get.truncate(uint64) else: 0'u64, + blob: blobBaseFee + ), + withdrawals_root: if h.withdrawalsRoot.isSome: + h.withdrawalsRoot.get + else: + default(Root), + excess_gas: blocks_ssz.GasAmounts( + regular: 0, # Regular gas has no excess concept + blob: if h.excessBlobGas.isSome: h.excessBlobGas.get else: 0'u64 + ), + parent_beacon_block_root: if h.parentBeaconBlockRoot.isSome: + h.parentBeaconBlockRoot.get + else: + default(Root), + requests_hash: if h.requestsHash.isSome: + h.requestsHash.get.Bytes32 + else: + default(Bytes32) + ) + +proc toSszBlockBody*(body: BlockBody_RLP): BlockBody_SSZ = + var sszBody = BlockBody_SSZ() + + for tx in body.transactions: + sszBody.transactions.add(toSszTx(tx)) + + for uncle in body.uncles: + sszBody.uncles.add(toSszHeader(uncle)) + + if body.withdrawals.isSome: + var withdrawalsList: seq[Withdrawal_SSZ] + for w in body.withdrawals.get: + withdrawalsList.add(toSszWithdrawal(w)) + sszBody.withdrawals = Opt.some(withdrawalsList) + else: + sszBody.withdrawals = Opt.none(seq[Withdrawal_SSZ]) + + sszBody + +proc toSszBlock*(blk: Block_RLP): Block_SSZ = + var sszBlock = Block_SSZ() + sszBlock.header = toSszHeader(blk.header) + for tx in blk.transactions: + sszBlock.transactions.add(toSszTx(tx)) + for uncle in blk.uncles: + sszBlock.uncles.add(toSszHeader(uncle)) + if blk.withdrawals.isSome: + var withdrawalsList: seq[Withdrawal_SSZ] + for w in blk.withdrawals.get: + withdrawalsList.add(toSszWithdrawal(w)) + sszBlock.withdrawals = Opt.some(withdrawalsList) + else: + sszBlock.withdrawals = Opt.none(seq[Withdrawal_SSZ]) + sszBlock + +proc fromSszWithdrawal*(w: Withdrawal_SSZ): Withdrawal_RLP = + Withdrawal_RLP( + index: w.index, + validatorIndex: w.validatorIndex, + address: w.address, + amount: w.amount + ) + + +proc fromSszHeader*(h: Header_SSZ): Header_RLP = + + var extraData: seq[byte] + for b in h.extra_data: + extraData.add(b) + + Header_RLP( + parentHash: h.parent_hash, + ommersHash: blocks_ssz.EMPTY_OMMERS_HASH, # Constant post-merge + coinbase: h.miner, # miner → coinbase + stateRoot: h.state_root.Hash32, + transactionsRoot: h.transactions_root, + receiptsRoot: h.receipts_root, + logsBloom: default(Bloom), # Not in minimal SSZ header + difficulty: 0.u256, # 0 post-merge + number: h.number.BlockNumber, + gasLimit: h.gas_limits.regular.GasInt, + gasUsed: h.gas_used.regular.GasInt, + timestamp: h.timestamp.EthTime, + extraData: extraData, + mixHash: h.mix_hash, + nonce: default(Bytes8), # 0 post-merge + baseFeePerGas: if h.base_fees_per_gas.regular != 0: + Opt.some(h.base_fees_per_gas.regular.u256) + else: + Opt.none(UInt256), + withdrawalsRoot: if h.withdrawals_root != default(Root): + Opt.some(h.withdrawals_root) + else: + Opt.none(Hash32), + blobGasUsed: if h.gas_used.blob != 0: + Opt.some(h.gas_used.blob) + else: + Opt.none(uint64), + excessBlobGas: if h.excess_gas.blob != 0: + Opt.some(h.excess_gas.blob) + else: + Opt.none(uint64), + parentBeaconBlockRoot: if h.parent_beacon_block_root != default(Root): + Opt.some(h.parent_beacon_block_root) + else: + Opt.none(Hash32), + requestsHash: if h.requests_hash != default(Bytes32): + Opt.some(h.requests_hash.Hash32) + else: + Opt.none(Hash32) + ) + +proc fromSszBlockBody*(body: BlockBody_SSZ): BlockBody_RLP = + var rlpBody = BlockBody_RLP() + for tx in body.transactions: + rlpBody.transactions.add(toOldTx(tx)) + for uncle in body.uncles: + rlpBody.uncles.add(fromSszHeader(uncle)) + if body.withdrawals.isSome: + var wds: seq[Withdrawal_RLP] + for w in body.withdrawals.get: + wds.add(fromSszWithdrawal(w)) + rlpBody.withdrawals = Opt.some(wds) + else: + rlpBody.withdrawals = Opt.none(seq[Withdrawal_RLP]) + + rlpBody + +proc fromSszBlock*(blk: Block_SSZ): Block_RLP = + + var rlpBlock = Block_RLP() + rlpBlock.header = fromSszHeader(blk.header) + for tx in blk.transactions: + rlpBlock.transactions.add(toOldTx(tx)) + for uncle in blk.uncles: + rlpBlock.uncles.add(fromSszHeader(uncle)) + if blk.withdrawals.isSome: + var wds: seq[Withdrawal_RLP] + for w in blk.withdrawals.get: + wds.add(fromSszWithdrawal(w)) + rlpBlock.withdrawals = Opt.some(wds) + else: + rlpBlock.withdrawals = Opt.none(seq[Withdrawal_RLP]) + + rlpBlock + + +proc computeTransactionsRootFromRlp*(txs: seq[transactions.Transaction]): Root = + var sszTxs: seq[transaction_ssz.Transaction] + for tx in txs: + sszTxs.add(toSszTx(tx)) + Hash32(sszTxs.hash_tree_root().data) + +proc computeWithdrawalsRootFromRlp*(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)) + Hash32(sszWds.hash_tree_root().data) + +proc computeBlockHashSsz*(blk: Block_RLP): Hash32 = + ## EIP-7807: Compute SSZ-based block hash from RLP block + let sszHeader = toSszHeader(blk.header) + Hash32(sszHeader.hash_tree_root().data) + +proc computeBlockHashSsz*(header: Header_RLP): Hash32 = + ## EIP-7807: Compute SSZ-based block hash from RLP header + let sszHeader = toSszHeader(header) + Hash32(sszHeader.hash_tree_root().data) diff --git a/eth/ssz/sszcodec.nim b/eth/ssz/sszcodec.nim index 17669fec..d0fb719a 100644 --- a/eth/ssz/sszcodec.nim +++ b/eth/ssz/sszcodec.nim @@ -1,12 +1,11 @@ import - std/[strutils, sequtils, options], + std/[sequtils], stint, - ssz_serialization/merkleization, - ./[signatures, receipts, transaction_builder], + ./signatures, ./transaction_ssz as ssz_tx, + ./transaction_builder, ../common/[addresses_rlp, base_rlp], - ../common/transactions as rlp_tx_mod, - ../rlp/[length_writer, two_pass_writer, hash_writer] + ../common/transactions as rlp_tx_mod # Gas -> FeePerGas proc feeFromGas(x: rlp_tx_mod.GasInt): ssz_tx.FeePerGas = @@ -21,15 +20,8 @@ proc toGasInt(x: ssz_tx.FeePerGas): rlp_tx_mod.GasInt = # TODO:verify with etan+advaita(advaita say sanity check one is ok) rlp_tx_mod.GasInt(x.limbs[0]) - # Normalize any legacy V into 0/1 -func vToParity(v: uint8): uint8 = - if v == 27'u8: 0'u8 - elif v == 28'u8: 1'u8 - else: v and 1'u8 - - proc accessTupleFrom(pair: rlp_tx_mod.AccessPair): ssz_tx.AccessTuple = - # Old storageKeys: seq[Bytes32]; new seq[Hash32].( bruh ) + # 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: @@ -38,71 +30,81 @@ proc accessTupleFrom(pair: rlp_tx_mod.AccessPair): ssz_tx.AccessTuple = proc accessListFrom(al: rlp_tx_mod.AccessList): seq[ssz_tx.AccessTuple] = al.map(accessTupleFrom) -proc ensureAuthMagic(m: TransactionType) = - if m != AuthMagic7702: - raise newException(ValueError, "authorization.magic must be 0x05") - +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.SignedTx[ssz_tx.Authorization]] = - result = newSeq[ssz_tx.SignedTx[ssz_tx.Authorization]](al.len) + 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.Authorization( - kind: authReplayableBasic, + ssz_tx.AuthorizationPayload( + kind: ssz_tx.authReplayableBasic, replayable: ssz_tx.RlpReplayableBasicAuthorizationPayload( - magic: AuthMagic7702, + magic: ssz_tx.AuthMagic7702, address: a.address, - nonce: uint64(a.nonce), + nonce: uint64(a.nonce), ) ) else: - ssz_tx.Authorization( - kind: authBasic, + ssz_tx.AuthorizationPayload( + kind: ssz_tx.authBasic, basic: ssz_tx.RlpBasicAuthorizationPayload( - magic: AuthMagic7702, + magic: ssz_tx.AuthMagic7702, chain_id: a.chainId, - address: a.address, - nonce: uint64(a.nonce), + address: a.address, + nonce: uint64(a.nonce), ) ) - let sig = secp256k1Pack(a.R, a.S, vToParity(a.yParity)) - result[i] = ssz_tx.SignedTx[ssz_tx.Authorization](payload: payload, signature: sig) + 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) -# sszcodec.nim -proc toRlpAuthList*(al: seq[ssz_tx.SignedTx[ssz_tx.Authorization]]): - seq[rlp_tx_mod.Authorization] = +proc toRlpAuthList*(al: seq[ssz_tx.Authorization]): seq[rlp_tx_mod.Authorization] = result = newSeq[rlp_tx_mod.Authorization](al.len) - for i, sa in al: - let (R, S, parity) = secp256k1Unpack(sa.signature) - case sa.payload.kind - of authReplayableBasic: - let p = sa.payload.replayable - ensureAuthMagic(p.magic) + 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, + chainId: ChainId(0.u256), + address: p.address, + nonce: AccountNonce(p.nonce), + yParity: parity, + r: R, + s: S ) - of authBasic: - let p = sa.payload.basic - ensureAuthMagic(p.magic) + 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, + 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 @@ -123,7 +125,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = case tx.txType of rlp_tx_mod.TxLegacy: - return Transaction( + return transaction_builder.Transaction( txType = ssz_tx.TxLegacy, chain_id = legacyChain, nonce = tx.nonce, @@ -135,7 +137,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = signature = sig, ) of rlp_tx_mod.TxEip2930: - return Transaction( + return transaction_builder.Transaction( txType = ssz_tx.TxAccessList, chain_id = tx.chainId, nonce = tx.nonce, @@ -148,7 +150,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = access_list = accessSSZ, ) of rlp_tx_mod.TxEip1559: - return Transaction( + return transaction_builder.Transaction( txType = ssz_tx.TxDynamicFee, chain_id = tx.chainId, nonce = tx.nonce, @@ -163,7 +165,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = access_list = accessSSZ, ) of rlp_tx_mod.TxEip4844: - return Transaction( + return transaction_builder.Transaction( txType = ssz_tx.TxBlob, chain_id = tx.chainId, nonce = tx.nonce, @@ -182,7 +184,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = of rlp_tx_mod.TxEip7702: if tx.to.isNone: raise newException(ValueError, "7702 setCode: requires 'to'") - return Transaction( + return transaction_builder.Transaction( txType = ssz_tx.TxSetCode, chain_id = tx.chainId, nonce = tx.nonce, @@ -195,9 +197,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.maxPriorityFeePerGas)), signature = sig, access_list = accessSSZ, - # authorization_list = toAuthList() - #TODO - authorization_list = @[], + authorization_list = toAuthTuples(tx.authorizationList), ) proc toOldTx*(tx: ssz_tx.Transaction): rlp_tx_mod.Transaction = @@ -261,7 +261,6 @@ proc toOldTx*(tx: ssz_tx.Transaction): rlp_tx_mod.Transaction = value: p.value, payload: p.input, gasPrice: toGasInt(p.max_fees_per_gas.regular), - # Similar TODO for typecast V: rlp_tx_mod.EIP155_CHAIN_ID_OFFSET + ((2 * p.chain_id).limbs[0]) + uint64(y), R: R, S: S, @@ -279,7 +278,6 @@ proc toOldTx*(tx: ssz_tx.Transaction): rlp_tx_mod.Transaction = value: p.value, payload: p.input, gasPrice: toGasInt(p.max_fees_per_gas.regular), - # Similar TODO for typecast V: rlp_tx_mod.EIP155_CHAIN_ID_OFFSET + ((2 * p.chain_id).limbs[0]) + uint64(y), R: R, S: S, @@ -395,7 +393,7 @@ proc toOldTx*(tx: ssz_tx.Transaction): rlp_tx_mod.Transaction = maxPriorityFeePerGas: toGasInt(p.max_priority_fees_per_gas.regular), maxFeePerGas: toGasInt(p.max_fees_per_gas.regular), accessList: toOldAccessList(p.access_list), - authorizationList: @[], + authorizationList: toRlpAuthList(p.authorization_list), V: uint64(y), R: R, S: S, diff --git a/eth/ssz/transaction_builder.nim b/eth/ssz/transaction_builder.nim index 49d14e1a..ba5a6723 100644 --- a/eth/ssz/transaction_builder.nim +++ b/eth/ssz/transaction_builder.nim @@ -1,103 +1,52 @@ -import stint, ./transaction_ssz, ./signatures, ../common/[addresses, base, hashes] +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 + ) + ) -#This should be placed in nimbus-eth1 -# template validateCommonFields( -# payload: untyped, -# expectedTxType: static[uint8], -# contextName: static[string], -# txTypeErrorMsg: static[string], -# ) = -# if payload.txType != expectedTxType: -# fail(txTypeErrorMsg) -# when compiles(payload.chain_id): -# if payload.chain_id == ChainId(0.u256): -# fail(contextName & ": chain_id must be non-zero") - -# # Per-payload validations -# proc validate*(p: RlpLegacyBasicTransactionPayload) = -# validateCommonFields( -# p, 0x00'u8, "legacy basic", "legacy basic: txType must be 0x00 (TxLegacy)" -# ) - -# proc validate*(p: RlpLegacyCreateTransactionPayload) = -# validateCommonFields( -# p, 0x00'u8, "legacy create", "legacy create: txType must be 0x00 (TxLegacy)" -# ) -# if p.input.len == 0: -# fail("legacy create: initcode (input) must be non-empty") - -# proc validate*(p: RlpAccessListBasicTransactionPayload) = -# validateCommonFields( -# p, 0x01'u8, "2930 basic", "2930 basic: txType must be 0x01 (TxAccessList)" -# ) - -# proc validate*(p: RlpAccessListCreateTransactionPayload) = -# validateCommonFields( -# p, 0x01'u8, "2930 create", "2930 create: txType must be 0x01 (TxAccessList)" -# ) -# if p.input.len == 0: -# fail("2930 create: initcode (input) must be non-empty") - -# proc validate*(p: RlpBasicTransactionPayload) = -# validateCommonFields( -# p, 0x02'u8, "1559 basic", "1559 basic: txType must be 0x02 (TxDynamicFee)" -# ) - -# proc validate*(p: RlpCreateTransactionPayload) = -# validateCommonFields( -# p, 0x02'u8, "1559 create", "1559 create: txType must be 0x02 (TxDynamicFee)" -# ) -# if p.input.len == 0: -# fail("1559 create: initcode (input) must be non-empty") - -# proc validate*(p: RlpBlobTransactionPayload) = -# validateCommonFields( -# p, 0x03'u8, "4844 blob", "4844 blob: txType must be 0x03 (TxBlob)" -# ) -# if p.blob_versioned_hashes.len == 0: -# fail("4844 blob: blob_versioned_hashes must be non-empty") - -# proc validate*(p: RlpSetCodeTransactionPayload) = -# validateCommonFields(p, 0x04'u8, "7702", "7702: txType must be 0x04 (SetCode)") -# if p.authorization_list.len == 0: -# fail("7702: authorization_list must be non-empty") - -# proc validate*(p: RlpLegacyReplayableBasicTransactionPayload) = -# validateCommonFields( -# p, 0x00'u8, -# "legacy replayable basic", -# "legacy replayable basic: txType must be 0x00 (TxLegacy)" -# ) - -# proc validate*(p: RlpLegacyReplayableCreateTransactionPayload) = -# validateCommonFields( -# p, 0x00'u8, -# "legacy replayable create", -# "legacy replayable create: txType must be 0x00 (TxLegacy)" -# ) -# if p.input.len == 0: -# fail("legacy create: initcode (input) must be non-empty") + Authorization( + payload: payload, + signature: secp256k1Pack(t.r, t.s, t.y_parity) + ) -# proc validate*(sig: Secp256k1ExecutionSignature) = -# if not secp256k1Validate(sig): -# fail("invalid secp256k1 signature") +proc makeAuthorizationList*(xs: openArray[AuthTuple]): seq[Authorization] = + result = newSeqOfCap[Authorization](xs.len) + for x in xs: + result.add makeAuthorization(x) -# BuildWrap: generates build(payload, signature) -> Transaction using the payload-specific validate* template BuildWrap( PayloadT, WrapperT: typedesc, tag: static[RLPTransactionKind], fieldSym: untyped ) = proc build*( payload: PayloadT, signature: Secp256k1ExecutionSignature ): Transaction {.inline.} = - # validate(payload) - # validate signature - # validate(signature) let inner = WrapperT(payload: payload, signature: signature) Transaction( kind: RlpTransaction, rlp: RlpTransactionObject(kind: tag, fieldSym: inner) @@ -119,7 +68,7 @@ proc Transaction*( chain_id: ChainId, nonce: uint64, gas: GasAmount, - to: Opt[Address], # some(addr) => call, none => create + to: Opt[Address], value: UInt256, input: openArray[byte], max_fees_per_gas: BasicFeesPerGas, @@ -128,8 +77,9 @@ proc Transaction*( access_list: seq[AccessTuple] = @[], blob_versioned_hashes: seq[VersionedHash] = @[], blob_fee: FeePerGas = 0.u256, - authorization_list: seq[transaction_ssz.Authorization] = @[], + authorization_list: seq[AuthTuple] = @[], ): Transaction = + let auths = makeAuthorizationList(authorization_list) case txType of TxLegacy: if to.isSome: @@ -233,9 +183,6 @@ proc Transaction*( ) return build(p, signature) of TxBlob: - # if to.isNone: - # fail("4844 blob: create-style not supported") - when compiles(BlobFeesPerGas): let blobFees = BlobFeesPerGas(regular: max_fees_per_gas.regular, blob: blob_fee) let p = RlpBlobTransactionPayload( txType: txType, @@ -251,22 +198,7 @@ proc Transaction*( blob_versioned_hashes: blob_versioned_hashes, ) return build(p, signature) - else: - fail("4844 blob: BlobFeesPerGas type not available in this build") of TxSetCode: - if to.isNone: - fail("7702 setCode: requires 'to'") - if authorization_list.len == 0: - fail("7702 setCode: authorization_list must be non-empty") - # Minimal validation: ensure auth magic is set correctly. - for i, a in authorization_list: - case a.kind - of transaction_ssz.AuthorizationKind.authReplayableBasic: - if a.replayable.magic != transaction_ssz.AuthMagic7702: - fail("7702 setCode: auth[" & $i & "] replayable.magic must be 0x05") - of transaction_ssz.AuthorizationKind.authBasic: - if a.basic.magic != transaction_ssz.AuthMagic7702: - fail("7702 setCode: auth[" & $i & "] basic.magic must be 0x05") let p = RlpSetCodeTransactionPayload( txType: TxSetCode, chain_id: chain_id, @@ -278,7 +210,7 @@ proc Transaction*( input: @input, access_list: access_list, max_priority_fees_per_gas: max_priority_fees_per_gas, - authorization_list: authorization_list, + authorization_list: auths, ) return build(p, signature) else: diff --git a/eth/ssz/transaction_ssz.nim b/eth/ssz/transaction_ssz.nim index 269a808e..d3168671 100644 --- a/eth/ssz/transaction_ssz.nim +++ b/eth/ssz/transaction_ssz.nim @@ -1,9 +1,9 @@ -import ssz_serialization -import stint -import ../common/[addresses, base, hashes] -import ./signatures -import ./adapter -import serialization/case_objects +import + ssz_serialization, stint, + ../common/[addresses, base, hashes], + ./signatures, + ./adapter, + serialization/case_objects export adapter @@ -146,7 +146,7 @@ type sszActiveFields: [1, 0, 1, 1] .} = object magic*: TransactionType # 0x05 (Auth) - address*: Address # ExecutionAddress + address*: Address nonce*: uint64 RlpBasicAuthorizationPayload* {. @@ -154,20 +154,24 @@ type .} = object magic*: TransactionType # 0x05 (Auth) chain_id*: ChainId - address*: Address # ExecutionAddress + address*: Address nonce*: uint64 AuthorizationKind* = enum authReplayableBasic authBasic - Authorization* = object + 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 @@ -251,3 +255,13 @@ type 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/ssz/all_tests.nim b/tests/ssz/all_tests.nim index c0417a04..1d38abb3 100644 --- a/tests/ssz/all_tests.nim +++ b/tests/ssz/all_tests.nim @@ -1,2 +1,7 @@ import - ./receipts, ./transaction_builder, ./transaction_ssz, ./signature, ./transaction_codec + ./receipts, + ./transaction_builder, + ./transaction_ssz, + ./signature, + ./transaction_codec, + ./types diff --git a/tests/ssz/block.nim b/tests/ssz/block.nim deleted file mode 100644 index 94325eac..00000000 --- a/tests/ssz/block.nim +++ /dev/null @@ -1,192 +0,0 @@ -import - std/[os, strutils, json], - stew/[byteutils, io2], - ssz_serialization, - ../../eth/[common, rlp], - ../../eth/common/transactions as rlp_tx_mod, - ../../eth/ssz/[sszcodec,adapter], - ../../eth/ssz/transaction_ssz as ssz_tx, - unittest2 - -proc eip2718Dir*(): string = - (currentSourcePath.parentDir / ".." / "common" / "eip2718").normalizedPath - -proc rlpsDir*(): string = - (currentSourcePath.parentDir / ".." / "common" / "rlps").normalizedPath - -proc listEip2718Files*(): seq[string] = - let dir = eip2718Dir() - if not dir.dirExists: return @[] - for kind, path in walkDir(dir): - if kind == pcFile and path.endsWith(".json"): - result.add path - -proc eip2718FilePath*(index: int): string = - eip2718Dir() / ("acl_block_" & $index & ".json") - -proc listSelectedEip2718Files*(indices: openArray[int]): seq[string] = - for i in indices: - let p = eip2718FilePath(i) - if p.fileExists: - result.add p - -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 - -# Loaders from a specific file -proc loadEip2718BlockFromFile*(path: string): EthBlock = - let n = json.parseFile(path) - if not n.hasKey("rlp"): - raise newException(ValueError, "JSON has no 'rlp' key: " & path) - let hexRlp = n["rlp"].getStr() - let bytes = hexToSeqByte(hexRlp) - rlp.decode(bytes, EthBlock) - -# Extract eth/common transactions from a JSON fixture -proc loadEip2718TransactionsFromFile*(path: string): seq[rlp_tx_mod.Transaction] = - let blk = loadEip2718BlockFromFile(path) - blk.transactions - -# Extract transactions and their RLP bytes from a JSON fixture -proc loadEip2718TransactionsWithRlp*(path: string): seq[tuple[tx: rlp_tx_mod.Transaction, rlp: seq[byte]]] = - for tx in loadEip2718TransactionsFromFile(path): - result.add (tx: tx, rlp: rlp.encode(tx)) - -# Convert eth/common transactions to SSZ transactions -proc toSszTransactions*(txs: seq[rlp_tx_mod.Transaction]): seq[ssz_tx.Transaction] = - for tx in txs: - result.add toSszTx(tx) - -# From a JSON fixture file, produce SSZ txs and their SSZ encodings -proc loadEip2718SszTransactionsWithSsz*(path: string): seq[tuple[tx: ssz_tx.Transaction, ssz: seq[byte]]] = - let rlpTxs = loadEip2718TransactionsFromFile(path) - for stx in toSszTransactions(rlpTxs): - result.add (tx: stx, ssz: SSZ.encode(stx)) - -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 - -# Load all EIP-2718 JSON fixtures (acl_block_*.json) and decode their RLP into EthBlock objects -proc loadEip2718Blocks*(): seq[EthBlock] = - let dir = eip2718Dir() - if not dir.dirExists: return @[] - for kind, path in walkDir(dir): - if kind == pcFile and path.endsWith(".json"): - try: - result.add loadEip2718BlockFromFile(path) - except CatchableError: - discard - -# Load blocks from binary .rlp fixtures; supports multi-block streams -# limitPerFile controls how many blocks to extract from each file (default 1 for speed) -proc loadRlpBlocks*(limitPerFile: int = 1): seq[EthBlock] = - let dir = rlpsDir() - if not dir.dirExists: return @[] - for kind, path in walkDir(dir): - if kind == pcFile and path.endsWith(".rlp"): - try: - result.add loadRlpBlocksFromFile(path, limitPerFile) - except CatchableError: - discard - -# --- Simple CLI printing helpers --- -proc summarize*(b: EthBlock): string = - let txCount = b.transactions.len - let uncles = b.uncles.len - let w = (if b.withdrawals.isSome: $b.withdrawals.get.len else: "none") - # compute a quick content hash for reference - let h = rlp.computeRlpHash(b) - "txs=" & $txCount & ", uncles=" & $uncles & ", withdrawals=" & w & - ", rlpHash=0x" & h.data.toHex - -proc printPicked*() = - echo "== EIP-2718 JSON fixtures ==" - let jsonFiles = listEip2718Files() - if jsonFiles.len == 0: - echo "(none)" - else: - for f in jsonFiles: - try: - let blk = loadEip2718BlockFromFile(f) - echo f, " -> ", summarize(blk) - # Also print transactions and their RLP bytes (hex) - let txsWithRlp = loadEip2718TransactionsWithRlp(f) - echo " txs: ", txsWithRlp.len - var idx = 0 - for item in txsWithRlp: - let hex = item.rlp.toHex() - echo " [", idx, "] type=", $item.tx.txType, ", nonce=", $item.tx.nonce - echo " rlp=0x", hex - inc idx - except CatchableError as e: - echo f, " -> ERROR: ", e.msg - - # echo "\n== RLP fixtures ==" - # let rlpFiles = listRlpFiles() - # if rlpFiles.len == 0: - # echo "(none)" - # else: - # for f in rlpFiles: - # try: - # let blks = loadRlpBlocksFromFile(f, 1) # first block per file - # if blks.len == 0: - # echo f, " -> (no blocks)" - # else: - # echo f, " -> ", summarize(blks[0]) - # except CatchableError as e: - # echo f, " -> ERROR: ", e.msg - -when isMainModule: - # Print only EIP-2718 blocks 9 and 8, with their transactions and full RLP - echo "== EIP-2718 JSON fixtures (selected: 9, 8) ==" - let selected = listSelectedEip2718Files([9,]) - if selected.len == 0: - echo "(none)" - else: - for f in selected: - try: - let blk = loadEip2718BlockFromFile(f) - echo f, " -> ", summarize(blk) - let txsWithRlp = loadEip2718TransactionsWithRlp(f) - echo " txs: ", txsWithRlp.len - var idx = 0 - for item in txsWithRlp: - let hex = item.rlp.toHex() - echo " [", idx, "] type=", $item.tx.txType, ", nonce=", $item.tx.nonce - echo " rlp=0x", hex - inc idx - # Also show SSZ-converted transactions and their SSZ bytes - let sszTxs = loadEip2718SszTransactionsWithSsz(f) - var sidx = 0 - for it in sszTxs: - let sszHex = it.ssz.toHex() - echo " (ssz)[", sidx, "] kind=", $it.tx.kind - echo " ssz=0x", sszHex - inc sidx - except CatchableError as e: - echo f, " -> ERROR: ", e.msg - -# Unit tests: RLP -> SSZ -> SSZ bytes are stable -suite "EIP-2718 tx RLP->SSZ->SSZ round-trip": - for idx in [9, 8]: - test "block " & $idx & ": tx SSZ bytes stable after decode": - let path = eip2718FilePath(idx) - let sszTuples = loadEip2718SszTransactionsWithSsz(path) - check sszTuples.len > 0 - for it in sszTuples: - let dec = SSZ.decode(it.ssz, ssz_tx.Transaction) - let enc2 = SSZ.encode(dec) - # check enc2 == it.ssz - - diff --git a/tests/ssz/block_rt.nim b/tests/ssz/block_rt.nim new file mode 100644 index 00000000..3259a811 --- /dev/null +++ b/tests/ssz/block_rt.nim @@ -0,0 +1,325 @@ +import + std/[os, strutils, json], + stew/[byteutils, io2], + ssz_serialization, + ../../eth/[common, rlp], + ../../eth/common/transactions as rlp_tx_mod, + ../../eth/ssz/[sszcodec,adapter,blocks_ssz,blocks_ssz_adapter], + ../../eth/ssz/transaction_ssz as ssz_tx, + unittest2 + +proc eip2718Dir*(): string = + (currentSourcePath.parentDir / ".." / "common" / "eip2718").normalizedPath + +proc rlpsDir*(): string = + (currentSourcePath.parentDir / ".." / "common" / "rlps").normalizedPath + +proc listEip2718Files*(): seq[string] = + let dir = eip2718Dir() + if not dir.dirExists: return @[] + for kind, path in walkDir(dir): + if kind == pcFile and path.endsWith(".json"): + result.add path + +proc eip2718FilePath*(index: int): string = + eip2718Dir() / ("acl_block_" & $index & ".json") + +proc listSelectedEip2718Files*(indices: openArray[int]): seq[string] = + for i in indices: + let p = eip2718FilePath(i) + if p.fileExists: + result.add p + +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 loadEip2718BlockFromFile*(path: string): EthBlock = + let n = json.parseFile(path) + if not n.hasKey("rlp"): + raise newException(ValueError, "JSON has no 'rlp' key: " & path) + let hexRlp = n["rlp"].getStr() + let bytes = hexToSeqByte(hexRlp) + rlp.decode(bytes, EthBlock) + +# Extract eth/common transactions from a JSON fixture +proc loadEip2718TransactionsFromFile*(path: string): seq[rlp_tx_mod.Transaction] = + let blk = loadEip2718BlockFromFile(path) + blk.transactions + +# Extract transactions and their RLP bytes from a JSON fixture +proc loadEip2718TransactionsWithRlp*(path: string): seq[tuple[tx: rlp_tx_mod.Transaction, rlp: seq[byte]]] = + for tx in loadEip2718TransactionsFromFile(path): + result.add (tx: tx, rlp: rlp.encode(tx)) + +# Convert eth/common transactions to SSZ transactions +proc toSszTransactions*(txs: seq[rlp_tx_mod.Transaction]): seq[ssz_tx.Transaction] = + for tx in txs: + result.add toSszTx(tx) + +# From a JSON fixture file, produce SSZ txs and their SSZ encodings +proc loadEip2718SszTransactionsWithSsz*(path: string): seq[tuple[tx: ssz_tx.Transaction, ssz: seq[byte]]] = + let rlpTxs = loadEip2718TransactionsFromFile(path) + for stx in toSszTransactions(rlpTxs): + result.add (tx: stx, ssz: SSZ.encode(stx)) + +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 + +# Load all EIP-2718 JSON fixtures (acl_block_*.json) and decode their RLP into EthBlock objects +proc loadEip2718Blocks*(): seq[EthBlock] = + let dir = eip2718Dir() + if not dir.dirExists: return @[] + for kind, path in walkDir(dir): + if kind == pcFile and path.endsWith(".json"): + try: + result.add loadEip2718BlockFromFile(path) + except CatchableError: + discard + + +suite "SSZ Block Roundtrip ": + test "All blocks: RLP → SSZ → RLP preserves data": + for i in 0..9: # Test blocks 0-9 + let path = eip2718FilePath(i) + if not path.fileExists: + continue + + echo "Testing block ", i, ": ", path + let rlpBlock = loadEip2718BlockFromFile(path) + let sszBlock = toSszBlock(rlpBlock) + # Convert back to common/block + let reconstructed = fromSszBlock(sszBlock) + + # Verify critical fields preserved + check reconstructed.header.number == rlpBlock.header.number + check reconstructed.header.parentHash == rlpBlock.header.parentHash + check reconstructed.header.stateRoot == rlpBlock.header.stateRoot + check reconstructed.header.gasUsed == rlpBlock.header.gasUsed + check reconstructed.header.gasLimit == rlpBlock.header.gasLimit + check reconstructed.transactions.len == rlpBlock.transactions.len + check reconstructed.uncles.len == rlpBlock.uncles.len + + # wont be able to do a full block rt as the opt[withdrawals] may not match ssz requirements + test "All blocks: SSZ → bytes → SSZ preserves data": + for i in 0..9: + let path = eip2718FilePath(i) + if not path.fileExists: + continue + + echo "Testing SSZ serialization for block ", i, ": ", path + let rlpBlock = loadEip2718BlockFromFile(path) + let sszBlock = toSszBlock(rlpBlock) + + let headerBytes = SSZ.encode(sszBlock.header) + let decodedHeader = SSZ.decode(headerBytes, Header_SSZ) + + check decodedHeader.number == sszBlock.header.number + check decodedHeader.parent_hash == sszBlock.header.parent_hash + check decodedHeader.state_root == sszBlock.header.state_root + + test "Individual block tests": + let path = eip2718FilePath(9) + if path.fileExists: + echo "Testing individual block 9: ", path + let rlpBlock = loadEip2718BlockFromFile(path) + let sszBlock = toSszBlock(rlpBlock) + # Convert back to common/block + let reconstructed = fromSszBlock(sszBlock) + # Verify critical fields preserved + check reconstructed.header.number == rlpBlock.header.number + check reconstructed.header.parentHash == rlpBlock.header.parentHash + check reconstructed.header.stateRoot == rlpBlock.header.stateRoot + check reconstructed.header.gasUsed == rlpBlock.header.gasUsed + check reconstructed.header.gasLimit == rlpBlock.header.gasLimit + check reconstructed.transactions.len == rlpBlock.transactions.len + check reconstructed.uncles.len == rlpBlock.uncles.len + +# suite "SSZ Root Computation (EIP-6404, 6465)": +# test "Block 9: Transaction root computation (EIP-6404)": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) + +# # Compute SSZ transactions root +# let txRoot = computeTransactionsRootFromRlp(rlpBlock.transactions) + +# # Should be non-zero +# check txRoot != default(Root) + +# # Should be stable (same result twice) +# let txRoot2 = computeTransactionsRootFromRlp(rlpBlock.transactions) +# check txRoot == txRoot2 + +# echo " TX Root: 0x", txRoot.data.toHex[0..15], "..." + +# test "Block 9: Withdrawals root (EIP-6465)": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) + +# # Compute SSZ withdrawals root +# let wdRoot = computeWithdrawalsRootFromRlp(rlpBlock.withdrawals) + +# # Block 9 has no withdrawals +# if rlpBlock.withdrawals.isNone: +# check wdRoot == default(Root) +# else: +# check wdRoot != default(Root) + +# echo " WD Root: 0x", wdRoot.data.toHex[0..15], "..." + +# test "Block 9: Root consistency check": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) +# let sszBlock = toSszBlock(rlpBlock) + +# # Direct computation +# let directTxRoot = computeTransactionsRootFromRlp(rlpBlock.transactions) + +# # From SSZ block +# let blockTxRoot = block_ssz.computeTransactionsRoot(sszBlock.transactions) + +# # Should match +# check directTxRoot == blockTxRoot + +# test "Transaction root: Order matters": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) + +# if rlpBlock.transactions.len >= 2: +# # Original order +# let root1 = computeTransactionsRootFromRlp(rlpBlock.transactions) + +# # Reversed order +# var reversed = rlpBlock.transactions +# reversed.reverse() +# let root2 = computeTransactionsRootFromRlp(reversed) + +# # Should be different +# check root1 != root2 + +# suite "SSZ Block Hash (EIP-7807)": +# test "Block 9: SSZ hash differs from RLP hash": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) + +# # OLD: RLP-based hash +# let rlpHash = rlp.computeRlpHash(rlpBlock.header) + +# # NEW: SSZ-based hash (EIP-7807) +# let sszHash = computeBlockHashSsz(rlpBlock) + +# # They SHOULD be different (this is the point of EIP-7807!) +# check rlpHash != sszHash + +# echo " RLP hash: 0x", rlpHash.data.toHex[0..15], "..." +# echo " SSZ hash: 0x", sszHash.data.toHex[0..15], "..." + +# test "Block 9: SSZ hash is stable": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) + +# # Compute multiple times +# let hash1 = computeBlockHashSsz(rlpBlock) +# let hash2 = computeBlockHashSsz(rlpBlock) +# let hash3 = computeBlockHashSsz(rlpBlock.header) + +# # All should be identical +# check hash1 == hash2 +# check hash2 == hash3 + +# test "Block 9: SSZ hash from header matches block": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) + +# # From full block +# let hashFromBlock = computeBlockHashSsz(rlpBlock) + +# # From header only +# let hashFromHeader = computeBlockHashSsz(rlpBlock.header) + +# # Should match +# check hashFromBlock == hashFromHeader + +# test "All blocks: Compare RLP vs SSZ hashes": +# let files = listEip2718Files() +# var differCount = 0 + +# for path in files: +# try: +# let rlpBlock = loadEip2718BlockFromFile(path) +# let comparison = compareHashMethods(rlpBlock) + +# # All should differ (EIP-7807 changes hash computation) +# if comparison.rlpHash != comparison.sszHash: +# inc differCount +# except: +# discard + +# echo " Blocks with different hashes: ", differCount +# check differCount > 0 # At least some should differ + + +# suite "SSZ Block Data Integrity": +# test "Block 9: Transaction count preserved": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) +# let sszBlock = toSszBlock(rlpBlock) +# let reconstructed = fromSszBlock(sszBlock) + +# check rlpBlock.transactions.len == 10 +# check sszBlock.transactions.len == 10 +# check reconstructed.transactions.len == 10 + +# test "Block 9: Uncle count preserved": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) +# let sszBlock = toSszBlock(rlpBlock) +# let reconstructed = fromSszBlock(sszBlock) + +# check rlpBlock.uncles.len == sszBlock.uncles.len +# check sszBlock.uncles.len == reconstructed.uncles.len + +# test "Block 9: Withdrawal presence preserved": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) +# let sszBlock = toSszBlock(rlpBlock) +# let reconstructed = fromSszBlock(sszBlock) + +# check rlpBlock.withdrawals.isNone == reconstructed.withdrawals.isNone + +# if rlpBlock.withdrawals.isSome and reconstructed.withdrawals.isSome: +# check rlpBlock.withdrawals.get.len == reconstructed.withdrawals.get.len + +# test "Block 9: Header timestamp preserved": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) +# let sszHeader = toSszHeader(rlpBlock.header) +# let reconstructed = fromSszHeader(sszHeader) + +# check reconstructed.timestamp == rlpBlock.header.timestamp + +# test "Block 9: Parent hash preserved": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) +# let sszHeader = toSszHeader(rlpBlock.header) +# let reconstructed = fromSszHeader(sszHeader) + +# check reconstructed.parentHash == rlpBlock.header.parentHash + +# test "Block 9: State root preserved": +# let path = eip2718FilePath(9) +# let rlpBlock = loadEip2718BlockFromFile(path) +# let sszHeader = toSszHeader(rlpBlock.header) +# let reconstructed = fromSszHeader(sszHeader) + +# check reconstructed.stateRoot == rlpBlock.header.stateRoot diff --git a/tests/ssz/transaction_builder.nim b/tests/ssz/transaction_builder.nim index 8e19d789..059d59cb 100644 --- a/tests/ssz/transaction_builder.nim +++ b/tests/ssz/transaction_builder.nim @@ -142,14 +142,14 @@ suite "SSZ Transactions (constructor)": check tx.rlp.blob.payload.blob_versioned_hashes.len == 1 test "7702 SetCode with replayable-basic auth": - let auths = @[ - Authorization( - kind: authReplayableBasic, - replayable: RlpReplayableBasicAuthorizationPayload( - magic: AuthMagic7702, - address: recipient, - nonce: 0'u64, - ) + 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( @@ -169,18 +169,17 @@ suite "SSZ Transactions (constructor)": 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].kind == authReplayableBasic + check tx.rlp.setCode.payload.authorization_list[0].payload.kind == authReplayableBasic test "7702 SetCode with basic auth": - let auths = @[ - Authorization( - kind: authBasic, - basic: RlpBasicAuthorizationPayload( - magic: AuthMagic7702, - chain_id: ChainId(1.u256), - address: recipient, - nonce: 0'u64, - ) + 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( @@ -200,7 +199,7 @@ suite "SSZ Transactions (constructor)": 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].kind == authBasic + check tx.rlp.setCode.payload.authorization_list[0].payload.kind == authBasic test "7702 SetCode: fails when auth list empty": expect(TxBuildError): diff --git a/tests/ssz/transaction_codec.nim b/tests/ssz/transaction_codec.nim index 95260491..181bb6d3 100644 --- a/tests/ssz/transaction_codec.nim +++ b/tests/ssz/transaction_codec.nim @@ -1,37 +1,52 @@ import unittest, - ../../eth/ssz/sszcodec, - ../../eth/common/[addresses, hashes, base, eth_types_json_serialization], + 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" -# const source = address"0x0000000000000000000000000000000000000001" -# let abcdef = hexToSeqByte("abcdef") -# let storageKey = default(Bytes32) -# let accesses = @[rlp_tx.AccessPair(address: source, storageKeys: @[storageKey])] -# proc someTo(a: Address): Opt[Address] = Opt.some(a) -# proc noneTo(): Opt[Address] = Opt.none(Address) -# let R1 = 1.u256 -# let S1 = 1.u256 +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) - # let back = toOldTx(sszTx) - # check back == 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) - check oldBack == oldTx - check sszBack == sszTx + # 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 "Transactions SSZ Roundtrip": +suite "SSZ Transactions (full round-trip)": test "Legacy Tx Call": sszRoundTrip(tx0, 1) test "Legacy tx contract creation": @@ -49,29 +64,225 @@ suite "Transactions SSZ Roundtrip": 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) - # test "Minimal Blob Tx contract creation": - # sszRoundTrip(tx8, 9) - - # test "EIP-7702 (currently fail-path only)": - # expect(ValueError): - # discard toSszTx(txEip7702(10)) - -# suite "sszcodec: passing SSZ round-trips (old -> new -> SSZ -> old)": -# sszRoundTripOK("Legacy CALL pre-155", legacyCallPre155(1)) -# sszRoundTripOK("Legacy CALL EIP-155", legacyCall155(2)) -# sszRoundTripOK("Legacy CREATE pre-155", legacyCreatePre155(3, true)) -# sszRoundTripOK("Legacy CREATE EIP-155", legacyCreate155(4, true)) -# sszRoundTripOK("EIP-2930 CALL (with AL)", eip2930Call(5, true)) -# sszRoundTripOK("EIP-2930 CALL (empty AL)", eip2930Call(6, false)) -# sszRoundTripOK("EIP-2930 CREATE", eip2930Create(7, true)) -# sszRoundTripOK("EIP-1559 CALL", eip1559Call(8)) -# sszRoundTripOK("EIP-1559 CREATE", eip1559Create(9, true)) -# sszRoundTripOK("EIP-4844 CALL (with blob fee)", eip4844Call(10, true)) -# sszRoundTripOK("EIP-4844 CALL (blob fee zero)", eip4844Call(11, false)) -# sszRoundTripOK("EIP-7702 setCode (empty auths)", eip7702SetCode(12, false)) +# 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.. 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 From 2755d1875ba294d02b6ee0114b9fa9f4103cd1a9 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Fri, 10 Oct 2025 19:16:13 +0530 Subject: [PATCH 03/10] reduce rt tests as Block rt not possible --- eth/ssz/blocks_ssz.nim | 1 + eth/ssz/blocks_ssz_adapter.nim | 229 ----------------------- eth/ssz/sszcodec.nim | 43 ++++- eth/ssz/transaction_ssz.nim | 5 +- tests/ssz/block.nim | 170 +++++++++++++++++ tests/ssz/block_rt.nim | 325 --------------------------------- 6 files changed, 214 insertions(+), 559 deletions(-) delete mode 100644 eth/ssz/blocks_ssz_adapter.nim create mode 100644 tests/ssz/block.nim delete mode 100644 tests/ssz/block_rt.nim diff --git a/eth/ssz/blocks_ssz.nim b/eth/ssz/blocks_ssz.nim index 38394b25..1779a128 100644 --- a/eth/ssz/blocks_ssz.nim +++ b/eth/ssz/blocks_ssz.nim @@ -7,6 +7,7 @@ import const # Post-merge constants EMPTY_OMMERS_HASH* = hash32"1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + MAX_BLOB_GAS_PER_BLOCK* = 786432'u64 type Withdrawal* {.sszActiveFields: [1, 1, 1, 1].} = object diff --git a/eth/ssz/blocks_ssz_adapter.nim b/eth/ssz/blocks_ssz_adapter.nim deleted file mode 100644 index eedcd2a5..00000000 --- a/eth/ssz/blocks_ssz_adapter.nim +++ /dev/null @@ -1,229 +0,0 @@ -import - ssz_serialization, - ssz_serialization/merkleization, - ../common/[addresses, base, hashes, times,transactions,blocks,headers ], - ./blocks_ssz, - ./transaction_ssz, - ./sszcodec - -type - Withdrawal_SSZ* = blocks_ssz.Withdrawal - Header_SSZ* = blocks_ssz.Header - BlockBody_SSZ* = blocks_ssz.BlockBody - Block_SSZ* = blocks_ssz.Block - Withdrawal_RLP* = blocks.Withdrawal - Header_RLP* = headers.Header - BlockBody_RLP* = blocks.BlockBody - Block_RLP* = blocks.Block - -proc toSszWithdrawal*(w: Withdrawal_RLP): Withdrawal_SSZ = - Withdrawal_SSZ( - index: w.index, - validatorIndex: w.validatorIndex, - address: w.address, - amount: w.amount - ) - -proc toSszHeader*(h: Header_RLP): Header_SSZ = - var extraData: seq[uint8] - for b in h.extraData: - extraData.add(b) - - - let blobBaseFee = if h.excessBlobGas.isSome and h.excessBlobGas.get > 0: - # TODO: Proper calculation: fake_exponential(MIN_BASE_FEE_PER_BLOB_GAS, excess, denominator) - 1'u64 - else: - 0'u64 - - Header_SSZ( - parent_hash: h.parentHash, - miner: h.coinbase, # coinbase → miner (Engine API naming) - state_root: h.stateRoot.Bytes32, - transactions_root: h.transactionsRoot, - receipts_root: h.receiptsRoot, - number: h.number.uint64, - gas_limits: blocks_ssz.GasAmounts( - regular: h.gasLimit, - blob: 0'u64 - ), - gas_used: blocks_ssz.GasAmounts( - regular: h.gasUsed, - blob: if h.blobGasUsed.isSome: h.blobGasUsed.get else: 0'u64 - ), - timestamp: h.timestamp.uint64, - extra_data: extraData, - mix_hash: h.mixHash, - base_fees_per_gas: blocks_ssz.BlobFeesPerGas( - regular: if h.baseFeePerGas.isSome: h.baseFeePerGas.get.truncate(uint64) else: 0'u64, - blob: blobBaseFee - ), - withdrawals_root: if h.withdrawalsRoot.isSome: - h.withdrawalsRoot.get - else: - default(Root), - excess_gas: blocks_ssz.GasAmounts( - regular: 0, # Regular gas has no excess concept - blob: if h.excessBlobGas.isSome: h.excessBlobGas.get else: 0'u64 - ), - parent_beacon_block_root: if h.parentBeaconBlockRoot.isSome: - h.parentBeaconBlockRoot.get - else: - default(Root), - requests_hash: if h.requestsHash.isSome: - h.requestsHash.get.Bytes32 - else: - default(Bytes32) - ) - -proc toSszBlockBody*(body: BlockBody_RLP): BlockBody_SSZ = - var sszBody = BlockBody_SSZ() - - for tx in body.transactions: - sszBody.transactions.add(toSszTx(tx)) - - for uncle in body.uncles: - sszBody.uncles.add(toSszHeader(uncle)) - - if body.withdrawals.isSome: - var withdrawalsList: seq[Withdrawal_SSZ] - for w in body.withdrawals.get: - withdrawalsList.add(toSszWithdrawal(w)) - sszBody.withdrawals = Opt.some(withdrawalsList) - else: - sszBody.withdrawals = Opt.none(seq[Withdrawal_SSZ]) - - sszBody - -proc toSszBlock*(blk: Block_RLP): Block_SSZ = - var sszBlock = Block_SSZ() - sszBlock.header = toSszHeader(blk.header) - for tx in blk.transactions: - sszBlock.transactions.add(toSszTx(tx)) - for uncle in blk.uncles: - sszBlock.uncles.add(toSszHeader(uncle)) - if blk.withdrawals.isSome: - var withdrawalsList: seq[Withdrawal_SSZ] - for w in blk.withdrawals.get: - withdrawalsList.add(toSszWithdrawal(w)) - sszBlock.withdrawals = Opt.some(withdrawalsList) - else: - sszBlock.withdrawals = Opt.none(seq[Withdrawal_SSZ]) - sszBlock - -proc fromSszWithdrawal*(w: Withdrawal_SSZ): Withdrawal_RLP = - Withdrawal_RLP( - index: w.index, - validatorIndex: w.validatorIndex, - address: w.address, - amount: w.amount - ) - - -proc fromSszHeader*(h: Header_SSZ): Header_RLP = - - var extraData: seq[byte] - for b in h.extra_data: - extraData.add(b) - - Header_RLP( - parentHash: h.parent_hash, - ommersHash: blocks_ssz.EMPTY_OMMERS_HASH, # Constant post-merge - coinbase: h.miner, # miner → coinbase - stateRoot: h.state_root.Hash32, - transactionsRoot: h.transactions_root, - receiptsRoot: h.receipts_root, - logsBloom: default(Bloom), # Not in minimal SSZ header - difficulty: 0.u256, # 0 post-merge - number: h.number.BlockNumber, - gasLimit: h.gas_limits.regular.GasInt, - gasUsed: h.gas_used.regular.GasInt, - timestamp: h.timestamp.EthTime, - extraData: extraData, - mixHash: h.mix_hash, - nonce: default(Bytes8), # 0 post-merge - baseFeePerGas: if h.base_fees_per_gas.regular != 0: - Opt.some(h.base_fees_per_gas.regular.u256) - else: - Opt.none(UInt256), - withdrawalsRoot: if h.withdrawals_root != default(Root): - Opt.some(h.withdrawals_root) - else: - Opt.none(Hash32), - blobGasUsed: if h.gas_used.blob != 0: - Opt.some(h.gas_used.blob) - else: - Opt.none(uint64), - excessBlobGas: if h.excess_gas.blob != 0: - Opt.some(h.excess_gas.blob) - else: - Opt.none(uint64), - parentBeaconBlockRoot: if h.parent_beacon_block_root != default(Root): - Opt.some(h.parent_beacon_block_root) - else: - Opt.none(Hash32), - requestsHash: if h.requests_hash != default(Bytes32): - Opt.some(h.requests_hash.Hash32) - else: - Opt.none(Hash32) - ) - -proc fromSszBlockBody*(body: BlockBody_SSZ): BlockBody_RLP = - var rlpBody = BlockBody_RLP() - for tx in body.transactions: - rlpBody.transactions.add(toOldTx(tx)) - for uncle in body.uncles: - rlpBody.uncles.add(fromSszHeader(uncle)) - if body.withdrawals.isSome: - var wds: seq[Withdrawal_RLP] - for w in body.withdrawals.get: - wds.add(fromSszWithdrawal(w)) - rlpBody.withdrawals = Opt.some(wds) - else: - rlpBody.withdrawals = Opt.none(seq[Withdrawal_RLP]) - - rlpBody - -proc fromSszBlock*(blk: Block_SSZ): Block_RLP = - - var rlpBlock = Block_RLP() - rlpBlock.header = fromSszHeader(blk.header) - for tx in blk.transactions: - rlpBlock.transactions.add(toOldTx(tx)) - for uncle in blk.uncles: - rlpBlock.uncles.add(fromSszHeader(uncle)) - if blk.withdrawals.isSome: - var wds: seq[Withdrawal_RLP] - for w in blk.withdrawals.get: - wds.add(fromSszWithdrawal(w)) - rlpBlock.withdrawals = Opt.some(wds) - else: - rlpBlock.withdrawals = Opt.none(seq[Withdrawal_RLP]) - - rlpBlock - - -proc computeTransactionsRootFromRlp*(txs: seq[transactions.Transaction]): Root = - var sszTxs: seq[transaction_ssz.Transaction] - for tx in txs: - sszTxs.add(toSszTx(tx)) - Hash32(sszTxs.hash_tree_root().data) - -proc computeWithdrawalsRootFromRlp*(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)) - Hash32(sszWds.hash_tree_root().data) - -proc computeBlockHashSsz*(blk: Block_RLP): Hash32 = - ## EIP-7807: Compute SSZ-based block hash from RLP block - let sszHeader = toSszHeader(blk.header) - Hash32(sszHeader.hash_tree_root().data) - -proc computeBlockHashSsz*(header: Header_RLP): Hash32 = - ## EIP-7807: Compute SSZ-based block hash from RLP header - let sszHeader = toSszHeader(header) - Hash32(sszHeader.hash_tree_root().data) diff --git a/eth/ssz/sszcodec.nim b/eth/ssz/sszcodec.nim index d0fb719a..427a0b5f 100644 --- a/eth/ssz/sszcodec.nim +++ b/eth/ssz/sszcodec.nim @@ -4,8 +4,31 @@ import ./signatures, ./transaction_ssz as ssz_tx, ./transaction_builder, - ../common/[addresses_rlp, base_rlp], - ../common/transactions as rlp_tx_mod + ../common/[addresses_rlp, base_rlp,blocks], + ../common/transactions as rlp_tx_mod, + ./blocks_ssz, + 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 = @@ -398,3 +421,19 @@ proc toOldTx*(tx: ssz_tx.Transaction): rlp_tx_mod.Transaction = 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_ssz.nim b/eth/ssz/transaction_ssz.nim index d3168671..d2c99332 100644 --- a/eth/ssz/transaction_ssz.nim +++ b/eth/ssz/transaction_ssz.nim @@ -2,9 +2,8 @@ import ssz_serialization, stint, ../common/[addresses, base, hashes], ./signatures, - ./adapter, - serialization/case_objects - + ./adapter + export adapter type SignedTx*[P] = object diff --git a/tests/ssz/block.nim b/tests/ssz/block.nim new file mode 100644 index 00000000..e1dd1aa6 --- /dev/null +++ b/tests/ssz/block.nim @@ -0,0 +1,170 @@ +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.. 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/block_rt.nim b/tests/ssz/block_rt.nim deleted file mode 100644 index 3259a811..00000000 --- a/tests/ssz/block_rt.nim +++ /dev/null @@ -1,325 +0,0 @@ -import - std/[os, strutils, json], - stew/[byteutils, io2], - ssz_serialization, - ../../eth/[common, rlp], - ../../eth/common/transactions as rlp_tx_mod, - ../../eth/ssz/[sszcodec,adapter,blocks_ssz,blocks_ssz_adapter], - ../../eth/ssz/transaction_ssz as ssz_tx, - unittest2 - -proc eip2718Dir*(): string = - (currentSourcePath.parentDir / ".." / "common" / "eip2718").normalizedPath - -proc rlpsDir*(): string = - (currentSourcePath.parentDir / ".." / "common" / "rlps").normalizedPath - -proc listEip2718Files*(): seq[string] = - let dir = eip2718Dir() - if not dir.dirExists: return @[] - for kind, path in walkDir(dir): - if kind == pcFile and path.endsWith(".json"): - result.add path - -proc eip2718FilePath*(index: int): string = - eip2718Dir() / ("acl_block_" & $index & ".json") - -proc listSelectedEip2718Files*(indices: openArray[int]): seq[string] = - for i in indices: - let p = eip2718FilePath(i) - if p.fileExists: - result.add p - -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 loadEip2718BlockFromFile*(path: string): EthBlock = - let n = json.parseFile(path) - if not n.hasKey("rlp"): - raise newException(ValueError, "JSON has no 'rlp' key: " & path) - let hexRlp = n["rlp"].getStr() - let bytes = hexToSeqByte(hexRlp) - rlp.decode(bytes, EthBlock) - -# Extract eth/common transactions from a JSON fixture -proc loadEip2718TransactionsFromFile*(path: string): seq[rlp_tx_mod.Transaction] = - let blk = loadEip2718BlockFromFile(path) - blk.transactions - -# Extract transactions and their RLP bytes from a JSON fixture -proc loadEip2718TransactionsWithRlp*(path: string): seq[tuple[tx: rlp_tx_mod.Transaction, rlp: seq[byte]]] = - for tx in loadEip2718TransactionsFromFile(path): - result.add (tx: tx, rlp: rlp.encode(tx)) - -# Convert eth/common transactions to SSZ transactions -proc toSszTransactions*(txs: seq[rlp_tx_mod.Transaction]): seq[ssz_tx.Transaction] = - for tx in txs: - result.add toSszTx(tx) - -# From a JSON fixture file, produce SSZ txs and their SSZ encodings -proc loadEip2718SszTransactionsWithSsz*(path: string): seq[tuple[tx: ssz_tx.Transaction, ssz: seq[byte]]] = - let rlpTxs = loadEip2718TransactionsFromFile(path) - for stx in toSszTransactions(rlpTxs): - result.add (tx: stx, ssz: SSZ.encode(stx)) - -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 - -# Load all EIP-2718 JSON fixtures (acl_block_*.json) and decode their RLP into EthBlock objects -proc loadEip2718Blocks*(): seq[EthBlock] = - let dir = eip2718Dir() - if not dir.dirExists: return @[] - for kind, path in walkDir(dir): - if kind == pcFile and path.endsWith(".json"): - try: - result.add loadEip2718BlockFromFile(path) - except CatchableError: - discard - - -suite "SSZ Block Roundtrip ": - test "All blocks: RLP → SSZ → RLP preserves data": - for i in 0..9: # Test blocks 0-9 - let path = eip2718FilePath(i) - if not path.fileExists: - continue - - echo "Testing block ", i, ": ", path - let rlpBlock = loadEip2718BlockFromFile(path) - let sszBlock = toSszBlock(rlpBlock) - # Convert back to common/block - let reconstructed = fromSszBlock(sszBlock) - - # Verify critical fields preserved - check reconstructed.header.number == rlpBlock.header.number - check reconstructed.header.parentHash == rlpBlock.header.parentHash - check reconstructed.header.stateRoot == rlpBlock.header.stateRoot - check reconstructed.header.gasUsed == rlpBlock.header.gasUsed - check reconstructed.header.gasLimit == rlpBlock.header.gasLimit - check reconstructed.transactions.len == rlpBlock.transactions.len - check reconstructed.uncles.len == rlpBlock.uncles.len - - # wont be able to do a full block rt as the opt[withdrawals] may not match ssz requirements - test "All blocks: SSZ → bytes → SSZ preserves data": - for i in 0..9: - let path = eip2718FilePath(i) - if not path.fileExists: - continue - - echo "Testing SSZ serialization for block ", i, ": ", path - let rlpBlock = loadEip2718BlockFromFile(path) - let sszBlock = toSszBlock(rlpBlock) - - let headerBytes = SSZ.encode(sszBlock.header) - let decodedHeader = SSZ.decode(headerBytes, Header_SSZ) - - check decodedHeader.number == sszBlock.header.number - check decodedHeader.parent_hash == sszBlock.header.parent_hash - check decodedHeader.state_root == sszBlock.header.state_root - - test "Individual block tests": - let path = eip2718FilePath(9) - if path.fileExists: - echo "Testing individual block 9: ", path - let rlpBlock = loadEip2718BlockFromFile(path) - let sszBlock = toSszBlock(rlpBlock) - # Convert back to common/block - let reconstructed = fromSszBlock(sszBlock) - # Verify critical fields preserved - check reconstructed.header.number == rlpBlock.header.number - check reconstructed.header.parentHash == rlpBlock.header.parentHash - check reconstructed.header.stateRoot == rlpBlock.header.stateRoot - check reconstructed.header.gasUsed == rlpBlock.header.gasUsed - check reconstructed.header.gasLimit == rlpBlock.header.gasLimit - check reconstructed.transactions.len == rlpBlock.transactions.len - check reconstructed.uncles.len == rlpBlock.uncles.len - -# suite "SSZ Root Computation (EIP-6404, 6465)": -# test "Block 9: Transaction root computation (EIP-6404)": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) - -# # Compute SSZ transactions root -# let txRoot = computeTransactionsRootFromRlp(rlpBlock.transactions) - -# # Should be non-zero -# check txRoot != default(Root) - -# # Should be stable (same result twice) -# let txRoot2 = computeTransactionsRootFromRlp(rlpBlock.transactions) -# check txRoot == txRoot2 - -# echo " TX Root: 0x", txRoot.data.toHex[0..15], "..." - -# test "Block 9: Withdrawals root (EIP-6465)": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) - -# # Compute SSZ withdrawals root -# let wdRoot = computeWithdrawalsRootFromRlp(rlpBlock.withdrawals) - -# # Block 9 has no withdrawals -# if rlpBlock.withdrawals.isNone: -# check wdRoot == default(Root) -# else: -# check wdRoot != default(Root) - -# echo " WD Root: 0x", wdRoot.data.toHex[0..15], "..." - -# test "Block 9: Root consistency check": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) -# let sszBlock = toSszBlock(rlpBlock) - -# # Direct computation -# let directTxRoot = computeTransactionsRootFromRlp(rlpBlock.transactions) - -# # From SSZ block -# let blockTxRoot = block_ssz.computeTransactionsRoot(sszBlock.transactions) - -# # Should match -# check directTxRoot == blockTxRoot - -# test "Transaction root: Order matters": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) - -# if rlpBlock.transactions.len >= 2: -# # Original order -# let root1 = computeTransactionsRootFromRlp(rlpBlock.transactions) - -# # Reversed order -# var reversed = rlpBlock.transactions -# reversed.reverse() -# let root2 = computeTransactionsRootFromRlp(reversed) - -# # Should be different -# check root1 != root2 - -# suite "SSZ Block Hash (EIP-7807)": -# test "Block 9: SSZ hash differs from RLP hash": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) - -# # OLD: RLP-based hash -# let rlpHash = rlp.computeRlpHash(rlpBlock.header) - -# # NEW: SSZ-based hash (EIP-7807) -# let sszHash = computeBlockHashSsz(rlpBlock) - -# # They SHOULD be different (this is the point of EIP-7807!) -# check rlpHash != sszHash - -# echo " RLP hash: 0x", rlpHash.data.toHex[0..15], "..." -# echo " SSZ hash: 0x", sszHash.data.toHex[0..15], "..." - -# test "Block 9: SSZ hash is stable": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) - -# # Compute multiple times -# let hash1 = computeBlockHashSsz(rlpBlock) -# let hash2 = computeBlockHashSsz(rlpBlock) -# let hash3 = computeBlockHashSsz(rlpBlock.header) - -# # All should be identical -# check hash1 == hash2 -# check hash2 == hash3 - -# test "Block 9: SSZ hash from header matches block": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) - -# # From full block -# let hashFromBlock = computeBlockHashSsz(rlpBlock) - -# # From header only -# let hashFromHeader = computeBlockHashSsz(rlpBlock.header) - -# # Should match -# check hashFromBlock == hashFromHeader - -# test "All blocks: Compare RLP vs SSZ hashes": -# let files = listEip2718Files() -# var differCount = 0 - -# for path in files: -# try: -# let rlpBlock = loadEip2718BlockFromFile(path) -# let comparison = compareHashMethods(rlpBlock) - -# # All should differ (EIP-7807 changes hash computation) -# if comparison.rlpHash != comparison.sszHash: -# inc differCount -# except: -# discard - -# echo " Blocks with different hashes: ", differCount -# check differCount > 0 # At least some should differ - - -# suite "SSZ Block Data Integrity": -# test "Block 9: Transaction count preserved": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) -# let sszBlock = toSszBlock(rlpBlock) -# let reconstructed = fromSszBlock(sszBlock) - -# check rlpBlock.transactions.len == 10 -# check sszBlock.transactions.len == 10 -# check reconstructed.transactions.len == 10 - -# test "Block 9: Uncle count preserved": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) -# let sszBlock = toSszBlock(rlpBlock) -# let reconstructed = fromSszBlock(sszBlock) - -# check rlpBlock.uncles.len == sszBlock.uncles.len -# check sszBlock.uncles.len == reconstructed.uncles.len - -# test "Block 9: Withdrawal presence preserved": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) -# let sszBlock = toSszBlock(rlpBlock) -# let reconstructed = fromSszBlock(sszBlock) - -# check rlpBlock.withdrawals.isNone == reconstructed.withdrawals.isNone - -# if rlpBlock.withdrawals.isSome and reconstructed.withdrawals.isSome: -# check rlpBlock.withdrawals.get.len == reconstructed.withdrawals.get.len - -# test "Block 9: Header timestamp preserved": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) -# let sszHeader = toSszHeader(rlpBlock.header) -# let reconstructed = fromSszHeader(sszHeader) - -# check reconstructed.timestamp == rlpBlock.header.timestamp - -# test "Block 9: Parent hash preserved": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) -# let sszHeader = toSszHeader(rlpBlock.header) -# let reconstructed = fromSszHeader(sszHeader) - -# check reconstructed.parentHash == rlpBlock.header.parentHash - -# test "Block 9: State root preserved": -# let path = eip2718FilePath(9) -# let rlpBlock = loadEip2718BlockFromFile(path) -# let sszHeader = toSszHeader(rlpBlock.header) -# let reconstructed = fromSszHeader(sszHeader) - -# check reconstructed.stateRoot == rlpBlock.header.stateRoot From 5a0329bdbe50f732346d1017c58b85c2596abfe1 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Tue, 14 Oct 2025 03:01:52 +0530 Subject: [PATCH 04/10] add new ssz receipts helpers --- eth/ssz/adapter.nim | 18 +- eth/ssz/blocks_ssz.nim | 59 ++- eth/ssz/receipts.nim | 57 --- eth/ssz/receipts_ssz.nim | 103 +++++ eth/ssz/sszcodec.nim | 44 +- eth/ssz/transaction_builder.nim | 87 ++-- eth/ssz/transaction_ssz.nim | 66 ++- tests/ssz/block.nim | 23 +- tests/ssz/receipts.nim | 717 +++++++++++------------------- tests/ssz/transaction_builder.nim | 102 ++--- tests/ssz/transaction_codec.nim | 76 ++-- tests/ssz/transaction_ssz.nim | 130 +++--- tests/ssz/types.nim | 56 ++- 13 files changed, 673 insertions(+), 865 deletions(-) delete mode 100644 eth/ssz/receipts.nim create mode 100644 eth/ssz/receipts_ssz.nim diff --git a/eth/ssz/adapter.nim b/eth/ssz/adapter.nim index ffba1c68..f3e2dd72 100644 --- a/eth/ssz/adapter.nim +++ b/eth/ssz/adapter.nim @@ -1,11 +1,11 @@ {.push raises: [].} import - ../common/[addresses, hashes, base], - std/[typetraits], - ssz_serialization, - ssz_serialization/codec, - ssz_serialization/merkleization + ../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 @@ -14,16 +14,16 @@ export ssz_serialization, codec, base, typetraits # SSZ for Address template toSszType*(T: Address): auto = - distinctBase(T) + distinctBase(T) -func fromSszBytes*( T: type Address, bytes: openArray[byte]): T {.raises: [SszError].} = +func fromSszBytes*(T: type Address, bytes: openArray[byte]): T {.raises: [SszError].} = readSszValue(bytes, distinctBase(result)) # SSZ for Hash32 -template toSszType*(T: Hash32): auto = +template toSszType*(T: Hash32): auto = distinctBase(T) -func fromSszBytes*( T: type Hash32, bytes: openArray[byte]): T {.raises: [SszError].} = +func fromSszBytes*(T: type Hash32, bytes: openArray[byte]): T {.raises: [SszError].} = readSszValue(bytes, distinctBase(result)) # SSZ for Bytes32 diff --git a/eth/ssz/blocks_ssz.nim b/eth/ssz/blocks_ssz.nim index 1779a128..12e5bcd7 100644 --- a/eth/ssz/blocks_ssz.nim +++ b/eth/ssz/blocks_ssz.nim @@ -1,36 +1,28 @@ -import - ssz_serialization, - ./adapter, - ../common/[addresses, hashes], - ./transaction_ssz +import ssz_serialization, ./adapter, ../common/[addresses, hashes], ./transaction_ssz const # Post-merge constants - EMPTY_OMMERS_HASH* = hash32"1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + 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 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 GasAmounts* {.sszActiveFields: [1, 1].} = object + regular*: uint64 + blob*: uint64 -type - BlobFeesPerGas* {.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 - ].} = object + Header* {.sszActiveFields: [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 @@ -43,25 +35,25 @@ type extra_data*: seq[uint8] mix_hash*: Bytes32 base_fees_per_gas*: BlobFeesPerGas - withdrawals_root*: Root # EIP-6465 hash_tree_root + withdrawals_root*: Root # EIP-6465 hash_tree_root excess_gas*: GasAmounts parent_beacon_block_root*: Root - requests_hash*: Bytes32 # EIP-6110 hash_tree_root + requests_hash*: Bytes32 # EIP-6110 hash_tree_root # Note: Field 16 (system_logs_root) not yet in use BlockBody* = object - transactions*: seq[Transaction] - uncles*: seq[Header] - withdrawals*: Opt[seq[Withdrawal]] # EIP-4895 + transactions*: seq[Transaction] + uncles*: seq[Header] + withdrawals*: Opt[seq[Withdrawal]] # EIP-4895 Block* = object - header* : Header + header*: Header transactions*: seq[Transaction] - uncles* : seq[Header] - withdrawals*: Opt[seq[Withdrawal]] # EIP-4895 + uncles*: seq[Header] + withdrawals*: Opt[seq[Withdrawal]] # EIP-4895 -const - EMPTY_UNCLE_HASH* = hash32"1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" +const EMPTY_UNCLE_HASH* = + hash32"1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" func init*(T: type Block, header: Header, body: BlockBody): T = T( @@ -73,4 +65,3 @@ func init*(T: type Block, header: Header, body: BlockBody): T = template txs*(blk: Block): seq[Transaction] = blk.transactions - diff --git a/eth/ssz/receipts.nim b/eth/ssz/receipts.nim deleted file mode 100644 index cc82eae2..00000000 --- a/eth/ssz/receipts.nim +++ /dev/null @@ -1,57 +0,0 @@ -import - ssz_serialization, - ./adapter, - ../common/[addresses, hashes] - -const MAX_TOPICS_PER_LOG* = 4 - -type - GasAmount* = uint64 - - Log* = object - address*: Address - topics*: List[Hash32, MAX_TOPICS_PER_LOG] - data*: seq[byte] - - BasicReceipt* = object - `from`*: Address - gas_used*: GasAmount - contract_address*: Address - logs*: seq[Log] - status*: bool - - CreateReceipt* = object - `from`*: Address - gas_used*: GasAmount - contract_address*: Address - logs*: seq[Log] - status*: bool - - SetCodeReceipt* = object - `from`*: Address - gas_used*: GasAmount - contract_address*: Address - logs*: seq[Log] - status*: bool - authorities*: seq[Address] - - #Run time ->ssz + collections - ReceiptKind* {.pure.} = 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) diff --git a/eth/ssz/receipts_ssz.nim b/eth/ssz/receipts_ssz.nim new file mode 100644 index 00000000..a596f340 --- /dev/null +++ b/eth/ssz/receipts_ssz.nim @@ -0,0 +1,103 @@ +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* = object + `from`*: Address + gas_used*: GasAmount + contract_address*: Address + logs*: seq[Log] + status*: bool + + CreateReceipt* = object + `from`*: Address + gas_used*: GasAmount + contract_address*: Address + logs*: seq[Log] + status*: bool + + SetCodeReceipt* = object + `from`*: Address + gas_used*: GasAmount + contract_address*: Address + 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, + contract_address: contractAddress, + 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, + contract_address: default(Address), + logs: sszLogs, + status: rec.status, + ) + return sszRec.toReceipt() diff --git a/eth/ssz/sszcodec.nim b/eth/ssz/sszcodec.nim index 427a0b5f..ef4938b4 100644 --- a/eth/ssz/sszcodec.nim +++ b/eth/ssz/sszcodec.nim @@ -1,15 +1,14 @@ import std/[sequtils], stint, - ./signatures, - ./transaction_ssz as ssz_tx, - ./transaction_builder, - ../common/[addresses_rlp, base_rlp,blocks], + ./[signatures, blocks_ssz, transaction_builder], + ../common/[addresses_rlp, base_rlp, blocks], ../common/transactions as rlp_tx_mod, - ./blocks_ssz, + ../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 @@ -19,7 +18,7 @@ proc toSszWithdrawal*(w: Withdrawal_RLP): Withdrawal_SSZ = index: w.index, validatorIndex: w.validatorIndex, address: w.address, - amount: w.amount + amount: w.amount, ) proc fromSszWithdrawal*(w: Withdrawal_SSZ): Withdrawal_RLP = @@ -27,7 +26,7 @@ proc fromSszWithdrawal*(w: Withdrawal_SSZ): Withdrawal_RLP = index: w.index, validatorIndex: w.validatorIndex, address: w.address, - amount: w.amount + amount: w.amount, ) # Gas -> FeePerGas @@ -63,11 +62,12 @@ proc toAuthTuples*(al: seq[rlp_tx_mod.Authorization]): seq[ssz_tx.AuthTuple] = nonce: uint64(a.nonce), y_parity: a.yParity, r: a.r, - s: a.s + s: a.s, ) -proc toSszSignedAuthList*(al: seq[rlp_tx_mod.Authorization]): - seq[ssz_tx.Authorization] = +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 = @@ -75,10 +75,8 @@ proc toSszSignedAuthList*(al: seq[rlp_tx_mod.Authorization]): ssz_tx.AuthorizationPayload( kind: ssz_tx.authReplayableBasic, replayable: ssz_tx.RlpReplayableBasicAuthorizationPayload( - magic: ssz_tx.AuthMagic7702, - address: a.address, - nonce: uint64(a.nonce), - ) + magic: ssz_tx.AuthMagic7702, address: a.address, nonce: uint64(a.nonce) + ), ) else: ssz_tx.AuthorizationPayload( @@ -88,17 +86,13 @@ proc toSszSignedAuthList*(al: seq[rlp_tx_mod.Authorization]): 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 - ) + result[i] = ssz_tx.Authorization(payload: payload, signature: sig) -proc toSszAuthList*(al: seq[rlp_tx_mod.Authorization]): - seq[ssz_tx.Authorization] = +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] = @@ -114,7 +108,7 @@ proc toRlpAuthList*(al: seq[ssz_tx.Authorization]): seq[rlp_tx_mod.Authorization nonce: AccountNonce(p.nonce), yParity: parity, r: R, - s: S + s: S, ) of ssz_tx.authBasic: let p = a.payload.basic @@ -124,10 +118,9 @@ proc toRlpAuthList*(al: seq[ssz_tx.Authorization]): seq[rlp_tx_mod.Authorization nonce: AccountNonce(p.nonce), yParity: parity, r: R, - s: S + s: S, ) - proc packSigFromTx(tx: rlp_tx_mod.Transaction): Secp256k1ExecutionSignature = let y: uint8 = case tx.txType @@ -436,4 +429,3 @@ proc computeWithdrawalsRootSsz*(withdrawals: Opt[seq[Withdrawal_RLP]]): Root = 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 index ba5a6723..c8fe8093 100644 --- a/eth/ssz/transaction_builder.nim +++ b/eth/ssz/transaction_builder.nim @@ -1,8 +1,4 @@ -import - stint, - ./transaction_ssz, - ./signatures, - ../common/[addresses, base, hashes] +import stint, ./transaction_ssz, ./signatures, ../common/[addresses, base, hashes] type TxBuildError* = object of ValueError @@ -15,26 +11,18 @@ proc makeAuthorization*(t: AuthTuple): Authorization = AuthorizationPayload( kind: authReplayableBasic, replayable: RlpReplayableBasicAuthorizationPayload( - magic: AuthMagic7702, - address: t.address, - nonce: t.nonce - ) + 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 - ) + 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) - ) + Authorization(payload: payload, signature: secp256k1Pack(t.r, t.s, t.y_parity)) proc makeAuthorizationList*(xs: openArray[AuthTuple]): seq[Authorization] = result = newSeqOfCap[Authorization](xs.len) @@ -52,16 +40,34 @@ template BuildWrap( 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( + 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) +BuildWrap( + RlpLegacyReplayableBasicTransactionPayload, RlpLegacyReplayableBasicTransaction, + txLegacyReplayableBasic, legacyReplayableBasic, +) +BuildWrap( + RlpLegacyReplayableCreateTransactionPayload, RlpLegacyReplayableCreateTransaction, + txLegacyReplayableCreate, legacyReplayableCreate, +) proc Transaction*( txType: uint8, @@ -77,7 +83,7 @@ proc Transaction*( access_list: seq[AccessTuple] = @[], blob_versioned_hashes: seq[VersionedHash] = @[], blob_fee: FeePerGas = 0.u256, - authorization_list: seq[AuthTuple] = @[], + authorization_list: seq[AuthTuple] = @[], ): Transaction = let auths = makeAuthorizationList(authorization_list) case txType @@ -183,21 +189,21 @@ proc Transaction*( ) 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) + 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, @@ -215,4 +221,3 @@ proc Transaction*( 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 index d2c99332..1448adb9 100644 --- a/eth/ssz/transaction_ssz.nim +++ b/eth/ssz/transaction_ssz.nim @@ -1,9 +1,6 @@ import - ssz_serialization, stint, - ../common/[addresses, base, hashes], - ./signatures, - ./adapter - + ssz_serialization, stint, ../common/[addresses, base, hashes], ./signatures, ./adapter + export adapter type SignedTx*[P] = object @@ -141,22 +138,18 @@ type RlpBlobTransactionPayload* {.sszActiveFields: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1 blob_versioned_hashes*: seq[VersionedHash] type - RlpReplayableBasicAuthorizationPayload* {. - sszActiveFields: [1, 0, 1, 1] - .} = object - magic*: TransactionType # 0x05 (Auth) + 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) + RlpBasicAuthorizationPayload* {.sszActiveFields: [1, 1, 1, 1].} = object + magic*: TransactionType # 0x05 (Auth) chain_id*: ChainId address*: Address nonce*: uint64 - AuthorizationKind* = enum + AuthorizationKind* = enum authReplayableBasic authBasic @@ -208,19 +201,19 @@ type # 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 + RLPTransactionKind* = enum + txLegacyReplayableBasic = 0 + txLegacyReplayableCreate = 1 + txLegacyBasic = 2 + txLegacyCreate = 3 + txAccessListBasic = 4 + txAccessListCreate = 5 + txBasic = 6 + txCreate = 7 + txBlob = 8 + txSetCode = 9 - RlpTransactionObject* = object + RlpTransactionObject* = object case kind*: RLPTransactionKind of txLegacyReplayableBasic: legacyReplayableBasic*: RlpLegacyReplayableBasicTransaction @@ -245,8 +238,8 @@ type type TransactionKind* {.pure.} = enum - TxNone=0 - RlpTransaction=1 + TxNone = 0 + RlpTransaction = 1 Transaction* = object case kind*: TransactionKind @@ -256,11 +249,12 @@ type 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 +type AuthTuple* = + tuple[ + chain_id: ChainId, + address: Address, + nonce: uint64, + y_parity: uint8, + r: UInt256, + s: UInt256, + ] diff --git a/tests/ssz/block.nim b/tests/ssz/block.nim index e1dd1aa6..bf75ef98 100644 --- a/tests/ssz/block.nim +++ b/tests/ssz/block.nim @@ -3,8 +3,8 @@ import stew/[byteutils, io2], ssz_serialization, ../../eth/[common, rlp], - ../../eth/ssz/[sszcodec,adapter,blocks_ssz,transaction_ssz], - unittest2 + ../../eth/ssz/[sszcodec, adapter, blocks_ssz, transaction_ssz], + unittest2 type TxSSZ = transaction_ssz.Transaction @@ -29,7 +29,8 @@ proc loadBlockFromFile*(path: string): EthBlock = proc listRlpFiles*(): seq[string] = let dir = rlpsDir() - if not dir.dirExists: return @[] + if not dir.dirExists: + return @[] for kind, path in walkDir(dir): if kind == pcFile and path.endsWith(".rlp"): result.add path @@ -44,7 +45,6 @@ proc loadRlpBlocksFromFile*(path: string, limit: int = 0): seq[EthBlock] = result.add r.read(EthBlock) inc taken - suite "Transaction List Roundtrip": test "Block 9: RLP → SSZ → RLP preserves all transactions": let path = eip2718FilePath(9) @@ -62,7 +62,7 @@ suite "Transaction List Roundtrip": check reconstructed.len == originalTxs.len # Verify each transaction - for i in 0.. rlp to receipts ssz - -# suite "SSZ Debug Tests": -# test "Test individual receipt components": -# echo "=== Testing individual SSZ components ===" - -# echo "Testing Address SSZ..." -# try: -# let address = addresses.zeroAddress -# let addrHash = hash_tree_root(address) -# # echo "Address hash_tree_root successful: ", addrHash.to0xHex() -# except Exception as e: -# echo "ERROR with Address SSZ: ", e.msg -# echo "Exception type: ", $e.name - - # echo "Testing Log SSZ..." - # try: - # let log = Log( - # address: addresses.zeroAddress, - # topics: List[Hash32, MAX_TOPICS_PER_LOG](@[]), - # data: @[], - # ) - # let logHash = hash_tree_root(log) - # # echo "Log hash_tree_root successful: ", logHash.to0xHex() - # except Exception as e: - # echo "ERROR with Log SSZ: ", e.msg - # echo "Exception type: ", $e.name - - # echo "Testing BasicReceipt SSZ..." - # try: - # let basicReceipt = BasicReceipt( - # `from`: addresses.zeroAddress, - # gas_used: 21_000'u64, - # contract_address: addresses.zeroAddress, - # logs: @[], - # status: true, - # ) - # let basicHash = hash_tree_root(basicReceipt) - # echo "BasicReceipt hash_tree_root successful: ", basicHash.to(Hash32).to0xHex() - # except Exception as e: - # echo "ERROR with BasicReceipt SSZ: ", e.msg - # echo "Exception type: ", $e.name - - # echo "Testing Receipt variant SSZ..." - # try: - # let receipt = toReceipt(BasicReceipt( - # `from`: addresses.zeroAddress, - # gas_used: 21_000'u64, - # contract_address: addresses.zeroAddress, - # logs: @[], - # status: true, - # )) - # # echo "Receipt created with kind: ", receipt.kind - # # let receiptHash = hash_tree_root(receipt) - # # echo "Receipt hash_tree_root successful: ", receiptHash.to0xHex() - # except Exception as e: - # echo "ERROR with Receipt variant SSZ: ", e.msg - # echo "Exception type: ", $e.name -# suite "Block receipts root (SSZ)": -# test "receipts root for 3 receipts: non-zero and stable": -# echo "Creating BasicReceipt..." -# let r0 = toReceipt( -# BasicReceipt( -# `from`: addresses.zeroAddress, -# gas_used: 21_000'u64, -# contract_address: addresses.zeroAddress, -# logs: @[], -# status: true, -# ) -# ) -# # echo "BasicReceipt created: ", r0.kind - - -# # echo "Creating CreateReceipt..." -# # let r1 = toReceipt( -# # CreateReceipt( -# # `from`: address"0x0000000000000000000000000000000000000001", -# # gas_used: 42_000'u64, -# # contract_address: address"0x00000000000000000000000000000000000000aa", -# # logs: @[], -# # status: false, -# # ) -# # ) -# # echo "CreateReceipt created: ", r1.kind - -# echo "Creating SetCodeReceipt..." -# let r2 = toReceipt( -# SetCodeReceipt( -# `from`: address"0x00000000000000000000000000000000000000bb", -# gas_used: 63_000'u64, -# contract_address: address"0x00000000000000000000000000000000000000cc", -# logs: @[], -# status: true, -# authorities: @[address"0x00000000000000000000000000000000000000f1"], -# ) -# ) -# # echo "SetCodeReceipt created: ", r2.kind + 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], + ) + ) -# var receipts: seq[Receipt] = @[r0, r2] -# # echo "Created receipts sequence with ", receipts.len, " items" + 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], + ) + ) -# echo "Attempting to compute hash_tree_root..." -# let root1 = hash_tree_root(receipts) - # check root1 != hashes.zeroHash32 - # echo "receipts_root: ", root1.to(Hash32).to0xHex() + 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, + contract_address: addresses.zeroAddress, + logs: @[], + status: true, + ) - # let root2 = hash_tree_root(receipts2) - # check root2 == root1 + testRT "Basic receipt data", + ( + block: + let log0 = Log(address: default(Address), topics: topicList(), data: @[]) + BasicReceipt( + `from`: default(Address), + gas_used: 100'u64, + contract_address: default(Address), + logs: @[log0], + status: true, + ) + ): + check v.gas_used == 100'u64 + check v.status == true + check v.contract_address == default(Address) + 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, + contract_address: address"0x00000000000000000000000000000000000000ee", + 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, + contract_address: addresses.zeroAddress, + 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, + contract_address: address"0x00000000000000000000000000000000000000cc", + 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, contract_address: default(Address), logs: @[], status: true), - BasicReceipt(`from`: default(Address), gas_used: 2'u64, contract_address: default(Address), logs: @[], status: true) - ] + var receipts = + @[ + BasicReceipt( + `from`: default(Address), + gas_used: 1'u64, + contract_address: default(Address), + logs: @[], + status: true, + ), + BasicReceipt( + `from`: default(Address), + gas_used: 2'u64, + contract_address: default(Address), + logs: @[], + status: true, + ), + ] let rootA = hash_tree_root(receipts) - # mutate gas_used in first receipt 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, contract_address: default(Address), logs: @[], status: true) - let b = BasicReceipt(`from`: default(Address), gas_used: 2'u64, contract_address: default(Address), logs: @[], status: true) + let a = BasicReceipt( + `from`: default(Address), + gas_used: 1'u64, + contract_address: default(Address), + logs: @[], + status: true, + ) + let b = BasicReceipt( + `from`: default(Address), + gas_used: 2'u64, + contract_address: default(Address), + 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 - echo "receipts_root(list1): ", r1.to(Hash32).to0xHex() - echo "receipts_root(list2): ", r2.to(Hash32).to0xHex() -suite "SSZ root ": +suite "SSZ root": test "hash_tree_root for Log": let log = Log( address: addresses.zeroAddress, - topics: List[Hash32, MAX_TOPICS_PER_LOG](@[]), - data: @[] + topics: + topicList(Bytes32.default, Bytes32.default, Bytes32.default, Bytes32.default), + data: @[], ) let root = hash_tree_root(log) - echo "Log root: ", root.to(Hash32).to0xHex() - - - test "hash_tree_root for receipts list (variant)": - let r0 = toReceipt(BasicReceipt( - `from`: addresses.zeroAddress, - gas_used: 21_000'u64, - contract_address: addresses.zeroAddress, - logs: @[], - status: true - )) - let r1 = toReceipt(BasicReceipt( - `from`: address"0x0000000000000000000000000000000000000001", - gas_used: 42_000'u64, - contract_address: address"0x00000000000000000000000000000000000000aa", - logs: @[], - status: false - )) - let r2 = toReceipt(BasicReceipt( - `from`: address"0x00000000000000000000000000000000000000bb", - gas_used: 63_000'u64, - contract_address: address"0x00000000000000000000000000000000000000cc", - logs: @[], - status: true - )) - - hash_tree_root(r1) - let receipts: seq[Receipt] = @[r0, r1, r2] - let root = hash_tree_root(receipts) - echo "Tagged receipts list root: ", root.to(Hash32).to0xHex() test "hash_tree_root for list of Log": let log = Log( address: addresses.zeroAddress, - topics: List[Hash32, MAX_TOPICS_PER_LOG](@[]), - data: @[] + topics: + topicList(Bytes32.default, Bytes32.default, Bytes32.default, Bytes32.default), + data: @[], ) let logs = @[log] let root = hash_tree_root(logs) - # echo "Logs list root: ", root.to0xHex() - echo "Logs list root: ", root.to(Hash32).to0xHex() test "hash_tree_root for BasicReceipt": let r = BasicReceipt( @@ -459,11 +343,9 @@ suite "SSZ root ": gas_used: 100'u64, contract_address: addresses.zeroAddress, logs: @[], - status: true + status: true, ) let root = hash_tree_root(r) -# echo "BasicReceipt root: ", root.to0xHex() - echo "BasicReceipt root: ", root.to(Hash32).to0xHex() test "hash_tree_root for CreateReceipt": let r = CreateReceipt( @@ -471,110 +353,17 @@ suite "SSZ root ": gas_used: 42_000'u64, contract_address: address"0x00000000000000000000000000000000000000aa", logs: @[], - status: false + status: false, ) let root = hash_tree_root(r) - echo "CreateReceipt root: ", root.to(Hash32).to0xHex() - - # test "hash_tree_root for SetCodeReceipt": - # let r = SetCodeReceipt( - # `from`: address"0x00000000000000000000000000000000000000bb", - # gas_used: 63_000'u64, - # contract_address: address"0x00000000000000000000000000000000000000cc", - # logs: @[], - # status: true, - # authorities: @[] - # # authorities: @[address"0x00000000000000000000000000000000000000f1"] - # ) - # let root = hash_tree_root(r) - # echo "SetCodeReceipt root: ", root.to(Hash32).to0xHex() - test "hash_tree_root for treceipts list (variant)": - # Build concrete receipts and convert to the Receipt variant using toReceipt - let r0 = BasicReceipt( - `from`: addresses.zeroAddress, - gas_used: 21_000'u64, - contract_address: addresses.zeroAddress, + test "hash_tree_root for SetCodeReceipt": + let r = SetCodeReceipt( + `from`: address"0x00000000000000000000000000000000000000bb", + gas_used: 63_000'u64, + contract_address: address"0x00000000000000000000000000000000000000cc", logs: @[], - status: true + status: true, + authorities: @[address"0x00000000000000000000000000000000000000f1"], ) - let r1 = BasicReceipt( - `from`: addresses.zeroAddress, - gas_used: 21_000'u64, - contract_address: addresses.zeroAddress, - logs: @[], - status: true - ) - let r2 = BasicReceipt( - `from`: addresses.zeroAddress, - gas_used: 21_000'u64, - contract_address: addresses.zeroAddress, - 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, - contract_address: address"0x00000000000000000000000000000000000000cc", - logs: @[], - status: true, - authorities: @[address"0x00000000000000000000000000000000000000f1"] - )) -# TODO make so we can tkae an arbitary amount of receipts with different kind - var receipts = @[r0, r1, r2] - let root = hash_tree_root(receipts) - echo "Tagged receipts list root: ", root.to(Hash32).to0xHex() - -# Focused tests to demonstrate current toReceipt SSZ behavior -# suite "Receipt variant SSZ behavior": -# proc sampleBasic(): BasicReceipt = -# BasicReceipt( -# `from`: addresses.zeroAddress, -# gas_used: 21_000'u64, -# contract_address: addresses.zeroAddress, -# logs: @[], -# status: true -# ) - -# proc sampleVariant(): Receipt = -# toReceipt(sampleBasic()) - -# test "SSZ.encode on BasicReceipt succeeds": -# let r = sampleBasic() -# let bytes = SSZ.encode(r) -# let r2 = SSZ.decode(bytes, BasicReceipt) -# check r == r2 - - # test "SSZ.encode on Receipt (toReceipt) currently unsupported": - # let rv = sampleVariant() - # when compiles(SSZ.encode(rv)): - # let bytes = SSZ.encode(rv) - # let rv2 = SSZ.decode(bytes, Receipt) - # check bytes.len > 0 - # check rv2.kind == rv.kind - # else: - # check true - - # test "hash_tree_root(Receipt) compile check": - # let rv = sampleVariant() - # when compiles(hash_tree_root(rv)): - # let root = hash_tree_root(rv) - # echo "Receipt variant root: ", root.to(Hash32).to0xHex() - # else: - # check true - - # test "SSZ.encode on seq[Receipt] compile check": - # let seqv = @[sampleVariant(), sampleVariant()] - # when compiles(SSZ.encode(seqv)): - # let bytes = SSZ.encode(seqv) - # let seq2 = SSZ.decode(bytes, type(seqv)) - # check seq2.len == seqv.len - # else: - # check true + let root = hash_tree_root(r) diff --git a/tests/ssz/transaction_builder.nim b/tests/ssz/transaction_builder.nim index 059d59cb..8976b16e 100644 --- a/tests/ssz/transaction_builder.nim +++ b/tests/ssz/transaction_builder.nim @@ -119,39 +119,39 @@ suite "SSZ Transactions (constructor)": check tx.rlp.kind == txCreate check tx.rlp.create.payload.input.len == abcdef.len - when compiles(BlobFeesPerGas): - 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 "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 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), @@ -169,19 +169,21 @@ suite "SSZ Transactions (constructor)": 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 + 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 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), @@ -200,19 +202,3 @@ suite "SSZ Transactions (constructor)": 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 - - test "7702 SetCode: fails when auth list empty": - expect(TxBuildError): - discard Transaction( - txType = 0x04'u8, - chain_id = ChainId(1.u256), - nonce = 11'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), - authorization_list = @[], - signature = dummySig(), - ) diff --git a/tests/ssz/transaction_codec.nim b/tests/ssz/transaction_codec.nim index 181bb6d3..3340363e 100644 --- a/tests/ssz/transaction_codec.nim +++ b/tests/ssz/transaction_codec.nim @@ -2,20 +2,20 @@ import unittest, stew/byteutils, std/sequtils, - ../../eth/ssz/[sszcodec,transaction_ssz, transaction_builder, signatures, adapter], + ../../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") + 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) @@ -64,18 +64,17 @@ suite "SSZ Transactions (full round-trip)": test "Dynamic Fee Tx": sszRoundTrip(tx5, 6) -# Will never work as blob Txns must have a To + # 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 + # 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) @@ -145,7 +144,8 @@ suite "Transactions SSZ Codec: 7702 SetCode (RLP ↔ SSZ)": let sszTx = toSszTx(tx) check sszTx.rlp.setCode.payload.authorization_list.len == 1 - check sszTx.rlp.setCode.payload.authorization_list[0].payload.kind == authReplayableBasic + 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 @@ -154,13 +154,13 @@ suite "Transactions SSZ Codec: 7702 SetCode (RLP ↔ SSZ)": 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 - ) + 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 @@ -177,15 +177,15 @@ suite "Transactions SSZ Codec: 7702 SetCode (RLP ↔ SSZ)": test "7702 with multiple authorizations (5 entries)": var tx = txEip7702(1) - for i in 1..4: + 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) - ) + 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 @@ -194,11 +194,9 @@ suite "Transactions SSZ Codec: 7702 SetCode (RLP ↔ SSZ)": check backTx.authorizationList.len == 5 check backTx == tx - for i in 0..4: + 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 @@ -234,7 +232,6 @@ suite "Transactions SSZ Codec: 7702 SetCode (RLP ↔ SSZ)": check backTx.authorizationList.len == tx.authorizationList.len check backTx == tx - suite "Transactions SSZ Codec: Double Roundtrip": test "Legacy Call: double roundtrip": sszDoubleRoundTrip(tx0, 1) @@ -257,16 +254,16 @@ suite "Transactions SSZ Codec: Double Roundtrip": 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 - ] + 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: @@ -274,14 +271,13 @@ suite "Transactions SSZ Codec: Mixed Transaction Lists": 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.. Date: Fri, 17 Oct 2025 23:39:57 +0530 Subject: [PATCH 05/10] Add new StoredReceipt variants --- eth/common/receipts.nim | 26 ++++++++++++-- eth/common/receipts_rlp.nim | 37 ++++++++++++++++++-- eth/common/transactions.nim | 1 + eth/common/transactions_rlp.nim | 24 ++++++++----- tests/common/test_receipts.nim | 61 +++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 13 deletions(-) diff --git a/eth/common/receipts.nim b/eth/common/receipts.nim index 659f9c12..60a70695 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,18 @@ 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 : @[], + 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/tests/common/test_receipts.nim b/tests/common/test_receipts.nim index e8416cf0..f3b16a17 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) From 581184cdc22f0660419410f772129c2610ed3cac Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Thu, 23 Oct 2025 17:30:16 +0530 Subject: [PATCH 06/10] address reviews --- eth/common/receipts.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/eth/common/receipts.nim b/eth/common/receipts.nim index 60a70695..27735de3 100644 --- a/eth/common/receipts.nim +++ b/eth/common/receipts.nim @@ -101,6 +101,7 @@ func to*(rec: Receipt, _: type StoredReceipt): StoredReceipt = cumulativeGasUsed : rec.cumulativeGasUsed, 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 From 7bfd271f57ccaafb77f62cc5d8453d3d309c15d3 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Thu, 23 Oct 2025 19:53:05 +0530 Subject: [PATCH 07/10] upstream fixes --- eth/ssz/sszcodec.nim | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/eth/ssz/sszcodec.nim b/eth/ssz/sszcodec.nim index ef4938b4..ae1b729f 100644 --- a/eth/ssz/sszcodec.nim +++ b/eth/ssz/sszcodec.nim @@ -139,8 +139,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = ChainId(0.u256) let accessSSZ = accessListFrom(tx.accessList) - case tx.txType - of rlp_tx_mod.TxLegacy: + if tx.txType == rlp_tx_mod.TxLegacy: return transaction_builder.Transaction( txType = ssz_tx.TxLegacy, chain_id = legacyChain, @@ -152,7 +151,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = max_fees_per_gas = ssz_tx.BasicFeesPerGas(regular: feeFromGas(tx.gasPrice)), signature = sig, ) - of rlp_tx_mod.TxEip2930: + if tx.txType == rlp_tx_mod.TxEip2930: return transaction_builder.Transaction( txType = ssz_tx.TxAccessList, chain_id = tx.chainId, @@ -165,7 +164,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = signature = sig, access_list = accessSSZ, ) - of rlp_tx_mod.TxEip1559: + if tx.txType == rlp_tx_mod.TxEip1559: return transaction_builder.Transaction( txType = ssz_tx.TxDynamicFee, chain_id = tx.chainId, @@ -180,7 +179,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = signature = sig, access_list = accessSSZ, ) - of rlp_tx_mod.TxEip4844: + if tx.txType == rlp_tx_mod.TxEip4844: return transaction_builder.Transaction( txType = ssz_tx.TxBlob, chain_id = tx.chainId, @@ -197,7 +196,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = blob_versioned_hashes = tx.versionedHashes, blob_fee = tx.maxFeePerBlobGas, ) - of rlp_tx_mod.TxEip7702: + if tx.txType == rlp_tx_mod.TxEip7702: if tx.to.isNone: raise newException(ValueError, "7702 setCode: requires 'to'") return transaction_builder.Transaction( @@ -215,6 +214,7 @@ proc toSszTx*(tx: rlp_tx_mod.Transaction): ssz_tx.Transaction = 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: From 697200197d5d42a77121099b7849fe4313404c49 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Sat, 25 Oct 2025 17:51:43 +0530 Subject: [PATCH 08/10] fix el errors --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) 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/ From 3ba8f696486d48ee240d80b98a25446413402452 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Tue, 28 Oct 2025 02:00:49 +0530 Subject: [PATCH 09/10] add systemsLogRoot in header --- eth/common/headers.nim | 1 + eth/ssz/blocks_ssz.nim | 4 ++-- tests/common/test_eth_types_rlp.nim | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/eth/common/headers.nim b/eth/common/headers.nim index 1bc00969..8b84b765 100644 --- a/eth/common/headers.nim +++ b/eth/common/headers.nim @@ -39,6 +39,7 @@ type excessBlobGas*: Opt[uint64] # EIP-4844 parentBeaconBlockRoot*: Opt[Hash32] # EIP-4788 requestsHash*: Opt[Hash32] # EIP-7685 + systemLogsRoot*: Opt[Hash32] # EIP-7685 # starting from EIP-4399, `mixDigest` field is called `prevRandao` template prevRandao*(h: Header): Bytes32 = diff --git a/eth/ssz/blocks_ssz.nim b/eth/ssz/blocks_ssz.nim index 12e5bcd7..7efa0da2 100644 --- a/eth/ssz/blocks_ssz.nim +++ b/eth/ssz/blocks_ssz.nim @@ -22,7 +22,7 @@ type BlobFeesPerGas* {.sszActiveFields: [1, 1].} = object # EIP-7807: Execution Block Header type - Header* {.sszActiveFields: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1].} = object + 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 @@ -39,7 +39,7 @@ type excess_gas*: GasAmounts parent_beacon_block_root*: Root requests_hash*: Bytes32 # EIP-6110 hash_tree_root - # Note: Field 16 (system_logs_root) not yet in use + system_logs_root*: Root BlockBody* = object transactions*: seq[Transaction] diff --git a/tests/common/test_eth_types_rlp.nim b/tests/common/test_eth_types_rlp.nim index ee0200d2..2cdc716b 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] From d4b3f280b5ff93f884627e2195953aebbedb0620 Mon Sep 17 00:00:00 2001 From: Samyxandz Date: Wed, 5 Nov 2025 01:30:17 +0530 Subject: [PATCH 10/10] Eip-6466 bumps --- eth/ssz/receipts_ssz.nim | 10 +++------- tests/ssz/receipts.nim | 12 ------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/eth/ssz/receipts_ssz.nim b/eth/ssz/receipts_ssz.nim index a596f340..2ee213ab 100644 --- a/eth/ssz/receipts_ssz.nim +++ b/eth/ssz/receipts_ssz.nim @@ -15,24 +15,22 @@ type topics*: List[Bytes32, MAX_TOPICS_PER_LOG] data*: seq[byte] - BasicReceipt* = object + BasicReceipt* {.sszActiveFields: [1, 1, 0, 1, 1].} = object `from`*: Address gas_used*: GasAmount - contract_address*: Address logs*: seq[Log] status*: bool - CreateReceipt* = object + CreateReceipt* {.sszActiveFields: [1, 1, 1, 1, 1].} = object `from`*: Address gas_used*: GasAmount contract_address*: Address logs*: seq[Log] status*: bool - SetCodeReceipt* = object + SetCodeReceipt* {.sszActiveFields: [1, 1, 0, 1, 1, 1].} = object `from`*: Address gas_used*: GasAmount - contract_address*: Address logs*: seq[Log] status*: bool authorities*: seq[Address] @@ -77,7 +75,6 @@ proc toSszReceipt*( let sszRec = SetCodeReceipt( `from`: sender, gas_used: gasUsed, - contract_address: contractAddress, logs: sszLogs, status: rec.status, authorities: authorities, @@ -96,7 +93,6 @@ proc toSszReceipt*( let sszRec = BasicReceipt( `from`: sender, gas_used: gasUsed, - contract_address: default(Address), logs: sszLogs, status: rec.status, ) diff --git a/tests/ssz/receipts.nim b/tests/ssz/receipts.nim index 2f2be611..9c313717 100644 --- a/tests/ssz/receipts.nim +++ b/tests/ssz/receipts.nim @@ -156,7 +156,6 @@ suite "Receipts Construction (SSZ)": BasicReceipt( `from`: addresses.zeroAddress, gas_used: 100'u64, - contract_address: addresses.zeroAddress, logs: @[], status: true, ) @@ -168,14 +167,12 @@ suite "Receipts Construction (SSZ)": BasicReceipt( `from`: default(Address), gas_used: 100'u64, - contract_address: default(Address), logs: @[log0], status: true, ) ): check v.gas_used == 100'u64 check v.status == true - check v.contract_address == default(Address) check v.logs.len == 1 testRT "CreateReceipt: no logs", @@ -225,7 +222,6 @@ suite "Receipts Construction (SSZ)": SetCodeReceipt( `from`: address"0x00000000000000000000000000000000000000dd", gas_used: 42000'u64, - contract_address: address"0x00000000000000000000000000000000000000ee", logs: @[log2], status: true, authorities: @@ -246,7 +242,6 @@ suite "Block receipts root (SSZ)": BasicReceipt( `from`: addresses.zeroAddress, gas_used: 21_000'u64, - contract_address: addresses.zeroAddress, logs: @[], status: true, ) @@ -264,7 +259,6 @@ suite "Block receipts root (SSZ)": SetCodeReceipt( `from`: address"0x00000000000000000000000000000000000000bb", gas_used: 63_000'u64, - contract_address: address"0x00000000000000000000000000000000000000cc", logs: @[], status: true, authorities: @[address"0x00000000000000000000000000000000000000f1"], @@ -279,14 +273,12 @@ test "receipts root changes when a receipt changes": BasicReceipt( `from`: default(Address), gas_used: 1'u64, - contract_address: default(Address), logs: @[], status: true, ), BasicReceipt( `from`: default(Address), gas_used: 2'u64, - contract_address: default(Address), logs: @[], status: true, ), @@ -300,14 +292,12 @@ test "receipts root is order-sensitive": let a = BasicReceipt( `from`: default(Address), gas_used: 1'u64, - contract_address: default(Address), logs: @[], status: true, ) let b = BasicReceipt( `from`: default(Address), gas_used: 2'u64, - contract_address: default(Address), logs: @[], status: true, ) @@ -341,7 +331,6 @@ suite "SSZ root": let r = BasicReceipt( `from`: addresses.zeroAddress, gas_used: 100'u64, - contract_address: addresses.zeroAddress, logs: @[], status: true, ) @@ -361,7 +350,6 @@ suite "SSZ root": let r = SetCodeReceipt( `from`: address"0x00000000000000000000000000000000000000bb", gas_used: 63_000'u64, - contract_address: address"0x00000000000000000000000000000000000000cc", logs: @[], status: true, authorities: @[address"0x00000000000000000000000000000000000000f1"],