Skip to content

Commit 2ad07a6

Browse files
authored
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.
1 parent 4433efe commit 2ad07a6

File tree

5 files changed

+198
-37
lines changed

5 files changed

+198
-37
lines changed

eth/common/eth_types_rlp.nim

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2022 Status Research & Development GmbH
1+
# Copyright (c) 2022-2024 Status Research & Development GmbH
22
# Licensed and distributed under either of
33
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
44
# * 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) =
349349
w.append(rec.bloom)
350350
w.append(rec.logs)
351351

352-
proc read*(rlp: var Rlp, T: type Receipt): T =
353-
if rlp.isList:
354-
result.receiptType = LegacyReceipt
352+
proc readReceiptLegacy(rlp: var Rlp, receipt: var Receipt) =
353+
receipt.receiptType = LegacyReceipt
354+
rlp.tryEnterList()
355+
if rlp.isBlob and rlp.blobLen in {0, 1}:
356+
receipt.isHash = false
357+
receipt.status = rlp.read(uint8) == 1
358+
elif rlp.isBlob and rlp.blobLen == 32:
359+
receipt.isHash = true
360+
receipt.hash = rlp.read(Hash256)
355361
else:
356-
# EIP 2718
357-
let recType = rlp.getByteValue
358-
rlp.position += 1
362+
raise newException(RlpTypeMismatch,
363+
"HashOrStatus expected, but the source RLP is not a blob of right size.")
364+
365+
rlp.read(receipt.cumulativeGasUsed)
366+
rlp.read(receipt.bloom)
367+
rlp.read(receipt.logs)
359368

360-
if recType notin {1, 2, 3}:
361-
raise newException(UnsupportedRlpError,
362-
"TxType expect 1, 2, or 3 got " & $recType)
363-
result.receiptType = ReceiptType(recType)
369+
proc readReceiptTyped(rlp: var Rlp, receipt: var Receipt) =
370+
if not rlp.hasData:
371+
raise newException(MalformedRlpError,
372+
"Receipt expected but source RLP is empty")
373+
if not rlp.isSingleByte:
374+
raise newException(MalformedRlpError,
375+
"ReceiptType byte is out of range, must be 0x00 to 0x7f")
376+
let recType = rlp.getByteValue
377+
rlp.position += 1
378+
379+
var txVal: ReceiptType
380+
if checkedEnumAssign(txVal, recType):
381+
case txVal:
382+
of Eip2930Receipt, Eip1559Receipt, Eip4844Receipt:
383+
receipt.receiptType = txVal
384+
of LegacyReceipt:
385+
# The legacy type should not be used here.
386+
raise newException(MalformedRlpError,
387+
"Invalid ReceiptType: " & $recType)
388+
else:
389+
raise newException(UnsupportedRlpError,
390+
"Unsupported ReceiptType: " & $recType)
364391

392+
# Note: This currently remains the same as the legacy receipt.
365393
rlp.tryEnterList()
366394
if rlp.isBlob and rlp.blobLen in {0, 1}:
367-
result.isHash = false
368-
result.status = rlp.read(uint8) == 1
395+
receipt.isHash = false
396+
receipt.status = rlp.read(uint8) == 1
369397
elif rlp.isBlob and rlp.blobLen == 32:
370-
result.isHash = true
371-
result.hash = rlp.read(Hash256)
398+
receipt.isHash = true
399+
receipt.hash = rlp.read(Hash256)
372400
else:
373401
raise newException(RlpTypeMismatch,
374402
"HashOrStatus expected, but the source RLP is not a blob of right size.")
375403

376-
rlp.read(result.cumulativeGasUsed)
377-
rlp.read(result.bloom)
378-
rlp.read(result.logs)
404+
rlp.read(receipt.cumulativeGasUsed)
405+
rlp.read(receipt.bloom)
406+
rlp.read(receipt.logs)
407+
408+
proc read*(rlp: var Rlp, T: type Receipt): T =
409+
# Individual receipts are encoded and stored as either `RLP([fields..])`
410+
# for legacy receipts, or `Type || RLP([fields..])`. Both of these
411+
# encodings are byte sequences. The part after `Type` doesn't have to be
412+
# RLP in theory, but all types so far use RLP. EIP-2718 covers this.
413+
var receipt: Receipt
414+
if rlp.isList:
415+
rlp.readReceiptLegacy(receipt)
416+
else:
417+
rlp.readReceiptTyped(receipt)
418+
receipt
419+
420+
proc read*(
421+
rlp: var Rlp,
422+
T: (type seq[Receipt]) | (type openArray[Receipt])
423+
): seq[Receipt] =
424+
# In arrays (sequences), receipts are encoded as either `RLP([fields..])`
425+
# for legacy receipts, or `RLP(Type || RLP([fields..]))` for all typed
426+
# receipts to date. Spot the extra `RLP(..)` blob encoding, to make it
427+
# valid RLP inside a larger RLP. EIP-2976 covers this, "Typed Transactions
428+
# over Gossip", although it's not very clear about the blob encoding.
429+
#
430+
# See also note about transactions above.
431+
if not rlp.isList:
432+
raise newException(RlpTypeMismatch,
433+
"Receipts list expected, but source RLP is not a list")
434+
435+
var receipts: seq[Receipt]
436+
for item in rlp:
437+
var receipt: Receipt
438+
if item.isList:
439+
item.readReceiptLegacy(receipt)
440+
else:
441+
var rr = rlpFromBytes(rlp.read(Blob))
442+
rr.readReceiptTyped(receipt)
443+
receipts.add receipt
444+
445+
receipts
446+
447+
proc append*(
448+
rlpWriter: var RlpWriter, receipts: seq[Receipt] | openArray[Receipt]
449+
) =
450+
# See above about encoding arrays/sequences of receipts.
451+
rlpWriter.startList(receipts.len)
452+
for receipt in receipts:
453+
if receipt.receiptType == LegacyReceipt:
454+
rlpWriter.append(receipt)
455+
else:
456+
rlpWriter.append(rlp.encode(receipt))
379457

