diff --git a/crates/protocol/src/batch/errors.rs b/crates/protocol/src/batch/errors.rs new file mode 100644 index 000000000..8422f4aba --- /dev/null +++ b/crates/protocol/src/batch/errors.rs @@ -0,0 +1,77 @@ +//! Span Batch Errors + +/// Span Batch Errors +#[derive(Debug, derive_more::Display, Clone, PartialEq, Eq)] +pub enum SpanBatchError { + /// The span batch is too big + #[display("The span batch is too big.")] + TooBigSpanBatchSize, + /// The bit field is too long + #[display("The bit field is too long")] + BitfieldTooLong, + /// Empty Span Batch + #[display("Empty span batch")] + EmptySpanBatch, + /// Missing L1 origin + #[display("Missing L1 origin")] + MissingL1Origin, + /// Decoding errors + #[display("Span batch decoding error: {_0}")] + Decoding(SpanDecodingError), +} + +impl From for SpanBatchError { + fn from(err: SpanDecodingError) -> Self { + Self::Decoding(err) + } +} + +impl core::error::Error for SpanBatchError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Decoding(err) => Some(err), + _ => None, + } + } +} + +/// Decoding Error +#[derive(Debug, derive_more::Display, Clone, PartialEq, Eq)] +pub enum SpanDecodingError { + /// Failed to decode relative timestamp + #[display("Failed to decode relative timestamp")] + RelativeTimestamp, + /// Failed to decode L1 origin number + #[display("Failed to decode L1 origin number")] + L1OriginNumber, + /// Failed to decode parent check + #[display("Failed to decode parent check")] + ParentCheck, + /// Failed to decode L1 origin check + #[display("Failed to decode L1 origin check")] + L1OriginCheck, + /// Failed to decode block count + #[display("Failed to decode block count")] + BlockCount, + /// Failed to decode block tx counts + #[display("Failed to decode block tx counts")] + BlockTxCounts, + /// Failed to decode transaction nonces + #[display("Failed to decode transaction nonces")] + TxNonces, + /// Mismatch in length between the transaction type and signature arrays in a span batch + /// transaction payload. + #[display("Mismatch in length between the transaction type and signature arrays")] + TypeSignatureLenMismatch, + /// Invalid transaction type + #[display("Invalid transaction type")] + InvalidTransactionType, + /// Invalid transaction data + #[display("Invalid transaction data")] + InvalidTransactionData, + /// Invalid transaction signature + #[display("Invalid transaction signature")] + InvalidTransactionSignature, +} + +impl core::error::Error for SpanDecodingError {} diff --git a/crates/protocol/src/batch/mod.rs b/crates/protocol/src/batch/mod.rs index 3c35a48bd..4306e4381 100644 --- a/crates/protocol/src/batch/mod.rs +++ b/crates/protocol/src/batch/mod.rs @@ -3,6 +3,12 @@ mod r#type; pub use r#type::*; +mod errors; +pub use errors::{SpanBatchError, SpanDecodingError}; + +mod signature; +pub use signature::SpanBatchSignature; + mod validity; pub use validity::BatchValidity; diff --git a/crates/protocol/src/batch/signature.rs b/crates/protocol/src/batch/signature.rs new file mode 100644 index 000000000..f385d73ab --- /dev/null +++ b/crates/protocol/src/batch/signature.rs @@ -0,0 +1,42 @@ +//! This module contains the [SpanBatchSignature] type, which represents the ECDSA signature of a +//! transaction within a span batch. + +use crate::{SpanBatchError, SpanDecodingError}; +use alloy_primitives::{Signature, U256}; + +/// The ECDSA signature of a transaction within a span batch. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SpanBatchSignature { + pub(crate) v: u64, + pub(crate) r: U256, + pub(crate) s: U256, +} + +impl From for SpanBatchSignature { + fn from(value: Signature) -> Self { + Self { v: value.v().to_u64(), r: value.r(), s: value.s() } + } +} + +impl TryFrom for Signature { + type Error = SpanBatchError; + + fn try_from(value: SpanBatchSignature) -> Result { + Self::from_rs_and_parity(value.r, value.s, value.v) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionSignature)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::Signature; + + #[test] + fn test_span_batch_signature_conversion() { + let signature = Signature::from_rs_and_parity(U256::from(1), U256::from(2), 27).unwrap(); + let span_batch_signature = SpanBatchSignature::from(signature); + let converted_signature = Signature::try_from(span_batch_signature).unwrap(); + assert_eq!(signature, converted_signature); + } +} diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index d1d719e85..42c320158 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -11,8 +11,8 @@ extern crate alloc; mod batch; pub use batch::{ - BatchType, BatchValidationProvider, BatchValidity, SingleBatch, SINGLE_BATCH_TYPE, - SPAN_BATCH_TYPE, + BatchType, BatchValidationProvider, BatchValidity, SingleBatch, SpanBatchError, + SpanBatchSignature, SpanDecodingError, SINGLE_BATCH_TYPE, SPAN_BATCH_TYPE, }; mod block; @@ -30,7 +30,10 @@ mod iter; pub use iter::FrameIter; mod utils; -pub use utils::{starts_with_2718_deposit, to_system_config, OpBlockConversionError}; +pub use utils::{ + convert_v_to_y_parity, is_protected_v, read_tx_data, starts_with_2718_deposit, + to_system_config, OpBlockConversionError, +}; mod channel; pub use channel::{ diff --git a/crates/protocol/src/utils.rs b/crates/protocol/src/utils.rs index e415e1ad0..62b665800 100644 --- a/crates/protocol/src/utils.rs +++ b/crates/protocol/src/utils.rs @@ -1,10 +1,17 @@ //! Utility methods used by protocol types. -use crate::{block_info::DecodeError, L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoTx}; +use alloc::vec::Vec; +use alloy_consensus::{TxEnvelope, TxType}; use alloy_primitives::B256; +use alloy_rlp::{Buf, Header}; use op_alloy_consensus::{OpBlock, OpTxEnvelope}; use op_alloy_genesis::{RollupConfig, SystemConfig}; +use crate::{ + block_info::DecodeError, L1BlockInfoBedrock, L1BlockInfoEcotone, L1BlockInfoTx, SpanBatchError, + SpanDecodingError, +}; + /// Returns if the given `value` is a deposit transaction. pub fn starts_with_2718_deposit(value: &B) -> bool where @@ -224,9 +231,85 @@ fn u24(input: &[u8], idx: u32) -> u32 { + (u32::from(input[(idx + 2) as usize]) << 16) } +/// Reads transaction data from a reader. +#[allow(unused)] +pub fn read_tx_data(r: &mut &[u8]) -> Result<(Vec, TxType), SpanBatchError> { + let mut tx_data = Vec::new(); + let first_byte = + *r.first().ok_or(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + let mut tx_type = 0; + if first_byte <= 0x7F { + // EIP-2718: Non-legacy tx, so write tx type + tx_type = first_byte; + tx_data.push(tx_type); + r.advance(1); + } + + // Read the RLP header with a different reader pointer. This prevents the initial pointer from + // being advanced in the case that what we read is invalid. + let rlp_header = Header::decode(&mut (**r).as_ref()) + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData))?; + + let tx_payload = if rlp_header.list { + // Grab the raw RLP for the transaction data from `r`. It was unaffected since we copied it. + let payload_length_with_header = rlp_header.payload_length + rlp_header.length(); + let payload = r[0..payload_length_with_header].to_vec(); + r.advance(payload_length_with_header); + Ok(payload) + } else { + Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionData)) + }?; + tx_data.extend_from_slice(&tx_payload); + + Ok(( + tx_data, + tx_type + .try_into() + .map_err(|_| SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType))?, + )) +} + +/// Converts a `v` value to a y parity bit, from the transaaction type. +#[allow(unused)] +pub const fn convert_v_to_y_parity(v: u64, tx_type: TxType) -> Result { + match tx_type { + TxType::Legacy => { + if v != 27 && v != 28 { + // EIP-155: v = 2 * chain_id + 35 + yParity + Ok((v - 35) & 1 == 1) + } else { + // Unprotected legacy txs must have v = 27 or 28 + Ok(v - 27 == 1) + } + } + TxType::Eip2930 | TxType::Eip1559 => Ok(v == 1), + _ => Err(SpanBatchError::Decoding(SpanDecodingError::InvalidTransactionType)), + } +} + +/// Checks if the signature of the passed [TxEnvelope] is protected. +#[allow(unused)] +pub const fn is_protected_v(tx: &TxEnvelope) -> bool { + match tx { + TxEnvelope::Legacy(tx) => { + let v = tx.signature().v().to_u64(); + if 64 - v.leading_zeros() <= 8 { + return v != 27 && v != 28 && v != 1 && v != 0; + } + // anything not 27 or 28 is considered protected + true + } + _ => true, + } +} + #[cfg(test)] mod tests { use super::*; + use alloy_consensus::{ + Signed, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxEip7702, TxLegacy, + }; + use alloy_primitives::{b256, Signature}; use alloy_sol_types::{sol, SolCall}; use revm::{ db::BenchmarkDB, @@ -237,6 +320,63 @@ mod tests { use rstest::rstest; + #[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(), + )))); + } + #[rstest] #[case::empty(&[], 0)] #[case::thousand_zeros(&[0; 1000], 21)]