diff --git a/crates/flashtypes/src/block.rs b/crates/flashtypes/src/block.rs index df1ddaf0..e9a2cdb0 100644 --- a/crates/flashtypes/src/block.rs +++ b/crates/flashtypes/src/block.rs @@ -46,6 +46,26 @@ impl Flashblock { }) } + /// Returns true if this flashblock carries the base payload data. + pub fn has_base_payload(&self) -> bool { + self.base.is_some() + } + + /// Returns true if this is the first flashblock in the payload sequence. + pub fn is_first_chunk(&self) -> bool { + self.index == 0 + } + + /// Returns the number of transactions carried by this flashblock. + pub fn transaction_count(&self) -> usize { + self.diff.transactions.len() + } + + /// Returns true if the flashblock includes withdrawals. + pub fn has_withdrawals(&self) -> bool { + !self.diff.withdrawals.is_empty() + } + fn try_parse_message(bytes: Bytes) -> Result { if let Ok(text) = std::str::from_utf8(&bytes) && text.trim_start().starts_with('{') @@ -106,6 +126,24 @@ mod tests { assert!(Flashblock::try_decode_message(bytes).is_err()); } + #[test] + fn helper_methods_reflect_flashblock_state() { + let mut payload = sample_payload(json!({ + "receipts": {}, + "new_account_balances": {}, + "block_number": 321u64 + })); + payload.index = 0; + + let flashblock = Flashblock::try_decode_message(encode_plain(&payload)) + .expect("payload should decode"); + + assert!(flashblock.has_base_payload()); + assert!(flashblock.is_first_chunk()); + assert_eq!(flashblock.transaction_count(), 1); + assert!(!flashblock.has_withdrawals()); + } + fn encode_plain(payload: &FlashblocksPayloadV1) -> Bytes { Bytes::from(serde_json::to_vec(payload).expect("serialize payload")) } diff --git a/crates/flashtypes/src/lib.rs b/crates/flashtypes/src/lib.rs index 16405685..637fae56 100644 --- a/crates/flashtypes/src/lib.rs +++ b/crates/flashtypes/src/lib.rs @@ -16,3 +16,9 @@ mod payload; pub use payload::{ ExecutionPayloadBaseV1, ExecutionPayloadFlashblockDeltaV1, FlashblocksPayloadV1, }; + +pub mod validation; +pub use validation::{ + is_non_zero_address, is_valid_blob_gas, is_valid_block_hash, is_valid_gas_usage, + is_valid_state_root, is_valid_timestamp, is_valid_transaction_bytes, +}; diff --git a/crates/flashtypes/src/metadata.rs b/crates/flashtypes/src/metadata.rs index daa4190c..54703f56 100644 --- a/crates/flashtypes/src/metadata.rs +++ b/crates/flashtypes/src/metadata.rs @@ -14,3 +14,62 @@ pub struct Metadata { /// Block number this flashblock belongs to. pub block_number: u64, } + +impl Metadata { + /// Returns true if there are no receipts or balance updates. + pub fn is_empty(&self) -> bool { + self.receipts.is_empty() && self.new_account_balances.is_empty() + } + + /// Returns the number of receipts tracked by this metadata. + pub fn receipts_len(&self) -> usize { + self.receipts.len() + } + + /// Returns the number of balance updates in this metadata. + pub fn balance_updates_len(&self) -> usize { + self.new_account_balances.len() + } + + /// Fetches the updated balance for a given address, if it exists. + pub fn balance_for(&self, address: &Address) -> Option<&U256> { + self.new_account_balances.get(address) + } + + /// Returns true if there is a balance update for the provided address. + pub fn has_balance_update(&self, address: &Address) -> bool { + self.new_account_balances.contains_key(address) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reports_empty_metadata() { + let metadata = Metadata::default(); + + assert!(metadata.is_empty()); + assert_eq!(metadata.receipts_len(), 0); + assert_eq!(metadata.balance_updates_len(), 0); + } + + #[test] + fn reports_balance_updates() { + let mut balances = HashMap::default(); + let address = Address::from([0x11u8; 20]); + balances.insert(address, U256::from(42u64)); + + let metadata = Metadata { + receipts: HashMap::default(), + new_account_balances: balances, + block_number: 1, + }; + + assert!(!metadata.is_empty()); + assert_eq!(metadata.balance_updates_len(), 1); + assert!(metadata.has_balance_update(&address)); + assert_eq!(metadata.balance_for(&address), Some(&U256::from(42u64))); + } +} diff --git a/crates/flashtypes/src/validation.rs b/crates/flashtypes/src/validation.rs new file mode 100644 index 00000000..7cc6276f --- /dev/null +++ b/crates/flashtypes/src/validation.rs @@ -0,0 +1,78 @@ +use alloy_primitives::{Address, B256}; + +/// Returns true if the address is non-zero. +pub fn is_non_zero_address(address: Address) -> bool { + address != Address::ZERO +} + +/// Returns true if the state root is non-zero. +pub fn is_valid_state_root(state_root: B256) -> bool { + state_root != B256::ZERO +} + +/// Returns true if the block hash is non-zero. +pub fn is_valid_block_hash(block_hash: B256) -> bool { + block_hash != B256::ZERO +} + +/// Returns true if blob gas is either absent or positive. +pub fn is_valid_blob_gas(blob_gas_used: Option) -> bool { + blob_gas_used.map_or(true, |value| value > 0) +} + +/// Returns true if gas used does not exceed the provided limit. +pub fn is_valid_gas_usage(gas_used: u64, gas_limit: u64) -> bool { + gas_limit > 0 && gas_used <= gas_limit +} + +/// Returns true if the timestamp is non-zero. +pub fn is_valid_timestamp(timestamp: u64) -> bool { + timestamp > 0 +} + +/// Returns true if the transaction bytes vector is non-empty. +pub fn is_valid_transaction_bytes(bytes: &[u8]) -> bool { + !bytes.is_empty() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validates_addresses() { + assert!(is_non_zero_address(Address::from([1u8; 20]))); + assert!(!is_non_zero_address(Address::ZERO)); + } + + #[test] + fn validates_hashes() { + assert!(is_valid_state_root(B256::from([0xAAu8; 32]))); + assert!(!is_valid_state_root(B256::ZERO)); + assert!(is_valid_block_hash(B256::from([0x11u8; 32]))); + assert!(!is_valid_block_hash(B256::ZERO)); + } + + #[test] + fn validates_blob_gas() { + assert!(is_valid_blob_gas(None)); + assert!(is_valid_blob_gas(Some(1))); + assert!(!is_valid_blob_gas(Some(0))); + } + + #[test] + fn validates_gas_usage() { + assert!(is_valid_gas_usage(5, 10)); + assert!(is_valid_gas_usage(0, 1)); + assert!(!is_valid_gas_usage(11, 10)); + assert!(!is_valid_gas_usage(1, 0)); + } + + #[test] + fn validates_timestamp_and_tx_bytes() { + assert!(is_valid_timestamp(1)); + assert!(!is_valid_timestamp(0)); + assert!(is_valid_transaction_bytes(&[0x01, 0x02])); + assert!(!is_valid_transaction_bytes(&[])); + } +}