Skip to content

Commit 390ec03

Browse files
authored
fix: decode EIP-7702 static-file transactions (#403)
* fix: decode EIP-7702 static-file transactions * perf: avoid duplicate compact transaction decompression * Update tx.rs
1 parent d48ace0 commit 390ec03

1 file changed

Lines changed: 132 additions & 8 deletions

File tree

  • crates/ev-primitives/src

‎crates/ev-primitives/src/tx.rs‎

Lines changed: 132 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -436,18 +436,20 @@ impl Compact for EvTxType {
436436

437437
/// Decodes `EvTxType` from compact format.
438438
///
439+
/// Static files store EIP-7702 as `0x04`, not `0x76`.
440+
///
439441
/// # Panics
440-
/// Panics if an unknown transaction type identifier is encountered. This indicates
441-
/// database corruption or a version mismatch - the node should not continue.
442+
///
443+
/// Panics on an unknown extended identifier or a truncated buffer.
442444
fn from_compact(mut buf: &[u8], identifier: usize) -> (Self, &[u8]) {
443445
match identifier {
444446
COMPACT_EXTENDED_IDENTIFIER_FLAG => {
445-
let extended_identifier = buf.get_u8();
446-
match extended_identifier {
447-
EVNODE_TX_TYPE_ID => (Self::EvNode, buf),
448-
_ => panic!(
449-
"failed to decode EvTxType from database: unknown identifier {extended_identifier:#x}"
450-
),
447+
if buf.first() == Some(&EVNODE_TX_TYPE_ID) {
448+
buf.advance(1);
449+
(Self::EvNode, buf)
450+
} else {
451+
let (inner, buf) = alloy_consensus::TxType::from_compact(buf, identifier);
452+
(Self::Ethereum(inner), buf)
451453
}
452454
}
453455
v => {
@@ -531,11 +533,50 @@ impl Compress for EvTxEnvelope {
531533

532534
impl Decompress for EvTxEnvelope {
533535
fn decompress(value: &[u8]) -> Result<Self, DecompressError> {
536+
ensure_supported_compact_tx_type(value)?;
534537
let (obj, _) = Compact::from_compact(value, value.len());
535538
Ok(obj)
536539
}
537540
}
538541

542+
/// Rejects unknown uncompressed extended identifiers so `Decompress` returns an error instead of
543+
/// panicking in `Compact`. Compressed payloads skip this check to avoid a second zstd pass.
544+
fn ensure_supported_compact_tx_type(value: &[u8]) -> Result<(), DecompressError> {
545+
const COMPACT_HEADER_LEN: usize = 1 + 64;
546+
547+
let flags = *value
548+
.first()
549+
.ok_or_else(|| compact_decode_error("missing compact header"))?;
550+
let identifier = (flags & 0b110) >> 1;
551+
if identifier != COMPACT_EXTENDED_IDENTIFIER_FLAG as u8 {
552+
return Ok(());
553+
}
554+
555+
if flags >> 3 != 0 {
556+
return Ok(());
557+
}
558+
559+
let tx_type = *value
560+
.get(COMPACT_HEADER_LEN)
561+
.ok_or_else(|| compact_decode_error("missing extended transaction identifier"))?;
562+
563+
match tx_type {
564+
alloy_consensus::constants::EIP4844_TX_TYPE_ID
565+
| alloy_consensus::constants::EIP7702_TX_TYPE_ID
566+
| EVNODE_TX_TYPE_ID => Ok(()),
567+
_ => Err(compact_decode_error(format!(
568+
"unsupported compact transaction identifier {tx_type:#x}"
569+
))),
570+
}
571+
}
572+
573+
fn compact_decode_error(message: impl Into<String>) -> DecompressError {
574+
DecompressError::new(std::io::Error::new(
575+
std::io::ErrorKind::InvalidData,
576+
message.into(),
577+
))
578+
}
579+
539580
fn optional_signature_length(value: Option<&Signature>) -> usize {
540581
match value {
541582
Some(sig) => sig.as_bytes().as_slice().length(),
@@ -566,6 +607,7 @@ fn decode_optional_signature(buf: &mut &[u8]) -> alloy_rlp::Result<Option<Signat
566607
#[cfg(test)]
567608
mod tests {
568609
use super::*;
610+
use alloy_consensus::TxEip7702;
569611
use alloy_eips::eip2930::AccessList;
570612

571613
fn sample_signature() -> Signature {
@@ -591,6 +633,38 @@ mod tests {
591633
}
592634
}
593635

636+
fn sample_eip7702_envelope(input: Bytes) -> EvTxEnvelope {
637+
let transaction = TxEip7702 {
638+
chain_id: 1,
639+
nonce: 1,
640+
gas_limit: 30_000,
641+
max_fee_per_gas: 2,
642+
max_priority_fee_per_gas: 1,
643+
to: Address::ZERO,
644+
value: U256::ZERO,
645+
access_list: AccessList::default(),
646+
authorization_list: Vec::new(),
647+
input,
648+
};
649+
let signed = alloy_consensus::Signed::new_unhashed(transaction, sample_signature());
650+
EvTxEnvelope::Ethereum(signed.into())
651+
}
652+
653+
fn assert_eip7702_static_file_roundtrip(input: Bytes, zstd: bool) {
654+
let encoded = sample_eip7702_envelope(input).compress();
655+
assert_eq!(
656+
encoded[0] >> 3 != 0,
657+
zstd,
658+
"compact zstd flag should match calldata size"
659+
);
660+
let decoded = EvTxEnvelope::decompress(&encoded).expect("decode static-file transaction");
661+
assert!(matches!(
662+
decoded,
663+
EvTxEnvelope::Ethereum(ref tx)
664+
if tx.tx_type() == alloy_consensus::TxType::Eip7702
665+
));
666+
}
667+
594668
#[test]
595669
fn executor_signing_hash_ignores_sponsor_fields() {
596670
let mut tx = sample_tx();
@@ -635,4 +709,54 @@ mod tests {
635709
let err = decode_optional_signature(&mut buf).expect_err("invalid length");
636710
assert_eq!(err, alloy_rlp::Error::UnexpectedLength);
637711
}
712+
713+
#[test]
714+
fn compact_eip7702_type_roundtrip() {
715+
let mut encoded = Vec::new();
716+
let identifier =
717+
EvTxType::Ethereum(alloy_consensus::TxType::Eip7702).to_compact(&mut encoded);
718+
719+
assert_eq!(identifier, COMPACT_EXTENDED_IDENTIFIER_FLAG);
720+
assert_eq!(encoded, [alloy_consensus::constants::EIP7702_TX_TYPE_ID]);
721+
722+
let (decoded, remaining) = EvTxType::from_compact(&encoded, identifier);
723+
assert_eq!(
724+
decoded,
725+
EvTxType::Ethereum(alloy_consensus::TxType::Eip7702)
726+
);
727+
assert!(remaining.is_empty());
728+
}
729+
730+
#[test]
731+
fn static_file_compact_eip7702_transaction_roundtrip() {
732+
assert_eip7702_static_file_roundtrip(Bytes::new(), false);
733+
}
734+
735+
#[test]
736+
fn static_file_compact_eip7702_compressed_transaction_roundtrip() {
737+
// CompactEnvelope sets the zstd bit at 32 bytes of calldata.
738+
assert_eip7702_static_file_roundtrip(Bytes::from(vec![0; 32]), true);
739+
}
740+
741+
#[test]
742+
fn static_file_unknown_extended_type_returns_error() {
743+
let mut encoded = vec![(COMPACT_EXTENDED_IDENTIFIER_FLAG as u8) << 1];
744+
encoded.extend_from_slice(&[0; 64]);
745+
encoded.push(0x7f);
746+
747+
let err = EvTxEnvelope::decompress(&encoded).expect_err("unsupported type must fail");
748+
assert!(err
749+
.to_string()
750+
.contains("unsupported compact transaction identifier 0x7f"));
751+
}
752+
753+
#[test]
754+
fn static_file_compressed_unknown_extended_type_is_skipped() {
755+
let mut encoded = vec![((COMPACT_EXTENDED_IDENTIFIER_FLAG as u8) << 1) | (1 << 3)];
756+
encoded.extend_from_slice(&[0; 64]);
757+
encoded.push(0x7f);
758+
759+
ensure_supported_compact_tx_type(&encoded)
760+
.expect("compressed identifiers are left to Compact");
761+
}
638762
}

0 commit comments

Comments
 (0)