380458
proc read*(rlp: var Rlp, T: type EthTime): T {.inline.} =
381459
result = EthTime rlp.read(uint64)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"listsoflists2": {
3-
"in": "VALID",
3+
"in": "VALID",
44
"out": "c7c0c1c0c3c0c1c0"
55
}
66
}

tests/rlp/cases/invalidRLPTest.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
{
22
"int32Overflow": {
3-
"in": "INVALID",
3+
"in": "INVALID",
44
"out": "bf0f000000000000021111"
55
},
66

77
"int32Overflow2": {
8-
"in": "INVALID",
8+
"in": "INVALID",
99
"out": "ff0f000000000000021111"
1010
},
1111

1212
"wrongSizeList": {
13-
"in": "INVALID",
13+
"in": "INVALID",
1414
"out": "f80180"
1515
},
1616

1717
"wrongSizeList2": {
18-
"in": "INVALID",
18+
"in": "INVALID",
1919
"out": "f80100"
2020
},
2121

2222
"incorrectLengthInArray": {
23-
"in": "INVALID",
23+
"in": "INVALID",
2424
"out": "b9002100dc2b275d0f74e8a53e6f4ec61b27f24278820be3f82ea2110e582081b0565df0"
2525
},
2626

2727
"randomRLP": {
28-
"in": "INVALID",
28+
"in": "INVALID",
2929
"out": "f861f83eb9002100dc2b275d0f74e8a53e6f4ec61b27f24278820be3f82ea2110e582081b0565df027b90015002d5ef8325ae4d034df55d4b58d0dfba64d61ddd17be00000b9001a00dae30907045a2f66fa36f2bb8aa9029cbb0b8a7b3b5c435ab331"
3030
},
3131

tests/rlp/cases/pyRlpInvalidCases.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
{
22
"T1": {
3-
"in": "INVALID",
3+
"in": "INVALID",
44
"out": ""
55
},
66

77
"T2": {
8-
"in": "INVALID",
8+
"in": "INVALID",
99
"out": "00ab"
1010
},
1111

1212
"T3": {
13-
"in": "INVALID",
13+
"in": "INVALID",
1414
"out": "0000ff"
1515
},
1616

1717
"T4": {
18-
"in": "VALID",
18+
"in": "VALID",
1919
"out": "83646F67636174"
2020
},
2121

2222
"T5": {
23-
"in": "INVALID",
23+
"in": "INVALID",
2424
"out": "83646F"
2525
},
2626

2727
"T6": {
28-
"in": "INVALID",
28+
"in": "INVALID",
2929
"out": "c7c0c1c0c3c0c1c0ff"
3030
},
3131

tests/rlp/test_common.nim

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Copyright (c) 2019-2024 Status Research & Development GmbH
2+
# Licensed and distributed under either of
3+
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
4+
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
5+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
6+
17
{.used.}
28

39
import
@@ -33,8 +39,8 @@ proc loadFile(x: int) =
3339
check bytes1 == bytes3
3440

3541
proc suite1() =
36-
suite "rlp encoding":
37-
test "receipt roundtrip":
42+
suite "RLP encoding":
43+
test "Receipt roundtrip":
3844
let a = Receipt(
3945
receiptType: LegacyReceipt,
4046
isHash: false,
@@ -57,7 +63,7 @@ proc suite1() =
5763
check aa == a
5864
check bb == b
5965

60-
test "EIP 2930 receipt":
66+
test "EIP-2930 receipt":
6167
let a = Receipt(
6268
receiptType: Eip2930Receipt,
6369
status: true
@@ -76,7 +82,7 @@ proc suite1() =
7682
check aa == a
7783
check bb == b
7884

79-
test "EIP 4895 roundtrip":
85+
test "EIP-4895 roundtrip":
8086
let a = Withdrawal(
8187
index: 1,
8288
validatorIndex: 2,
@@ -90,11 +96,11 @@ proc suite1() =
9096
check aa == a
9197

9298
proc suite2() =
93-
suite "eip 2718 transaction":
99+
suite "EIP-2718 transaction / receipt":
94100
for i in 0..<10:
95101
loadFile(i)
96102

97-
test "rlp roundtrip EIP1559 / EIP4895 / EIP4844":
103+
test "BlockHeader: rlp roundtrip EIP-1559 / EIP-4895 / EIP-4844":
98104
proc doTest(h: BlockHeader) =
99105
let xy = rlp.encode(h)
100106
let hh = rlp.decode(xy, BlockHeader)
@@ -117,5 +123,82 @@ proc suite2() =
117123
h.excessBlobGas = some 1234'u64
118124
doTest h
119125

126+
test "Receipts EIP-2718 + EIP-2976 encoding":
127+
const
128+
# Test payload from
129+
# https://github.com/ethereum/go-ethereum/blob/253447a4f5e5f7f65c0605d490360bb58fb5f8e0/core/types/receipt_test.go#L370
130+
payload = "f9043eb9010c01f90108018262d4b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0b9010c01f901080182cd14b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0b9010d01f901090183013754b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0b9010d01f90109018301a194b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0"
131+
receiptsBytes = hexToSeqByte(payload)
132+
let receipts = rlp.decode(receiptsBytes, seq[Receipt])
133+
134+
check receipts.len() == 4
135+
for receipt in receipts:
136+
check receipt.receiptType == TxEip2930
137+
138+
let encoded = rlp.encode(receipts)
139+
140+
check receiptsBytes == encoded
141+
142+
test "Receipts EIP-2718 encoding - invalid - empty":
143+
let receiptBytes: seq[byte] = @[]
144+
expect MalformedRlpError:
145+
let _ = rlp.decode(receiptBytes, Receipt)
146+
147+
test "Receipts EIP-2718 encoding - invalid - unsupported tx type":
148+
let receiptBytes: seq[byte] = @[0x04]
149+
expect UnsupportedRlpError:
150+
let _ = rlp.decode(receiptBytes, Receipt)
151+
152+
test "Receipts EIP-2718 encoding - invalid - legacy tx type":
153+
let receiptBytes: seq[byte] = @[0x00]
154+
expect MalformedRlpError:
155+
let _ = rlp.decode(receiptBytes, Receipt)
156+
157+
test "Receipts EIP-2718 encoding - invalid - out of bounds tx type":
158+
let receiptBytes: seq[byte] = @[0x81, 0x80]
159+
expect MalformedRlpError:
160+
let _ = rlp.decode(receiptBytes, Receipt)
161+
162+
test "Receipts EIP-2718 encoding - invalid - empty receipt payload":
163+
let receiptBytes: seq[byte] = @[0x02]
164+
expect RlpTypeMismatch:
165+
let _ = rlp.decode(receiptBytes, Receipt)
166+
167+
test "Receipt legacy":
168+
const
169+
# Test payload from
170+
# https://github.com/ethereum/go-ethereum/blob/253447a4f5e5f7f65c0605d490360bb58fb5f8e0/core/types/receipt_test.go#L417
171+
payload = "f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
172+
receiptsBytes = hexToSeqByte(payload)
173+
174+
let receipt = rlp.decode(receiptsBytes, Receipt)
175+
check receipt.receiptType == LegacyReceipt
176+
let encoded = rlp.encode(receipt)
177+
check receiptsBytes == encoded
178+
179+
test "Receipt EIP-2930":
180+
const
181+
# Test payload from
182+
# https://github.com/ethereum/go-ethereum/blob/253447a4f5e5f7f65c0605d490360bb58fb5f8e0/core/types/receipt_test.go#L435
183+
payload = "01f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
184+
receiptsBytes = hexToSeqByte(payload)
185+
186+
let receipt = rlp.decode(receiptsBytes, Receipt)
187+
check receipt.receiptType == Eip2930Receipt
188+
let encoded = rlp.encode(receipt)
189+
check receiptsBytes == encoded
190+
191+
test "Receipt EIP-1559":
192+
const
193+
# Test payload from
194+
# https://github.com/ethereum/go-ethereum/blob/253447a4f5e5f7f65c0605d490360bb58fb5f8e0/core/types/receipt_test.go#L453
195+
payload = "02f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff"
196+
receiptsBytes = hexToSeqByte(payload)
197+
198+
let receipt = rlp.decode(receiptsBytes, Receipt)
199+
check receipt.receiptType == Eip1559Receipt
200+
let encoded = rlp.encode(receipt)
201+
check receiptsBytes == encoded
202+
120203
suite1()
121204
suite2()

0 commit comments

Comments
 (0)