diff --git a/Cargo.lock b/Cargo.lock index b891a2df8..633dc8a5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,9 +201,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a289ffd7448036f2f436b377f981c79ce0b2090877bad938d43387dc09931877" dependencies = [ "alloy-rlp", + "arbitrary", "bytes", "cfg-if", "const-hex", + "derive_arbitrary", "derive_more", "foldhash", "hashbrown 0.15.0", @@ -214,6 +216,7 @@ dependencies = [ "keccak-asm", "paste", "proptest", + "proptest-derive", "rand", "ruint", "rustc-hash 2.0.0", @@ -539,6 +542,12 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + [[package]] name = "ark-ff" version = "0.3.0" @@ -1262,6 +1271,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -1861,6 +1881,7 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ + "arbitrary", "equivalent", "hashbrown 0.15.0", "serde", @@ -3046,6 +3067,17 @@ dependencies = [ "unarray", ] +[[package]] +name = "proptest-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -3426,6 +3458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286" dependencies = [ "alloy-rlp", + "arbitrary", "ark-ff 0.3.0", "ark-ff 0.4.2", "bytes", diff --git a/crates/derive/Cargo.toml b/crates/derive/Cargo.toml index c4d714c41..7e2d8d518 100644 --- a/crates/derive/Cargo.toml +++ b/crates/derive/Cargo.toml @@ -56,6 +56,7 @@ spin.workspace = true anyhow.workspace = true alloy-rpc-client.workspace = true alloy-transport-http.workspace = true +alloy-primitives = { workspace = true, features = ["rlp", "k256", "map", "arbitrary"] } tokio.workspace = true proptest.workspace = true tracing-subscriber.workspace = true diff --git a/crates/derive/src/batch/span_batch/bits.rs b/crates/derive/src/batch/span_batch/bits.rs index 1932d213e..94f7859eb 100644 --- a/crates/derive/src/batch/span_batch/bits.rs +++ b/crates/derive/src/batch/span_batch/bits.rs @@ -9,25 +9,22 @@ use core::cmp::Ordering; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct SpanBatchBits(pub Vec); -impl AsRef> for SpanBatchBits { - fn as_ref(&self) -> &Vec { - &self.0 - } -} - impl AsRef<[u8]> for SpanBatchBits { fn as_ref(&self) -> &[u8] { &self.0 } } -impl From for Vec { - fn from(bits: SpanBatchBits) -> Self { - bits.0 +impl SpanBatchBits { + /// Returns the max amount of bytes that can be stored in the bitlist. + pub const fn max_bytes(is_fjord_active: bool) -> usize { + if is_fjord_active { + FJORD_MAX_SPAN_BATCH_BYTES as usize + } else { + MAX_SPAN_BATCH_BYTES as usize + } } -} -impl SpanBatchBits { /// Decodes a standard span-batch bitlist from a reader. /// The bitlist is encoded as big-endian integer, left-padded with zeroes to a multiple of 8 /// bits. The encoded bitlist cannot be longer than [MAX_SPAN_BATCH_BYTES]. @@ -37,11 +34,7 @@ impl SpanBatchBits { is_fjord_active: bool, ) -> Result { let buffer_len = bit_length / 8 + if bit_length % 8 != 0 { 1 } else { 0 }; - let max_bytes = if is_fjord_active { - FJORD_MAX_SPAN_BATCH_BYTES as usize - } else { - MAX_SPAN_BATCH_BYTES as usize - }; + let max_bytes = Self::max_bytes(is_fjord_active); if buffer_len > max_bytes { return Err(SpanBatchError::TooBigSpanBatchSize); } @@ -82,11 +75,7 @@ impl SpanBatchBits { // Round up, ensure enough bytes when number of bits is not a multiple of 8. // Alternative of (L+7)/8 is not overflow-safe. let buf_len = bit_length / 8 + if bit_length % 8 != 0 { 1 } else { 0 }; - let max_bytes = if is_fjord_active { - FJORD_MAX_SPAN_BATCH_BYTES as usize - } else { - MAX_SPAN_BATCH_BYTES as usize - }; + let max_bytes = Self::max_bytes(is_fjord_active); if buf_len > max_bytes { return Err(SpanBatchError::TooBigSpanBatchSize); } @@ -185,6 +174,12 @@ mod test { use super::*; use proptest::{collection::vec, prelude::any, proptest}; + #[test] + fn test_bitlist_max_bytes() { + assert_eq!(SpanBatchBits::max_bytes(false), MAX_SPAN_BATCH_BYTES as usize); + assert_eq!(SpanBatchBits::max_bytes(true), FJORD_MAX_SPAN_BATCH_BYTES as usize); + } + proptest! { #[test] fn test_encode_decode_roundtrip_span_bitlist(vec in vec(any::(), 0..5096)) { diff --git a/crates/derive/src/batch/span_batch/element.rs b/crates/derive/src/batch/span_batch/element.rs index 6af125335..a07065473 100644 --- a/crates/derive/src/batch/span_batch/element.rs +++ b/crates/derive/src/batch/span_batch/element.rs @@ -26,3 +26,27 @@ impl From for SpanBatchElement { } } } + +#[cfg(test)] +mod tests { + use super::*; + use proptest::{collection::vec, prelude::any, proptest}; + + proptest! { + #[test] + fn test_span_batch_element_from_single_batch(epoch_num in 0u64..u64::MAX, timestamp in 0u64..u64::MAX, transactions in vec(any::(), 0..100)) { + let single_batch = SingleBatch { + epoch_num, + timestamp, + transactions: transactions.clone(), + ..Default::default() + }; + + let span_batch_element: SpanBatchElement = single_batch.into(); + + assert_eq!(span_batch_element.epoch_num, epoch_num); + assert_eq!(span_batch_element.timestamp, timestamp); + assert_eq!(span_batch_element.transactions, transactions); + } + } +} diff --git a/crates/derive/src/batch/span_batch/transactions.rs b/crates/derive/src/batch/span_batch/transactions.rs index 17e9e50b8..c092f52ec 100644 --- a/crates/derive/src/batch/span_batch/transactions.rs +++ b/crates/derive/src/batch/span_batch/transactions.rs @@ -403,3 +403,94 @@ impl SpanBatchTransactions { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::{Signed, TxEip1559, TxEip2930, TxLegacy}; + use alloy_primitives::{address, Signature}; + + #[test] + fn test_span_batch_transactions_add_empty_txs() { + let mut span_batch_txs = SpanBatchTransactions::default(); + let txs = vec![]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert!(result.is_ok()); + assert_eq!(span_batch_txs.total_block_tx_count, 0); + } + + #[test] + fn test_span_batch_transactions_add_invalid_legacy_parity_decoding() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy { to: TxKind::Call(to), ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let err = span_batch_txs.add_txs(txs, chain_id).unwrap_err(); + assert_eq!(err, SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + + #[test] + fn test_span_batch_transactions_add_eip2930_tx_wrong_chain_id() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { to: TxKind::Call(to), ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let err = span_batch_txs.add_txs(txs, chain_id).unwrap_err(); + assert_eq!(err, SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)); + } + + #[test] + fn test_span_batch_transactions_add_eip2930_tx() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930 { to: TxKind::Call(to), chain_id: 1, ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert_eq!(result, Ok(())); + assert_eq!(span_batch_txs.total_block_tx_count, 1); + } + + #[test] + fn test_span_batch_transactions_add_eip1559_tx() { + let sig = Signature::test_signature(); + let to = address!("0123456789012345678901234567890123456789"); + let tx = TxEnvelope::Eip1559(Signed::new_unchecked( + TxEip1559 { to: TxKind::Call(to), chain_id: 1, ..Default::default() }, + sig, + Default::default(), + )); + let mut span_batch_txs = SpanBatchTransactions::default(); + let mut buf = vec![]; + tx.encode(&mut buf); + let txs = vec![Bytes::from(buf)]; + let chain_id = 1; + let result = span_batch_txs.add_txs(txs, chain_id); + assert_eq!(result, Ok(())); + assert_eq!(span_batch_txs.total_block_tx_count, 1); + } +} diff --git a/crates/derive/src/batch/span_batch/utils.rs b/crates/derive/src/batch/span_batch/utils.rs index a5d98be6e..06338cbe7 100644 --- a/crates/derive/src/batch/span_batch/utils.rs +++ b/crates/derive/src/batch/span_batch/utils.rs @@ -73,3 +73,69 @@ pub(crate) const fn is_protected_v(tx: &TxEnvelope) -> bool { _ => true, } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::{ + Signed, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxEip7702, TxLegacy, + }; + use alloy_primitives::{b256, Signature}; + + #[test] + fn test_convert_v_to_y_parity() { + assert_eq!(convert_v_to_y_parity(27, TxType::Legacy), Ok(false)); + assert_eq!(convert_v_to_y_parity(28, TxType::Legacy), Ok(true)); + assert_eq!(convert_v_to_y_parity(36, TxType::Legacy), Ok(true)); + assert_eq!(convert_v_to_y_parity(37, TxType::Legacy), Ok(false)); + assert_eq!(convert_v_to_y_parity(1, TxType::Eip2930), Ok(true)); + assert_eq!(convert_v_to_y_parity(1, TxType::Eip1559), Ok(true)); + assert_eq!( + convert_v_to_y_parity(1, TxType::Eip4844), + Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)) + ); + assert_eq!( + convert_v_to_y_parity(0, TxType::Eip7702), + Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)) + ); + } + + #[test] + fn test_is_protected_v() { + let sig = Signature::test_signature(); + assert!(!is_protected_v(&TxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy::default(), + sig, + Default::default(), + )))); + let r = b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"); + let s = b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"); + let v = 27; + let valid_sig = Signature::from_scalars_and_parity(r, s, v).unwrap(); + assert!(!is_protected_v(&TxEnvelope::Legacy(Signed::new_unchecked( + TxLegacy::default(), + valid_sig, + Default::default(), + )))); + assert!(is_protected_v(&TxEnvelope::Eip2930(Signed::new_unchecked( + TxEip2930::default(), + sig, + Default::default(), + )))); + assert!(is_protected_v(&TxEnvelope::Eip1559(Signed::new_unchecked( + TxEip1559::default(), + sig, + Default::default(), + )))); + assert!(is_protected_v(&TxEnvelope::Eip4844(Signed::new_unchecked( + TxEip4844Variant::TxEip4844(TxEip4844::default()), + sig, + Default::default(), + )))); + assert!(is_protected_v(&TxEnvelope::Eip7702(Signed::new_unchecked( + TxEip7702::default(), + sig, + Default::default(), + )))); + } +}