diff --git a/eth/common/eth_types.nim b/eth/common/eth_types.nim index f281327..14e0f07 100644 --- a/eth/common/eth_types.nim +++ b/eth/common/eth_types.nim @@ -4,7 +4,7 @@ import ../rlp, ../trie/[trie_defs, db] export - stint, read, append, KeccakHash, rlp + stint, read, append, KeccakHash, rlp, options type Hash256* = MDigest[256] @@ -39,21 +39,10 @@ type ChainId* = distinct uint64 Account* = object - nonce*: AccountNonce - balance*: UInt256 - storageRoot*: Hash256 - codeHash*: Hash256 - - LegacyTx* = object - nonce* : AccountNonce - gasPrice*: GasInt - gasLimit*: GasInt - to* {.rlpCustomSerialization.}: EthAddress - value* : UInt256 - payload* : Blob - V* : int64 - R*, S* : UInt256 - isContractCreation* {.rlpIgnore.}: bool + nonce*: AccountNonce + balance*: UInt256 + storageRoot*: Hash256 + codeHash*: Hash256 AccessPair* = object address* : EthAddress @@ -61,29 +50,25 @@ type AccessList* = seq[AccessPair] - AccessListTx* = object - chainId* {.rlpCustomSerialization.}: ChainId - nonce* : AccountNonce - gasPrice* : GasInt - gasLimit* : GasInt - to* {.rlpCustomSerialization.}: EthAddress - value* : UInt256 - payload* : Blob - accessList*: AccessList - V* : int64 - R*, S* : UInt256 - isContractCreation* {.rlpIgnore.}: bool - TxType* = enum - LegacyTxType - AccessListTxType + TxLegacy + TxEip2930 + TxEip1559 Transaction* = object - case txType*: TxType - of LegacyTxType: - legacyTx*: LegacyTx - of AccessListTxType: - accessListTx*: AccessListTx + txType* : TxType # EIP-2718 + chainId* : ChainId # EIP-2930 + nonce* : AccountNonce + gasPrice* : GasInt + maxPriorityFee*: GasInt # EIP-1559 + maxFee* : GasInt # EIP-1559 + gasLimit* : GasInt + to* : Option[EthAddress] + value* : UInt256 + payload* : Blob + accessList* : AccessList # EIP-2930 + V* : int64 + R*, S* : UInt256 TransactionStatus* = enum Unknown, @@ -112,6 +97,8 @@ type extraData*: Blob mixDigest*: Hash256 nonce*: BlockNonce + # `baseFee` is the get/set of `fee` + fee*: Option[UInt256] # EIP-1559 BlockBody* = object transactions*{.rlpCustomSerialization.}: seq[Transaction] @@ -122,35 +109,21 @@ type topics*: seq[Topic] data*: Blob - HashOrStatus* = object - case isHash*: bool - of true: - hash*: Hash256 - else: - status*: bool - - LegacyReceipt* = object - stateRootOrStatus*: HashOrStatus - cumulativeGasUsed*: GasInt - bloom*: BloomFilter - logs* : seq[Log] - - AccessListReceipt* = object - status*: bool - cumulativeGasUsed*: GasInt - bloom* : BloomFilter - logs* : seq[Log] - - ReceiptType* = enum - LegacyReceiptType - AccessListReceiptType + # easily convertible between + # ReceiptType and TxType + ReceiptType* = TxType + # LegacyReceipt = TxLegacy + # Eip2930Receipt = TxEip2930 + # Eip1559Receipt = TxEip1559 Receipt* = object - case receiptType*: ReceiptType - of LegacyReceiptType: - legacyReceipt*: LegacyReceipt - of AccessListReceiptType: - accessListReceipt*: AccessListReceipt + receiptType* : ReceiptType + isHash* : bool # hash or status + status* : bool # EIP-658 + hash* : Hash256 + cumulativeGasUsed*: GasInt + bloom* : BloomFilter + logs* : seq[Log] EthBlock* = object header*: BlockHeader @@ -222,6 +195,11 @@ type OK Error +const + LegacyReceipt* = TxLegacy + Eip2930Receipt* = TxEip2930 + Eip1559Receipt* = TxEip1559 + when BlockNumber is int64: ## The goal of these templates is to make it easier to switch ## the block number type to a different representation @@ -256,6 +234,16 @@ else: template u256*(n: BlockNumber): UInt256 = n +# EIP-1559 conveniences +func baseFee*(h: BlockHeader | BlockHeaderRef): UInt256 = + if h.fee.isSome: + h.fee.get() + else: + 0.u256 + +template `baseFee=`*(h: BlockHeader | BlockHeaderRef, data: UInt256) = + h.fee = some(data) + func toBlockNonce*(n: uint64): BlockNonce = n.toBytesBE() @@ -268,25 +256,24 @@ proc newAccount*(nonce: AccountNonce = 0, balance: UInt256 = 0.u256): Account = result.storageRoot = emptyRlpHash result.codeHash = blankStringHash -proc hashOrStatus*(hash: Hash256): HashOrStatus = - HashOrStatus(isHash: true, hash: hash) +proc hasStatus*(rec: Receipt): bool {.inline.} = + rec.isHash == false -proc hashOrStatus*(status: bool): HashOrStatus = - HashOrStatus(isHash: false, status: status) +proc hasStateRoot*(rec: Receipt): bool {.inline.} = + rec.isHash == true -proc hasStatus*(rec: LegacyReceipt): bool {.inline.} = - rec.stateRootOrStatus.isHash == false - -proc hasStateRoot*(rec: LegacyReceipt): bool {.inline.} = - rec.stateRootOrStatus.isHash == true - -proc stateRoot*(rec: LegacyReceipt): Hash256 {.inline.} = +proc stateRoot*(rec: Receipt): Hash256 {.inline.} = doAssert(rec.hasStateRoot) - rec.stateRootOrStatus.hash + rec.hash -proc status*(rec: LegacyReceipt): int {.inline.} = - doAssert(rec.hasStatus) - if rec.stateRootOrStatus.status: 1 else: 0 +template contractCreation*(tx: Transaction): bool = + tx.to.isNone + +func destination*(tx: Transaction): EthAddress = + # use getRecipient if you also want to get + # the contract address + if tx.to.isSome: + return tx.to.get # # Rlp serialization: @@ -329,52 +316,132 @@ proc append*(rlpWriter: var RlpWriter, value: Stint) = {.fatal: "RLP serialization of signed integers is not allowed".} discard -type - TxTypes* = LegacyTx | AccessListTx +proc append*[T](w: var RlpWriter, val: Option[T]) = + if val.isSome: + w.append(val.get()) + else: + w.append("") -proc read*(rlp: var Rlp, t: var TxTypes, _: type EthAddress): EthAddress {.inline.} = +proc appendTxLegacy(w: var RlpWriter, tx: Transaction) = + w.startList(9) + w.append(tx.nonce) + w.append(tx.gasPrice) + w.append(tx.gasLimit) + w.append(tx.to) + w.append(tx.value) + w.append(tx.payload) + w.append(tx.V) + w.append(tx.R) + w.append(tx.S) + +proc appendTxEip2930(w: var RlpWriter, tx: Transaction) = + w.append(1) + w.startList(11) + w.append(tx.chainId.uint64) + w.append(tx.nonce) + w.append(tx.gasPrice) + w.append(tx.gasLimit) + w.append(tx.to) + w.append(tx.value) + w.append(tx.payload) + w.append(tx.accessList) + w.append(tx.V) + w.append(tx.R) + w.append(tx.S) + +proc appendTxEip1559(w: var RlpWriter, tx: Transaction) = + w.append(2) + w.startList(12) + w.append(tx.chainId.uint64) + w.append(tx.nonce) + w.append(tx.maxPriorityFee) + w.append(tx.maxFee) + w.append(tx.gasLimit) + w.append(tx.to) + w.append(tx.value) + w.append(tx.payload) + w.append(tx.accessList) + w.append(tx.V) + w.append(tx.R) + w.append(tx.S) + +proc append*(w: var RlpWriter, tx: Transaction) = + case tx.txType + of TxLegacy: + w.appendTxLegacy(tx) + of TxEip2930: + w.appendTxEip2930(tx) + of TxEip1559: + w.appendTxEip1559(tx) + +template read[T](rlp: var Rlp, val: var T)= + val = rlp.read(type val) + +proc read[T](rlp: var Rlp, val: var Option[T])= if rlp.blobLen != 0: - result = rlp.read(EthAddress) + val = some(rlp.read(T)) else: - t.isContractCreation = true - rlp.skipElem() + rlp.skipElem -proc append*(rlpWriter: var RlpWriter, t: TxTypes, a: EthAddress) {.inline.} = - if t.isContractCreation: - rlpWriter.append("") - else: - rlpWriter.append(a) +proc readTxLegacy(rlp: var Rlp, tx: var Transaction)= + tx.txType = TxLegacy + rlp.tryEnterList() + rlp.read(tx.nonce) + rlp.read(tx.gasPrice) + rlp.read(tx.gasLimit) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.payload) + rlp.read(tx.V) + rlp.read(tx.R) + rlp.read(tx.S) -proc read*(rlp: var Rlp, t: var AccessListTx, _: type ChainId): ChainId {.inline.} = - rlp.read(uint64).ChainId +proc readTxEip2930(rlp: var Rlp, tx: var Transaction)= + tx.txType = TxEip2930 + rlp.tryEnterList() + tx.chainId = rlp.read(uint64).ChainId + rlp.read(tx.nonce) + rlp.read(tx.gasPrice) + rlp.read(tx.gasLimit) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.payload) + rlp.read(tx.accessList) + rlp.read(tx.V) + rlp.read(tx.R) + rlp.read(tx.S) -proc append*(rlpWriter: var RlpWriter, t: AccessListTx, a: ChainId) {.inline.} = - rlpWriter.append(a.uint64) - -proc append*(rlpWriter: var RlpWriter, tx: Transaction) = - if tx.txType == LegacyTxType: - rlpWriter.append(tx.legacyTx) - else: - # EIP 2718/2930 - rlpWriter.append(1) - rlpWriter.append(tx.accessListTx) +proc readTxEip1559(rlp: var Rlp, tx: var Transaction)= + tx.txType = TxEip1559 + rlp.tryEnterList() + tx.chainId = rlp.read(uint64).ChainId + rlp.read(tx.nonce) + rlp.read(tx.maxPriorityFee) + rlp.read(tx.maxFee) + rlp.read(tx.gasLimit) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.payload) + rlp.read(tx.accessList) + rlp.read(tx.V) + rlp.read(tx.R) + rlp.read(tx.S) proc read*(rlp: var Rlp, T: type Transaction): T = if rlp.isList: - return Transaction( - txType: LegacyTxType, - legacyTx: rlp.read(LegacyTx) - ) + rlp.readTxLegacy(result) + return - # EIP 2718/2930 + # EIP 2718 let txType = rlp.read(int) - if txType != 1: + if txType notin {1, 2}: raise newException(UnsupportedRlpError, - "TxType expect 1 got " & $txType) - return Transaction( - txType: AccessListTxType, - accessListTx: rlp.read(AccessListTx) - ) + "TxType expect 1 or 2 got " & $txType) + + if TxType(txType) == TxEip2930: + rlp.readTxEip2930(result) + else: + rlp.readTxEip1559(result) proc read*(rlp: var Rlp, t: var (EthBlock | BlockBody), _: type seq[Transaction]): seq[Transaction] {.inline.} = # EIP 2718/2930: we have to override this field @@ -395,21 +462,11 @@ proc append*(rlpWriter: var RlpWriter, blk: EthBlock | BlockBody, txs: seq[Trans # not rlp(txType, txPayload) -> two list elem, wrong! rlpWriter.startList(txs.len) for tx in txs: - if tx.txType == LegacyTxType: + if tx.txType == TxLegacy: rlpWriter.append(tx) else: rlpWriter.append(rlp.encode(tx)) -proc read*(rlp: var Rlp, T: type HashOrStatus): T {.inline.} = - if rlp.isBlob() and (rlp.blobLen() == 32 or rlp.blobLen() == 1): - if rlp.blobLen == 1: - result = hashOrStatus(rlp.read(uint8) == 1) - else: - result = hashOrStatus(rlp.read(Hash256)) - else: - raise newException(RlpTypeMismatch, - "HashOrStatus expected, but the source RLP is not a blob of right size.") - func init*(T: type BlockHashOrNumber, str: string): T {.raises: [ValueError, Defect].} = if str.startsWith "0x": @@ -428,36 +485,45 @@ func `$`*(x: BlockHashOrNumber): string = else: $x.number -proc append*(rlpWriter: var RlpWriter, value: HashOrStatus) {.inline.} = - if value.isHash: - rlpWriter.append(value.hash) - else: - rlpWriter.append(if value.status: 1'u8 else: 0'u8) +proc append*(w: var RlpWriter, rec: Receipt) = + if rec.receiptType in {Eip2930Receipt, Eip1559Receipt}: + w.append(rec.receiptType.int) -proc append*(rlpWriter: var RlpWriter, rec: Receipt) = - if rec.receiptType == LegacyReceiptType: - rlpWriter.append(rec.legacyReceipt) + w.startList(4) + if rec.isHash: + w.append(rec.hash) else: - # EIP 2718/2930 - rlpWriter.append(1) - rlpWriter.append(rec.accessListReceipt) + w.append(rec.status.uint8) + + w.append(rec.cumulativeGasUsed) + w.append(rec.bloom) + w.append(rec.logs) proc read*(rlp: var Rlp, T: type Receipt): T = if rlp.isList: - return Receipt( - receiptType: LegacyReceiptType, - legacyReceipt: rlp.read(LegacyReceipt) - ) + result.receiptType = LegacyReceipt + else: + # EIP 2718 + let recType = rlp.read(int) + if recType notin {1, 2}: + raise newException(UnsupportedRlpError, + "TxType expect 1 or 2 got " & $recType) + result.receiptType = ReceiptType(recType) - # EIP 2718/2930 - let recType = rlp.read(int) - if recType != 1: - raise newException(UnsupportedRlpError, - "TxType expect 1 got " & $recType) - return Receipt( - receiptType: AccessListReceiptType, - accessListReceipt: rlp.read(AccessListReceipt) - ) + rlp.tryEnterList() + if rlp.isBlob and rlp.blobLen in {0, 1}: + result.isHash = false + result.status = rlp.read(uint8) == 1 + elif rlp.isBlob and rlp.blobLen == 32: + result.isHash = true + result.hash = rlp.read(Hash256) + else: + raise newException(RlpTypeMismatch, + "HashOrStatus expected, but the source RLP is not a blob of right size.") + + rlp.read(result.cumulativeGasUsed) + rlp.read(result.bloom) + rlp.read(result.logs) proc read*(rlp: var Rlp, T: type Time): T {.inline.} = result = fromUnix(rlp.read(int64)) @@ -478,6 +544,30 @@ proc read*(rlp: var Rlp, T: type HashOrNum): T = proc append*(rlpWriter: var RlpWriter, t: Time) {.inline.} = rlpWriter.append(t.toUnix()) +proc append*(w: var RlpWriter, h: BlockHeader) = + w.startList(if h.fee.isSome: 16 else: 15) + for k, v in fieldPairs(h): + when k != "fee": + w.append(v) + if h.fee.isSome: + w.append(h.fee.get()) + +proc read*(rlp: var Rlp, T: type BlockHeader): T = + let len = rlp.listLen + + if len notin {15, 16}: + raise newException(UnsupportedRlpError, + "BlockHeader elems should be 15 or 16 got " & $len) + + rlp.tryEnterList() + for k, v in fieldPairs(result): + when k != "fee": + v = rlp.read(type v) + + if len == 16: + # EIP-1559 + result.baseFee = rlp.read(UInt256) + proc rlpHash*[T](v: T): Hash256 = keccak256.digest(rlp.encode(v)) diff --git a/eth/common/transaction.nim b/eth/common/transaction.nim index e2af9a6..2527cf2 100644 --- a/eth/common/transaction.nim +++ b/eth/common/transaction.nim @@ -2,118 +2,76 @@ import nimcrypto/keccak, ".."/[common, rlp, keys] -proc initLegacyTx*(nonce: AccountNonce, gasPrice, gasLimit: GasInt, to: EthAddress, - value: UInt256, payload: Blob, V: int64, R, S: UInt256, isContractCreation = false): LegacyTx = - result.nonce = nonce - result.gasPrice = gasPrice - result.gasLimit = gasLimit - result.to = to - result.value = value - result.payload = payload - result.V = V - result.R = R - result.S = S - result.isContractCreation = isContractCreation - -type - LegacyUnsignedTx* = object - nonce*: AccountNonce - gasPrice*: GasInt - gasLimit*: GasInt - to* {.rlpCustomSerialization.}: EthAddress - value*: UInt256 - payload*: Blob - isContractCreation* {.rlpIgnore.}: bool - - AccessListUnsignedTx* = object - chainId* {.rlpCustomSerialization.}: ChainId - nonce* : AccountNonce - gasPrice* : GasInt - gasLimit* : GasInt - to* {.rlpCustomSerialization.}: EthAddress - value* : UInt256 - payload* : Blob - accessList*: AccessList - isContractCreation* {.rlpIgnore.}: bool - - UnsignedTxTypes* = LegacyUnsignedTx | AccessListUnsignedTx - -proc read*(rlp: var Rlp, t: var UnsignedTxTypes, _: type EthAddress): EthAddress {.inline.} = - if rlp.blobLen != 0: - result = rlp.read(EthAddress) - else: - t.isContractCreation = true - -proc append*(rlpWriter: var RlpWriter, t: UnsignedTxTypes, a: EthAddress) {.inline.} = - if t.isContractCreation: - rlpWriter.append("") - else: - rlpWriter.append(a) - -proc read*(rlp: var Rlp, t: var AccessListUnsignedTx, _: type ChainId): ChainId {.inline.} = - rlp.read(uint64).ChainId - -proc append*(rlpWriter: var RlpWriter, t: AccessListUnsignedTx, a: ChainId) {.inline.} = - rlpWriter.append(a.uint64) - const EIP155_CHAIN_ID_OFFSET* = 35'i64 -func rlpEncode*(tx: LegacyTx): auto = - # Encode transaction without signature - return rlp.encode(LegacyUnsignedTx( - nonce: tx.nonce, - gasPrice: tx.gasPrice, - gasLimit: tx.gasLimit, - to: tx.to, - value: tx.value, - payload: tx.payload, - isContractCreation: tx.isContractCreation - )) +func rlpEncodeLegacy(tx: Transaction): auto = + var w = initRlpWriter() + w.startList(6) + w.append(tx.nonce) + w.append(tx.gasPrice) + w.append(tx.gasLimit) + w.append(tx.to) + w.append(tx.value) + w.append(tx.payload) + w.finish() -func rlpEncodeEIP155*(tx: LegacyTx): auto = - let V = (tx.V - EIP155_CHAIN_ID_OFFSET) div 2 - # Encode transaction without signature - return rlp.encode(LegacyTx( - nonce: tx.nonce, - gasPrice: tx.gasPrice, - gasLimit: tx.gasLimit, - to: tx.to, - value: tx.value, - payload: tx.payload, - isContractCreation: tx.isContractCreation, - V: V, - R: 0.u256, - S: 0.u256 - )) +func rlpEncodeEip155(tx: Transaction): auto = + let chainId = (tx.V - EIP155_CHAIN_ID_OFFSET) div 2 + var w = initRlpWriter() + w.startList(9) + w.append(tx.nonce) + w.append(tx.gasPrice) + w.append(tx.gasLimit) + w.append(tx.to) + w.append(tx.value) + w.append(tx.payload) + w.append(chainId) + w.append(0) + w.append(0) + w.finish() -func rlpEncode*(tx: AccessListTx): auto = - # EIP 2718/2930 - let unsignedTx = AccessListUnsignedTx( - chainId: tx.chainId, - nonce: tx.nonce, - gasPrice: tx.gasPrice, - gasLimit: tx.gasLimit, - to: tx.to, - value: tx.value, - payload: tx.payload, - accessList: tx.accessList, - isContractCreation: tx.isContractCreation - ) - var rw = initRlpWriter() - rw.append(1) - rw.append(unsignedTx) - rw.finish() +func rlpEncodeEip2930(tx: Transaction): auto = + var w = initRlpWriter() + w.append(1) + w.startList(8) + w.append(tx.chainId.uint64) + w.append(tx.nonce) + w.append(tx.gasPrice) + w.append(tx.gasLimit) + w.append(tx.to) + w.append(tx.value) + w.append(tx.payload) + w.append(tx.accessList) + w.finish() -func txHashNoSignature*(tx: LegacyTx): Hash256 = - # Hash transaction without signature - keccak256.digest(if tx.V >= EIP155_CHAIN_ID_OFFSET: tx.rlpEncodeEIP155 else: tx.rlpEncode) +func rlpEncodeEip1559(tx: Transaction): auto = + var w = initRlpWriter() + w.append(2) + w.startList(9) + w.append(tx.chainId.uint64) + w.append(tx.nonce) + w.append(tx.maxPriorityFee) + w.append(tx.maxFee) + w.append(tx.gasLimit) + w.append(tx.to) + w.append(tx.value) + w.append(tx.payload) + w.append(tx.accessList) + w.finish() -func txHashNoSignature*(tx: AccessListTx): Hash256 = - keccak256.digest(tx.rlpEncode) +func rlpEncode*(tx: Transaction): auto = + case tx.txType + of TxLegacy: + if tx.V >= EIP155_CHAIN_ID_OFFSET: + tx.rlpEncodeEIP155 + else: + tx.rlpEncodeLegacy + of TxEip2930: + tx.rlpEncodeEip2930 + of TxEip1559: + tx.rlpEncodeEip1559 func txHashNoSignature*(tx: Transaction): Hash256 = - if tx.txType == LegacyTxType: - txHashNoSignature(tx.legacyTx) - else: - txHashNoSignature(tx.accessListTx) + # Hash transaction without signature + keccak256.digest(rlpEncode(tx)) diff --git a/tests/rlp/test_common.nim b/tests/rlp/test_common.nim index ed8b113..48feee9 100644 --- a/tests/rlp/test_common.nim +++ b/tests/rlp/test_common.nim @@ -9,33 +9,9 @@ type EthHeader = object header: BlockHeader -proc `==`(a, b: HashOrStatus): bool = - result = a.isHash == b.isHash - if not result: return - if a.isHash: - result = result and a.hash == b.hash - else: - result = result and a.status == b.status - func `==`(a, b: ChainId): bool = a.uint64 == b.uint64 -func `==`(a, b: Transaction): bool = - if a.txType != b.txType: - return false - if a.txType == LegacyTxType: - return a.legacyTx == b.legacyTx - else: - return a.accessListTx == b.accessListTx - -func `==`(a, b: Receipt): bool = - if a.receiptType != b.receiptType: - return false - if a.receiptType == LegacyReceiptType: - return a.legacyReceipt == b.legacyReceipt - else: - return a.accessListReceipt == b.accessListReceipt - proc loadFile(x: int) = let fileName = "tests" / "rlp" / "eip2718" / "acl_block_" & $x & ".json" test fileName: @@ -62,20 +38,18 @@ proc suite1() = suite "rlp encoding": test "receipt roundtrip": let a = Receipt( - receiptType: LegacyReceiptType, - legacyReceipt: LegacyReceipt( - stateRootOrStatus: hashOrStatus(true), - cumulativeGasUsed: 51000 - ) + receiptType: LegacyReceipt, + isHash: false, + status: false, + cumulativeGasUsed: 51000 ) let hash = rlpHash(a) let b = Receipt( - receiptType: LegacyReceiptType, - legacyReceipt: LegacyReceipt( - stateRootOrStatus: hashOrStatus(hash), - cumulativeGasUsed: 21000 - ) + receiptType: LegacyReceipt, + isHash: true, + hash: hash, + cumulativeGasUsed: 21000 ) let abytes = rlp.encode(a) @@ -85,20 +59,16 @@ proc suite1() = check aa == a check bb == b - test "access list receipt": + test "EIP 2930 receipt": let a = Receipt( - receiptType: AccessListReceiptType, - accessListReceipt: AccessListReceipt( - status: true - ) + receiptType: Eip2930Receipt, + status: true ) let b = Receipt( - receiptType: AccessListReceiptType, - accessListReceipt: AccessListReceipt( - status: false, - cumulativeGasUsed: 21000 - ) + receiptType: Eip2930Receipt, + status: false, + cumulativeGasUsed: 21000 ) let abytes = rlp.encode(a) @@ -113,5 +83,11 @@ proc suite2() = for i in 0..<10: loadFile(i) + test "rlp roundtrip EIP1559": + var h: BlockHeader + let xy = rlp.encode(h) + let hh = rlp.decode(xy, BlockHeader) + check h == hh + suite1() -suite2() \ No newline at end of file +suite2()