From 2ad07a60109a4609d672f9f147eeee987e43a15d Mon Sep 17 00:00:00 2001 From: Kim De Mey Date: Mon, 5 Feb 2024 23:22:15 +0100 Subject: [PATCH] Fix two bugs in Receipts RLP encoding/decoding (#672) 1. Fix Assertion error when receipt is not a List nor has a single byte value. Receiving such garbage data would cause a crash. 2. Fix decoding of Receipt list by adding the missing Blob encapsulation Also added tests for these scenarios. --- eth/common/eth_types_rlp.nim | 120 ++++++++++++++++---- tests/rlp/cases/RandomRLPTests/example.json | 2 +- tests/rlp/cases/invalidRLPTest.json | 12 +- tests/rlp/cases/pyRlpInvalidCases.json | 12 +- tests/rlp/test_common.nim | 95 +++++++++++++++- 5 files changed, 201 insertions(+), 40 deletions(-) diff --git a/eth/common/eth_types_rlp.nim b/eth/common/eth_types_rlp.nim index 70ef59d..144140c 100644 --- a/eth/common/eth_types_rlp.nim +++ b/eth/common/eth_types_rlp.nim @@ -1,4 +1,4 @@ -# Copyright (c) 2022 Status Research & Development GmbH +# Copyright (c) 2022-2024 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -349,33 +349,111 @@ proc append*(w: var RlpWriter, rec: Receipt) = w.append(rec.bloom) w.append(rec.logs) -proc read*(rlp: var Rlp, T: type Receipt): T = - if rlp.isList: - result.receiptType = LegacyReceipt - else: - # EIP 2718 - let recType = rlp.getByteValue - rlp.position += 1 - - if recType notin {1, 2, 3}: - raise newException(UnsupportedRlpError, - "TxType expect 1, 2, or 3 got " & $recType) - result.receiptType = ReceiptType(recType) - +proc readReceiptLegacy(rlp: var Rlp, receipt: var Receipt) = + receipt.receiptType = LegacyReceipt rlp.tryEnterList() if rlp.isBlob and rlp.blobLen in {0, 1}: - result.isHash = false - result.status = rlp.read(uint8) == 1 + receipt.isHash = false + receipt.status = rlp.read(uint8) == 1 elif rlp.isBlob and rlp.blobLen == 32: - result.isHash = true - result.hash = rlp.read(Hash256) + receipt.isHash = true + receipt.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) + rlp.read(receipt.cumulativeGasUsed) + rlp.read(receipt.bloom) + rlp.read(receipt.logs) + +proc readReceiptTyped(rlp: var Rlp, receipt: var Receipt) = + if not rlp.hasData: + raise newException(MalformedRlpError, + "Receipt expected but source RLP is empty") + if not rlp.isSingleByte: + raise newException(MalformedRlpError, + "ReceiptType byte is out of range, must be 0x00 to 0x7f") + let recType = rlp.getByteValue + rlp.position += 1 + + var txVal: ReceiptType + if checkedEnumAssign(txVal, recType): + case txVal: + of Eip2930Receipt, Eip1559Receipt, Eip4844Receipt: + receipt.receiptType = txVal + of LegacyReceipt: + # The legacy type should not be used here. + raise newException(MalformedRlpError, + "Invalid ReceiptType: " & $recType) + else: + raise newException(UnsupportedRlpError, + "Unsupported ReceiptType: " & $recType) + + # Note: This currently remains the same as the legacy receipt. + rlp.tryEnterList() + if rlp.isBlob and rlp.blobLen in {0, 1}: + receipt.isHash = false + receipt.status = rlp.read(uint8) == 1 + elif rlp.isBlob and rlp.blobLen == 32: + receipt.isHash = true + receipt.hash = rlp.read(Hash256) + else: + raise newException(RlpTypeMismatch, + "HashOrStatus expected, but the source RLP is not a blob of right size.") + + rlp.read(receipt.cumulativeGasUsed) + rlp.read(receipt.bloom) + rlp.read(receipt.logs) + +proc read*(rlp: var Rlp, T: type Receipt): T = + # Individual receipts are encoded and stored as either `RLP([fields..])` + # for legacy receipts, or `Type || RLP([fields..])`. Both of these + # encodings are byte sequences. The part after `Type` doesn't have to be + # RLP in theory, but all types so far use RLP. EIP-2718 covers this. + var receipt: Receipt + if rlp.isList: + rlp.readReceiptLegacy(receipt) + else: + rlp.readReceiptTyped(receipt) + receipt + +proc read*( + rlp: var Rlp, + T: (type seq[Receipt]) | (type openArray[Receipt]) + ): seq[Receipt] = + # In arrays (sequences), receipts are encoded as either `RLP([fields..])` + # for legacy receipts, or `RLP(Type || RLP([fields..]))` for all typed + # receipts to date. Spot the extra `RLP(..)` blob encoding, to make it + # valid RLP inside a larger RLP. EIP-2976 covers this, "Typed Transactions + # over Gossip", although it's not very clear about the blob encoding. + # + # See also note about transactions above. + if not rlp.isList: + raise newException(RlpTypeMismatch, + "Receipts list expected, but source RLP is not a list") + + var receipts: seq[Receipt] + for item in rlp: + var receipt: Receipt + if item.isList: + item.readReceiptLegacy(receipt) + else: + var rr = rlpFromBytes(rlp.read(Blob)) + rr.readReceiptTyped(receipt) + receipts.add receipt + + receipts + +proc append*( + rlpWriter: var RlpWriter, receipts: seq[Receipt] | openArray[Receipt] + ) = + # See above about encoding arrays/sequences of receipts. + rlpWriter.startList(receipts.len) + for receipt in receipts: + if receipt.receiptType == LegacyReceipt: + rlpWriter.append(receipt) + else: + rlpWriter.append(rlp.encode(receipt)) proc read*(rlp: var Rlp, T: type EthTime): T {.inline.} = result = EthTime rlp.read(uint64) diff --git a/tests/rlp/cases/RandomRLPTests/example.json b/tests/rlp/cases/RandomRLPTests/example.json index e395204..e883ad4 100644 --- a/tests/rlp/cases/RandomRLPTests/example.json +++ b/tests/rlp/cases/RandomRLPTests/example.json @@ -1,6 +1,6 @@ { "listsoflists2": { - "in": "VALID", + "in": "VALID", "out": "c7c0c1c0c3c0c1c0" } } diff --git a/tests/rlp/cases/invalidRLPTest.json b/tests/rlp/cases/invalidRLPTest.json index 73cfc93..8a4585f 100644 --- a/tests/rlp/cases/invalidRLPTest.json +++ b/tests/rlp/cases/invalidRLPTest.json @@ -1,31 +1,31 @@ { "int32Overflow": { - "in": "INVALID", + "in": "INVALID", "out": "bf0f000000000000021111" }, "int32Overflow2": { - "in": "INVALID", + "in": "INVALID", "out": "ff0f000000000000021111" }, "wrongSizeList": { - "in": "INVALID", + "in": "INVALID", "out": "f80180" }, "wrongSizeList2": { - "in": "INVALID", + "in": "INVALID", "out": "f80100" }, "incorrectLengthInArray": { - "in": "INVALID", + "in": "INVALID", "out": "b9002100dc2b275d0f74e8a53e6f4ec61b27f24278820be3f82ea2110e582081b0565df0" }, "randomRLP": { - "in": "INVALID", + "in": "INVALID", "out": "f861f83eb9002100dc2b275d0f74e8a53e6f4ec61b27f24278820be3f82ea2110e582081b0565df027b90015002d5ef8325ae4d034df55d4b58d0dfba64d61ddd17be00000b9001a00dae30907045a2f66fa36f2bb8aa9029cbb0b8a7b3b5c435ab331" }, diff --git a/tests/rlp/cases/pyRlpInvalidCases.json b/tests/rlp/cases/pyRlpInvalidCases.json index 4a91b52..ace2bef 100644 --- a/tests/rlp/cases/pyRlpInvalidCases.json +++ b/tests/rlp/cases/pyRlpInvalidCases.json @@ -1,31 +1,31 @@ { "T1": { - "in": "INVALID", + "in": "INVALID", "out": "" }, "T2": { - "in": "INVALID", + "in": "INVALID", "out": "00ab" }, "T3": { - "in": "INVALID", + "in": "INVALID", "out": "0000ff" }, "T4": { - "in": "VALID", + "in": "VALID", "out": "83646F67636174" }, "T5": { - "in": "INVALID", + "in": "INVALID", "out": "83646F" }, "T6": { - "in": "INVALID", + "in": "INVALID", "out": "c7c0c1c0c3c0c1c0ff" }, diff --git a/tests/rlp/test_common.nim b/tests/rlp/test_common.nim index 3829db2..2227f31 100644 --- a/tests/rlp/test_common.nim +++ b/tests/rlp/test_common.nim @@ -1,3 +1,9 @@ +# Copyright (c) 2019-2024 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + {.used.} import @@ -33,8 +39,8 @@ proc loadFile(x: int) = check bytes1 == bytes3 proc suite1() = - suite "rlp encoding": - test "receipt roundtrip": + suite "RLP encoding": + test "Receipt roundtrip": let a = Receipt( receiptType: LegacyReceipt, isHash: false, @@ -57,7 +63,7 @@ proc suite1() = check aa == a check bb == b - test "EIP 2930 receipt": + test "EIP-2930 receipt": let a = Receipt( receiptType: Eip2930Receipt, status: true @@ -76,7 +82,7 @@ proc suite1() = check aa == a check bb == b - test "EIP 4895 roundtrip": + test "EIP-4895 roundtrip": let a = Withdrawal( index: 1, validatorIndex: 2, @@ -90,11 +96,11 @@ proc suite1() = check aa == a proc suite2() = - suite "eip 2718 transaction": + suite "EIP-2718 transaction / receipt": for i in 0..<10: loadFile(i) - test "rlp roundtrip EIP1559 / EIP4895 / EIP4844": + test "BlockHeader: rlp roundtrip EIP-1559 / EIP-4895 / EIP-4844": proc doTest(h: BlockHeader) = let xy = rlp.encode(h) let hh = rlp.decode(xy, BlockHeader) @@ -117,5 +123,82 @@ proc suite2() = h.excessBlobGas = some 1234'u64 doTest h + test "Receipts EIP-2718 + EIP-2976 encoding": + const + # Test payload from + # https://github.com/ethereum/go-ethereum/blob/253447a4f5e5f7f65c0605d490360bb58fb5f8e0/core/types/receipt_test.go#L370 + payload = "f9043eb9010c01f90108018262d4b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0b9010c01f901080182cd14b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0b9010d01f901090183013754b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0b9010d01f90109018301a194b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0" + receiptsBytes = hexToSeqByte(payload) + let receipts = rlp.decode(receiptsBytes, seq[Receipt]) + + check receipts.len() == 4 + for receipt in receipts: + check receipt.receiptType == TxEip2930 + + let encoded = rlp.encode(receipts) + + check receiptsBytes == encoded + + test "Receipts EIP-2718 encoding - invalid - empty": + let receiptBytes: seq[byte] = @[] + expect MalformedRlpError: + let _ = rlp.decode(receiptBytes, Receipt) + + test "Receipts EIP-2718 encoding - invalid - unsupported tx type": + let receiptBytes: seq[byte] = @[0x04] + expect UnsupportedRlpError: + let _ = rlp.decode(receiptBytes, Receipt) + + test "Receipts EIP-2718 encoding - invalid - legacy tx type": + let receiptBytes: seq[byte] = @[0x00] + expect MalformedRlpError: + let _ = rlp.decode(receiptBytes, Receipt) + + test "Receipts EIP-2718 encoding - invalid - out of bounds tx type": + let receiptBytes: seq[byte] = @[0x81, 0x80] + expect MalformedRlpError: + let _ = rlp.decode(receiptBytes, Receipt) + + test "Receipts EIP-2718 encoding - invalid - empty receipt payload": + let receiptBytes: seq[byte] = @[0x02] + expect RlpTypeMismatch: + let _ = rlp.decode(receiptBytes, Receipt) + + test "Receipt legacy": + const + # Test payload from + # https://github.com/ethereum/go-ethereum/blob/253447a4f5e5f7f65c0605d490360bb58fb5f8e0/core/types/receipt_test.go#L417 + payload = "f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff" + receiptsBytes = hexToSeqByte(payload) + + let receipt = rlp.decode(receiptsBytes, Receipt) + check receipt.receiptType == LegacyReceipt + let encoded = rlp.encode(receipt) + check receiptsBytes == encoded + + test "Receipt EIP-2930": + const + # Test payload from + # https://github.com/ethereum/go-ethereum/blob/253447a4f5e5f7f65c0605d490360bb58fb5f8e0/core/types/receipt_test.go#L435 + payload = "01f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff" + receiptsBytes = hexToSeqByte(payload) + + let receipt = rlp.decode(receiptsBytes, Receipt) + check receipt.receiptType == Eip2930Receipt + let encoded = rlp.encode(receipt) + check receiptsBytes == encoded + + test "Receipt EIP-1559": + const + # Test payload from + # https://github.com/ethereum/go-ethereum/blob/253447a4f5e5f7f65c0605d490360bb58fb5f8e0/core/types/receipt_test.go#L453 + payload = "02f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff" + receiptsBytes = hexToSeqByte(payload) + + let receipt = rlp.decode(receiptsBytes, Receipt) + check receipt.receiptType == Eip1559Receipt + let encoded = rlp.encode(receipt) + check receiptsBytes == encoded + suite1() suite2()