Skip to content

Commit b314adc

Browse files
fix(primitives): encode typed receipts correctly (#16420)
1 parent c81f336 commit b314adc

2 files changed

Lines changed: 50 additions & 118 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
foundry-primitives: patch
3+
---
4+
5+
Fixed typed receipt RLP encoding to use the canonical EIP-2718 network format.

crates/primitives/src/transaction/receipt.rs

Lines changed: 45 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use alloy_network::eip2718::{
66
Eip2718Error, Encodable2718, LEGACY_TX_TYPE_ID,
77
};
88
use alloy_primitives::{Bloom, Log, TxHash, logs_bloom};
9-
use alloy_rlp::{BufMut, Decodable, Encodable, Header, bytes};
9+
use alloy_rlp::{BufMut, Decodable, Encodable, bytes};
1010
use alloy_rpc_types::{BlockNumHash, trace::otterscan::OtsReceipt};
1111
#[cfg(feature = "optimism")]
1212
use op_alloy_consensus::{
@@ -293,129 +293,17 @@ where
293293

294294
impl Encodable for FoundryReceiptEnvelope {
295295
fn encode(&self, out: &mut dyn bytes::BufMut) {
296-
match self {
297-
Self::Legacy(r) => r.encode(out),
298-
receipt => {
299-
let payload_len = match receipt {
300-
Self::Eip2930(r) => r.length() + 1,
301-
Self::Eip1559(r) => r.length() + 1,
302-
Self::Eip4844(r) => r.length() + 1,
303-
Self::Eip7702(r) => r.length() + 1,
304-
#[cfg(feature = "optimism")]
305-
Self::PostExec(r) => r.length() + 1,
306-
#[cfg(feature = "optimism")]
307-
Self::Deposit(r) => r.length() + 1,
308-
Self::Tempo(r) => r.length() + 1,
309-
_ => unreachable!("receipt already matched"),
310-
};
296+
self.network_encode(out);
297+
}
311298

312-
match receipt {
313-
Self::Eip2930(r) => {
314-
Header { list: true, payload_length: payload_len }.encode(out);
315-
EIP2930_TX_TYPE_ID.encode(out);
316-
r.encode(out);
317-
}
318-
Self::Eip1559(r) => {
319-
Header { list: true, payload_length: payload_len }.encode(out);
320-
EIP1559_TX_TYPE_ID.encode(out);
321-
r.encode(out);
322-
}
323-
Self::Eip4844(r) => {
324-
Header { list: true, payload_length: payload_len }.encode(out);
325-
EIP4844_TX_TYPE_ID.encode(out);
326-
r.encode(out);
327-
}
328-
Self::Eip7702(r) => {
329-
Header { list: true, payload_length: payload_len }.encode(out);
330-
EIP7702_TX_TYPE_ID.encode(out);
331-
r.encode(out);
332-
}
333-
#[cfg(feature = "optimism")]
334-
Self::PostExec(r) => {
335-
Header { list: true, payload_length: payload_len }.encode(out);
336-
POST_EXEC_TX_TYPE_ID.encode(out);
337-
r.encode(out);
338-
}
339-
#[cfg(feature = "optimism")]
340-
Self::Deposit(r) => {
341-
Header { list: true, payload_length: payload_len }.encode(out);
342-
DEPOSIT_TX_TYPE_ID.encode(out);
343-
r.encode(out);
344-
}
345-
Self::Tempo(r) => {
346-
Header { list: true, payload_length: payload_len }.encode(out);
347-
TEMPO_TX_TYPE_ID.encode(out);
348-
r.encode(out);
349-
}
350-
_ => unreachable!("receipt already matched"),
351-
}
352-
}
353-
}
299+
fn length(&self) -> usize {
300+
self.network_len()
354301
}
355302
}
356303

357304
impl Decodable for FoundryReceiptEnvelope {
358305
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
359-
use bytes::Buf;
360-
use std::cmp::Ordering;
361-
362-
// a receipt is either encoded as a string (non legacy) or a list (legacy).
363-
// We should not consume the buffer if we are decoding a legacy receipt, so let's
364-
// check if the first byte is between 0x80 and 0xbf.
365-
let rlp_type = *buf
366-
.first()
367-
.ok_or(alloy_rlp::Error::Custom("cannot decode a receipt from empty bytes"))?;
368-
369-
match rlp_type.cmp(&alloy_rlp::EMPTY_LIST_CODE) {
370-
Ordering::Less => {
371-
// strip out the string header
372-
let _header = Header::decode(buf)?;
373-
let receipt_type = *buf.first().ok_or(alloy_rlp::Error::Custom(
374-
"typed receipt cannot be decoded from an empty slice",
375-
))?;
376-
if receipt_type == EIP2930_TX_TYPE_ID {
377-
buf.advance(1);
378-
<ReceiptWithBloom as Decodable>::decode(buf)
379-
.map(FoundryReceiptEnvelope::Eip2930)
380-
} else if receipt_type == EIP1559_TX_TYPE_ID {
381-
buf.advance(1);
382-
<ReceiptWithBloom as Decodable>::decode(buf)
383-
.map(FoundryReceiptEnvelope::Eip1559)
384-
} else if receipt_type == EIP4844_TX_TYPE_ID {
385-
buf.advance(1);
386-
<ReceiptWithBloom as Decodable>::decode(buf)
387-
.map(FoundryReceiptEnvelope::Eip4844)
388-
} else if receipt_type == EIP7702_TX_TYPE_ID {
389-
buf.advance(1);
390-
<ReceiptWithBloom as Decodable>::decode(buf)
391-
.map(FoundryReceiptEnvelope::Eip7702)
392-
} else if receipt_type == TEMPO_TX_TYPE_ID {
393-
buf.advance(1);
394-
<ReceiptWithBloom as Decodable>::decode(buf).map(FoundryReceiptEnvelope::Tempo)
395-
} else {
396-
#[cfg(feature = "optimism")]
397-
{
398-
if receipt_type == POST_EXEC_TX_TYPE_ID {
399-
buf.advance(1);
400-
return <ReceiptWithBloom as Decodable>::decode(buf)
401-
.map(FoundryReceiptEnvelope::PostExec);
402-
}
403-
if receipt_type == DEPOSIT_TX_TYPE_ID {
404-
buf.advance(1);
405-
return <OpDepositReceiptWithBloom as Decodable>::decode(buf)
406-
.map(FoundryReceiptEnvelope::Deposit);
407-
}
408-
}
409-
Err(alloy_rlp::Error::Custom("invalid receipt type"))
410-
}
411-
}
412-
Ordering::Equal => {
413-
Err(alloy_rlp::Error::Custom("an empty list is not a valid receipt encoding"))
414-
}
415-
Ordering::Greater => {
416-
<ReceiptWithBloom as Decodable>::decode(buf).map(FoundryReceiptEnvelope::Legacy)
417-
}
418-
}
306+
Self::network_decode(buf).map_err(Into::into)
419307
}
420308
}
421309

@@ -532,6 +420,45 @@ mod tests {
532420
.map_logs(|log| log.inner)
533421
}
534422

423+
#[test]
424+
fn rlp_roundtrip() {
425+
fn assert_roundtrip(receipt: FoundryReceiptEnvelope) {
426+
let mut encoded = Vec::new();
427+
receipt.encode(&mut encoded);
428+
assert_eq!(encoded.len(), receipt.length());
429+
430+
let mut encoded = encoded.as_slice();
431+
let decoded = FoundryReceiptEnvelope::decode(&mut encoded).unwrap();
432+
assert_eq!(decoded, receipt);
433+
assert!(encoded.is_empty());
434+
}
435+
436+
for tx_type in [
437+
FoundryTxType::Legacy,
438+
FoundryTxType::Eip2930,
439+
FoundryTxType::Eip1559,
440+
FoundryTxType::Eip4844,
441+
FoundryTxType::Eip7702,
442+
FoundryTxType::Tempo,
443+
] {
444+
assert_roundtrip(receipt_for(tx_type));
445+
}
446+
#[cfg(feature = "optimism")]
447+
for tx_type in [FoundryTxType::PostExec, FoundryTxType::Deposit] {
448+
assert_roundtrip(receipt_for(tx_type));
449+
}
450+
}
451+
452+
#[test]
453+
fn encode_typed_receipt_uses_rlp_string() {
454+
let receipt = receipt_for(FoundryTxType::Eip2930);
455+
let mut encoded = Vec::new();
456+
receipt.encode(&mut encoded);
457+
458+
// Long string containing a 266-byte EIP-2718 envelope, beginning with type 0x01.
459+
assert_eq!(&encoded[..4], &[0xb9, 0x01, 0x0a, EIP2930_TX_TYPE_ID]);
460+
}
461+
535462
#[test]
536463
fn receipt_predicates() {
537464
assert!(receipt_for(FoundryTxType::Legacy).is_legacy());

0 commit comments

Comments
 (0)