From 23b524903575e0b0d38ea98e13c7df4a65f90137 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Wed, 9 Sep 2026 20:05:32 +0200 Subject: [PATCH 1/3] fix: decode EIP-7702 static-file transactions --- Cargo.lock | 1 + Cargo.toml | 1 + crates/ev-primitives/Cargo.toml | 1 + crates/ev-primitives/src/tx.rs | 127 +++++++++++++++++++++++++++++--- 4 files changed, 121 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b51604e..4a7e76fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3083,6 +3083,7 @@ dependencies = [ "reth-db-api", "reth-ethereum-primitives", "reth-primitives-traits", + "reth-zstd-compressors", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index cb1c1657..45ad660f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ reth-rpc-engine-api = { git = "https://github.com/paradigmxyz/reth.git", tag = " reth-rpc = { git = "https://github.com/paradigmxyz/reth.git", tag = "v2.5.0" } reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth.git", tag = "v2.5.0" } reth-codecs = { version = "0.6.0", default-features = false } +reth-zstd-compressors = { version = "0.6.0", default-features = false } ev-revm = { path = "crates/ev-revm" } ev-primitives = { path = "crates/ev-primitives" } diff --git a/crates/ev-primitives/Cargo.toml b/crates/ev-primitives/Cargo.toml index bb0746aa..73acffaf 100644 --- a/crates/ev-primitives/Cargo.toml +++ b/crates/ev-primitives/Cargo.toml @@ -13,6 +13,7 @@ alloy-serde = { workspace = true } alloy-rlp = { workspace = true, features = ["derive"] } bytes = { workspace = true } reth-codecs = { workspace = true, features = ["alloy"] } +reth-zstd-compressors = { workspace = true } reth-db-api = { workspace = true } reth-ethereum-primitives = { workspace = true } reth-primitives-traits = { workspace = true } diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index 7555acbf..ba2e5111 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -436,18 +436,18 @@ impl Compact for EvTxType { /// Decodes `EvTxType` from compact format. /// - /// # Panics - /// Panics if an unknown transaction type identifier is encountered. This indicates - /// database corruption or a version mismatch - the node should not continue. + /// Standard Ethereum extended identifiers are delegated to `TxType`. This is important for + /// EIP-7702 (`0x04`), which was written to existing static files before EvNode introduced its + /// own extended identifier. fn from_compact(mut buf: &[u8], identifier: usize) -> (Self, &[u8]) { match identifier { COMPACT_EXTENDED_IDENTIFIER_FLAG => { - let extended_identifier = buf.get_u8(); - match extended_identifier { - EVNODE_TX_TYPE_ID => (Self::EvNode, buf), - _ => panic!( - "failed to decode EvTxType from database: unknown identifier {extended_identifier:#x}" - ), + if buf.first() == Some(&EVNODE_TX_TYPE_ID) { + buf.advance(1); + (Self::EvNode, buf) + } else { + let (inner, buf) = alloy_consensus::TxType::from_compact(buf, identifier); + (Self::Ethereum(inner), buf) } } v => { @@ -531,11 +531,62 @@ impl Compress for EvTxEnvelope { impl Decompress for EvTxEnvelope { fn decompress(value: &[u8]) -> Result { + ensure_supported_compact_tx_type(value)?; let (obj, _) = Compact::from_compact(value, value.len()); Ok(obj) } } +/// Rejects unknown extended transaction identifiers before the infallible compact decoder runs. +/// +/// `Compact` predates fallible database decoding and therefore panics for unsupported types. The +/// static-file path uses `Decompress`, so validating here turns an unsupported on-disk type into a +/// normal database decoding error instead of terminating the cache worker task. +fn ensure_supported_compact_tx_type(value: &[u8]) -> Result<(), DecompressError> { + const COMPACT_HEADER_LEN: usize = 1 + 64; + + let flags = *value + .first() + .ok_or_else(|| compact_decode_error("missing compact header"))?; + let identifier = (flags & 0b110) >> 1; + if identifier != COMPACT_EXTENDED_IDENTIFIER_FLAG as u8 { + return Ok(()); + } + + let tx_type = if flags >> 3 == 0 { + *value + .get(COMPACT_HEADER_LEN) + .ok_or_else(|| compact_decode_error("missing extended transaction identifier"))? + } else { + if value.len() < COMPACT_HEADER_LEN { + return Err(compact_decode_error("missing compact signature")); + } + reth_zstd_compressors::with_tx_decompressor(|decompressor| { + decompressor + .decompress(&value[COMPACT_HEADER_LEN..]) + .first() + .copied() + }) + .ok_or_else(|| compact_decode_error("missing compressed transaction identifier"))? + }; + + match tx_type { + alloy_consensus::constants::EIP4844_TX_TYPE_ID + | alloy_consensus::constants::EIP7702_TX_TYPE_ID + | EVNODE_TX_TYPE_ID => Ok(()), + _ => Err(compact_decode_error(format!( + "unsupported compact transaction identifier {tx_type:#x}" + ))), + } +} + +fn compact_decode_error(message: impl Into) -> DecompressError { + DecompressError::new(std::io::Error::new( + std::io::ErrorKind::InvalidData, + message.into(), + )) +} + fn optional_signature_length(value: Option<&Signature>) -> usize { match value { Some(sig) => sig.as_bytes().as_slice().length(), @@ -566,6 +617,7 @@ fn decode_optional_signature(buf: &mut &[u8]) -> alloy_rlp::Result Signature { @@ -635,4 +687,61 @@ mod tests { let err = decode_optional_signature(&mut buf).expect_err("invalid length"); assert_eq!(err, alloy_rlp::Error::UnexpectedLength); } + + #[test] + fn compact_eip7702_type_roundtrip() { + let mut encoded = Vec::new(); + let identifier = + EvTxType::Ethereum(alloy_consensus::TxType::Eip7702).to_compact(&mut encoded); + + assert_eq!(identifier, COMPACT_EXTENDED_IDENTIFIER_FLAG); + assert_eq!(encoded, [alloy_consensus::constants::EIP7702_TX_TYPE_ID]); + + let (decoded, remaining) = EvTxType::from_compact(&encoded, identifier); + assert_eq!( + decoded, + EvTxType::Ethereum(alloy_consensus::TxType::Eip7702) + ); + assert!(remaining.is_empty()); + } + + #[test] + fn static_file_compact_eip7702_transaction_roundtrip() { + let transaction = TxEip7702 { + chain_id: 1, + nonce: 1, + gas_limit: 30_000, + max_fee_per_gas: 2, + max_priority_fee_per_gas: 1, + to: Address::ZERO, + value: U256::ZERO, + access_list: AccessList::default(), + authorization_list: Vec::new(), + input: Bytes::new(), + }; + let signed = alloy_consensus::Signed::new_unhashed(transaction, sample_signature()); + let envelope = EvTxEnvelope::Ethereum(signed.into()); + + let compressed = envelope.compress(); + let decoded = + EvTxEnvelope::decompress(&compressed).expect("decode static-file transaction"); + + assert!(matches!( + decoded, + EvTxEnvelope::Ethereum(ref tx) + if tx.tx_type() == alloy_consensus::TxType::Eip7702 + )); + } + + #[test] + fn static_file_unknown_extended_type_returns_error() { + let mut encoded = vec![(COMPACT_EXTENDED_IDENTIFIER_FLAG as u8) << 1]; + encoded.extend_from_slice(&[0; 64]); + encoded.push(0x7f); + + let err = EvTxEnvelope::decompress(&encoded).expect_err("unsupported type must fail"); + assert!(err + .to_string() + .contains("unsupported compact transaction identifier 0x7f")); + } } From 4efb7b7c225383bddb60da8ab56a478c1b7adba4 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Wed, 9 Sep 2026 20:19:08 +0200 Subject: [PATCH 2/3] perf: avoid duplicate compact transaction decompression --- Cargo.lock | 1 - Cargo.toml | 1 - crates/ev-primitives/Cargo.toml | 1 - crates/ev-primitives/src/tx.rs | 40 ++++++++++++++++----------------- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a7e76fa..1b51604e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3083,7 +3083,6 @@ dependencies = [ "reth-db-api", "reth-ethereum-primitives", "reth-primitives-traits", - "reth-zstd-compressors", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 45ad660f..cb1c1657 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,6 @@ reth-rpc-engine-api = { git = "https://github.com/paradigmxyz/reth.git", tag = " reth-rpc = { git = "https://github.com/paradigmxyz/reth.git", tag = "v2.5.0" } reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth.git", tag = "v2.5.0" } reth-codecs = { version = "0.6.0", default-features = false } -reth-zstd-compressors = { version = "0.6.0", default-features = false } ev-revm = { path = "crates/ev-revm" } ev-primitives = { path = "crates/ev-primitives" } diff --git a/crates/ev-primitives/Cargo.toml b/crates/ev-primitives/Cargo.toml index 73acffaf..bb0746aa 100644 --- a/crates/ev-primitives/Cargo.toml +++ b/crates/ev-primitives/Cargo.toml @@ -13,7 +13,6 @@ alloy-serde = { workspace = true } alloy-rlp = { workspace = true, features = ["derive"] } bytes = { workspace = true } reth-codecs = { workspace = true, features = ["alloy"] } -reth-zstd-compressors = { workspace = true } reth-db-api = { workspace = true } reth-ethereum-primitives = { workspace = true } reth-primitives-traits = { workspace = true } diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index ba2e5111..50ec8419 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -537,11 +537,14 @@ impl Decompress for EvTxEnvelope { } } -/// Rejects unknown extended transaction identifiers before the infallible compact decoder runs. +/// Rejects unknown uncompressed extended transaction identifiers before the infallible compact +/// decoder runs. /// /// `Compact` predates fallible database decoding and therefore panics for unsupported types. The -/// static-file path uses `Decompress`, so validating here turns an unsupported on-disk type into a -/// normal database decoding error instead of terminating the cache worker task. +/// static-file path uses `Decompress`, so validating an uncompressed unsupported on-disk type here +/// turns it into a normal database decoding error instead of terminating the cache worker task. +/// Compressed transactions are left to the compact decoder so a static-file read performs only one +/// zstd decompression. fn ensure_supported_compact_tx_type(value: &[u8]) -> Result<(), DecompressError> { const COMPACT_HEADER_LEN: usize = 1 + 64; @@ -553,22 +556,13 @@ fn ensure_supported_compact_tx_type(value: &[u8]) -> Result<(), DecompressError> return Ok(()); } - let tx_type = if flags >> 3 == 0 { - *value - .get(COMPACT_HEADER_LEN) - .ok_or_else(|| compact_decode_error("missing extended transaction identifier"))? - } else { - if value.len() < COMPACT_HEADER_LEN { - return Err(compact_decode_error("missing compact signature")); - } - reth_zstd_compressors::with_tx_decompressor(|decompressor| { - decompressor - .decompress(&value[COMPACT_HEADER_LEN..]) - .first() - .copied() - }) - .ok_or_else(|| compact_decode_error("missing compressed transaction identifier"))? - }; + if flags >> 3 != 0 { + return Ok(()); + } + + let tx_type = *value + .get(COMPACT_HEADER_LEN) + .ok_or_else(|| compact_decode_error("missing extended transaction identifier"))?; match tx_type { alloy_consensus::constants::EIP4844_TX_TYPE_ID @@ -717,12 +711,18 @@ mod tests { value: U256::ZERO, access_list: AccessList::default(), authorization_list: Vec::new(), - input: Bytes::new(), + // Reth's compact envelope enables zstd compression at 32 bytes of calldata. + input: Bytes::from(vec![0; 32]), }; let signed = alloy_consensus::Signed::new_unhashed(transaction, sample_signature()); let envelope = EvTxEnvelope::Ethereum(signed.into()); let compressed = envelope.compress(); + assert_ne!( + compressed[0] >> 3, + 0, + "transaction should use zstd compact encoding" + ); let decoded = EvTxEnvelope::decompress(&compressed).expect("decode static-file transaction"); From 4dfff22f7712bbfe01b97db55329ce7af421a39c Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Wed, 9 Sep 2026 20:25:59 +0200 Subject: [PATCH 3/3] Update tx.rs --- crates/ev-primitives/src/tx.rs | 95 ++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/crates/ev-primitives/src/tx.rs b/crates/ev-primitives/src/tx.rs index 50ec8419..efa9830b 100644 --- a/crates/ev-primitives/src/tx.rs +++ b/crates/ev-primitives/src/tx.rs @@ -436,9 +436,11 @@ impl Compact for EvTxType { /// Decodes `EvTxType` from compact format. /// - /// Standard Ethereum extended identifiers are delegated to `TxType`. This is important for - /// EIP-7702 (`0x04`), which was written to existing static files before EvNode introduced its - /// own extended identifier. + /// Static files store EIP-7702 as `0x04`, not `0x76`. + /// + /// # Panics + /// + /// Panics on an unknown extended identifier or a truncated buffer. fn from_compact(mut buf: &[u8], identifier: usize) -> (Self, &[u8]) { match identifier { COMPACT_EXTENDED_IDENTIFIER_FLAG => { @@ -537,14 +539,8 @@ impl Decompress for EvTxEnvelope { } } -/// Rejects unknown uncompressed extended transaction identifiers before the infallible compact -/// decoder runs. -/// -/// `Compact` predates fallible database decoding and therefore panics for unsupported types. The -/// static-file path uses `Decompress`, so validating an uncompressed unsupported on-disk type here -/// turns it into a normal database decoding error instead of terminating the cache worker task. -/// Compressed transactions are left to the compact decoder so a static-file read performs only one -/// zstd decompression. +/// Rejects unknown uncompressed extended identifiers so `Decompress` returns an error instead of +/// panicking in `Compact`. Compressed payloads skip this check to avoid a second zstd pass. fn ensure_supported_compact_tx_type(value: &[u8]) -> Result<(), DecompressError> { const COMPACT_HEADER_LEN: usize = 1 + 64; @@ -637,6 +633,38 @@ mod tests { } } + fn sample_eip7702_envelope(input: Bytes) -> EvTxEnvelope { + let transaction = TxEip7702 { + chain_id: 1, + nonce: 1, + gas_limit: 30_000, + max_fee_per_gas: 2, + max_priority_fee_per_gas: 1, + to: Address::ZERO, + value: U256::ZERO, + access_list: AccessList::default(), + authorization_list: Vec::new(), + input, + }; + let signed = alloy_consensus::Signed::new_unhashed(transaction, sample_signature()); + EvTxEnvelope::Ethereum(signed.into()) + } + + fn assert_eip7702_static_file_roundtrip(input: Bytes, zstd: bool) { + let encoded = sample_eip7702_envelope(input).compress(); + assert_eq!( + encoded[0] >> 3 != 0, + zstd, + "compact zstd flag should match calldata size" + ); + let decoded = EvTxEnvelope::decompress(&encoded).expect("decode static-file transaction"); + assert!(matches!( + decoded, + EvTxEnvelope::Ethereum(ref tx) + if tx.tx_type() == alloy_consensus::TxType::Eip7702 + )); + } + #[test] fn executor_signing_hash_ignores_sponsor_fields() { let mut tx = sample_tx(); @@ -701,36 +729,13 @@ mod tests { #[test] fn static_file_compact_eip7702_transaction_roundtrip() { - let transaction = TxEip7702 { - chain_id: 1, - nonce: 1, - gas_limit: 30_000, - max_fee_per_gas: 2, - max_priority_fee_per_gas: 1, - to: Address::ZERO, - value: U256::ZERO, - access_list: AccessList::default(), - authorization_list: Vec::new(), - // Reth's compact envelope enables zstd compression at 32 bytes of calldata. - input: Bytes::from(vec![0; 32]), - }; - let signed = alloy_consensus::Signed::new_unhashed(transaction, sample_signature()); - let envelope = EvTxEnvelope::Ethereum(signed.into()); - - let compressed = envelope.compress(); - assert_ne!( - compressed[0] >> 3, - 0, - "transaction should use zstd compact encoding" - ); - let decoded = - EvTxEnvelope::decompress(&compressed).expect("decode static-file transaction"); + assert_eip7702_static_file_roundtrip(Bytes::new(), false); + } - assert!(matches!( - decoded, - EvTxEnvelope::Ethereum(ref tx) - if tx.tx_type() == alloy_consensus::TxType::Eip7702 - )); + #[test] + fn static_file_compact_eip7702_compressed_transaction_roundtrip() { + // CompactEnvelope sets the zstd bit at 32 bytes of calldata. + assert_eip7702_static_file_roundtrip(Bytes::from(vec![0; 32]), true); } #[test] @@ -744,4 +749,14 @@ mod tests { .to_string() .contains("unsupported compact transaction identifier 0x7f")); } + + #[test] + fn static_file_compressed_unknown_extended_type_is_skipped() { + let mut encoded = vec![((COMPACT_EXTENDED_IDENTIFIER_FLAG as u8) << 1) | (1 << 3)]; + encoded.extend_from_slice(&[0; 64]); + encoded.push(0x7f); + + ensure_supported_compact_tx_type(&encoded) + .expect("compressed identifiers are left to Compact"); + } }