diff --git a/crates/rbuilder-primitives/src/lib.rs b/crates/rbuilder-primitives/src/lib.rs index 2a39a921d..e90681b71 100644 --- a/crates/rbuilder-primitives/src/lib.rs +++ b/crates/rbuilder-primitives/src/lib.rs @@ -40,6 +40,8 @@ pub use test_data_generator::TestDataGenerator; use thiserror::Error; use uuid::Uuid; +use crate::serialize::TxEncoding; + /// Extra metadata for an order. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Metadata { @@ -167,7 +169,7 @@ pub struct BundleRefund { pub delayed: bool, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum BundleVersion { V1, V2, @@ -670,6 +672,47 @@ impl ShareBundle { } } +#[derive(Error, Debug, derive_more::From)] +pub enum TxWithBlobsCreateError { + #[error("Failed to decode transaction, error: {0}")] + FailedToDecodeTransaction(Eip2718Error), + #[error("Invalid transaction signature")] + InvalidTransactionSignature, + #[error("UnexpectedError")] + UnexpectedError, + /// This error is generated when we fail (like FailedToDecodeTransaction) parsing in TxEncoding::WithBlobData mode (Network encoding) but the header looks + /// like the beginning of an ethereum mainnet Canonical encoding 4484 tx. + /// To avoid consuming resources the generation of this error might not be perfect but helps 99% of the time. + #[error("Failed to decode transaction, error: {0}. It probably is a 4484 canonical tx.")] + FailedToDecodeTransactionProbablyIs4484Canonical(alloy_rlp::Error), + #[error("Tried to create an EIP4844 transaction without a blob")] + Eip4844MissingBlobSidecar, + #[error("Tried to create a non-EIP4844 transaction while passing blobs")] + BlobsMissingEip4844, + #[error("BlobStoreError: {0}")] + BlobStore(BlobStoreError), +} + +trait FakeSidecar { + fn fake_sidecar(blob_versioned_hashes_len: usize) -> BlobTransactionSidecar; +} + +impl FakeSidecar for BlobTransactionSidecar { + fn fake_sidecar(blob_versioned_hashes_len: usize) -> BlobTransactionSidecar { + let mut fake_sidecar = BlobTransactionSidecar::default(); + for _ in 0..blob_versioned_hashes_len { + fake_sidecar.blobs.push(Blob::from([0u8; BYTES_PER_BLOB])); + fake_sidecar + .commitments + .push(Bytes48::from([0u8; BYTES_PER_COMMITMENT])); + fake_sidecar + .proofs + .push(Bytes48::from([0u8; BYTES_PER_PROOF])); + } + fake_sidecar + } +} + /// First idea to handle blobs, might change. /// Don't like the fact that blobs_sidecar exists no matter if Recovered contains a non blob tx. /// Great effort was put in avoiding simple access to the internal tx so we don't accidentally leak information on logs (particularly the tx sign). @@ -721,25 +764,36 @@ impl std::fmt::Debug for TransactionSignedEcRecoveredWithBlobs { } } -#[derive(Error, Debug, derive_more::From)] -pub enum TxWithBlobsCreateError { - #[error("Failed to decode transaction, error: {0}")] - FailedToDecodeTransaction(Eip2718Error), - #[error("Invalid transaction signature")] - InvalidTransactionSignature, - #[error("UnexpectedError")] - UnexpectedError, - /// This error is generated when we fail (like FailedToDecodeTransaction) parsing in TxEncoding::WithBlobData mode (Network encoding) but the header looks - /// like the beginning of an ethereum mainnet Canonical encoding 4484 tx. - /// To avoid consuming resources the generation of this error might not be perfect but helps 99% of the time. - #[error("Failed to decode transaction, error: {0}. It probably is a 4484 canonical tx.")] - FailedToDecodeTransactionProbablyIs4484Canonical(alloy_rlp::Error), - #[error("Tried to create an EIP4844 transaction without a blob")] - Eip4844MissingBlobSidecar, - #[error("Tried to create a non-EIP4844 transaction while passing blobs")] - BlobsMissingEip4844, - #[error("BlobStoreError: {0}")] - BlobStore(BlobStoreError), +impl TryFrom> for TransactionSignedEcRecoveredWithBlobs { + type Error = TxWithBlobsCreateError; + + fn try_from(value: Recovered) -> Result { + let (tx, signer) = value.into_parts(); + + match tx { + PooledTransactionVariant::Legacy(_) + | PooledTransactionVariant::Eip2930(_) + | PooledTransactionVariant::Eip1559(_) + | PooledTransactionVariant::Eip7702(_) => { + let tx_signed = TransactionSigned::from(tx); + TransactionSignedEcRecoveredWithBlobs::new_no_blobs(tx_signed.with_signer(signer)) + } + PooledTransactionVariant::Eip4844(blob_tx) => { + let (blob_tx, signature, hash) = blob_tx.into_parts(); + let (blob_tx, sidecar) = blob_tx.into_parts(); + let tx_signed = TransactionSigned::new_unchecked( + Transaction::Eip4844(blob_tx), + signature, + hash, + ); + Ok(TransactionSignedEcRecoveredWithBlobs { + tx: tx_signed.with_signer(signer), + blobs_sidecar: Arc::new(sidecar), + metadata: Metadata::default(), + }) + } + } + } } impl TransactionSignedEcRecoveredWithBlobs { @@ -877,59 +931,93 @@ impl TransactionSignedEcRecoveredWithBlobs { self.tx.encode_2718(&mut buf); buf.into() } +} + +/// Trait alias to lookup the signer of a tx by its hash. +pub trait SignerLookup: Fn(B256) -> Option
{} +impl Option
> SignerLookup for T {} + +/// Raw transaction bytes along with: +/// - the encoding used (with or without blob data) +/// - an optional signer lookup to avoid signature recovery when we already know the signer +pub struct RawTransactionDecodable { + pub raw: Bytes, + pub encoding: TxEncoding, + signer_lookup: Option, +} + +impl RawTransactionDecodable Option
> { + pub fn new(raw: Bytes, encoding: TxEncoding) -> Self { + Self { + raw, + encoding, + signer_lookup: Option:: Option
>::None, + } + } +} + +impl RawTransactionDecodable { + /// Allows to set a custom signer lookup. + pub fn with_signer_lookup(self, lookup: U) -> RawTransactionDecodable { + RawTransactionDecodable { + raw: self.raw, + encoding: self.encoding, + signer_lookup: Some(lookup), + } + } + + /// Decodes the raw transaction bytes into a [`TransactionSignedEcRecoveredWithBlobs`]. + /// + /// If the encoding is `TxEncoding::WithBlobData`, the blob data is expected to be + /// present in the raw bytes. Otherwise, fake blob data is generated. + pub fn decode_enveloped( + &self, + ) -> Result { + match self.encoding { + TxEncoding::WithBlobData => self.decode_enveloped_with_real_blobs(), + TxEncoding::NoBlobData => self.decode_enveloped_with_fake_blobs(), + } + } /// Decodes the "raw" format of transaction (e.g. `eth_sendRawTransaction`) with the blob data (network format) - pub fn decode_enveloped_with_real_blobs( - raw_tx: Bytes, + fn decode_enveloped_with_real_blobs( + &self, ) -> Result { - let raw_tx = &mut raw_tx.as_ref(); + let raw_tx = &mut self.raw.as_ref(); + let pooled_tx = PooledTransactionVariant::decode_2718(raw_tx) .map_err(TxWithBlobsCreateError::FailedToDecodeTransaction)?; - let signer = pooled_tx - .recover_signer() - .map_err(|_| TxWithBlobsCreateError::InvalidTransactionSignature)?; - match pooled_tx { - PooledTransactionVariant::Legacy(_) - | PooledTransactionVariant::Eip2930(_) - | PooledTransactionVariant::Eip1559(_) - | PooledTransactionVariant::Eip7702(_) => { - let tx_signed = TransactionSigned::from(pooled_tx); - TransactionSignedEcRecoveredWithBlobs::new_no_blobs(tx_signed.with_signer(signer)) - } - PooledTransactionVariant::Eip4844(blob_tx) => { - let (blob_tx, signature, hash) = blob_tx.into_parts(); - let (blob_tx, sidecar) = blob_tx.into_parts(); - let tx_signed = TransactionSigned::new_unchecked( - Transaction::Eip4844(blob_tx), - signature, - hash, - ); - Ok(TransactionSignedEcRecoveredWithBlobs { - tx: tx_signed.with_signer(signer), - blobs_sidecar: Arc::new(sidecar), - metadata: Metadata::default(), - }) - } - } + + let signer = self + .signer_lookup + .as_ref() + .and_then(|sl| sl(*pooled_tx.tx_hash())) + .or_else(|| pooled_tx.recover_signer().ok()) + .ok_or(TxWithBlobsCreateError::InvalidTransactionSignature)?; + + Recovered::::new_unchecked(pooled_tx, signer).try_into() } + /// Decodes the "raw" canonical format of transaction (NOT the one used in `eth_sendRawTransaction`) generating fake blob data for backtesting - pub fn decode_enveloped_with_fake_blobs( - raw_tx: Bytes, + fn decode_enveloped_with_fake_blobs( + &self, ) -> Result { - let decoded = TransactionSigned::decode_2718(&mut raw_tx.as_ref()) + let decoded = TransactionSigned::decode_2718(&mut self.raw.as_ref()) .map_err(TxWithBlobsCreateError::FailedToDecodeTransaction)?; - let tx = SignerRecoverable::try_into_recovered(decoded) - .map_err(|_| TxWithBlobsCreateError::InvalidTransactionSignature)?; - let mut fake_sidecar = BlobTransactionSidecar::default(); - for _ in 0..tx.blob_versioned_hashes().map_or(0, |hashes| hashes.len()) { - fake_sidecar.blobs.push(Blob::from([0u8; BYTES_PER_BLOB])); - fake_sidecar - .commitments - .push(Bytes48::from([0u8; BYTES_PER_COMMITMENT])); - fake_sidecar - .proofs - .push(Bytes48::from([0u8; BYTES_PER_PROOF])); - } + + let hash = *decoded.hash(); + + let signer = self + .signer_lookup + .as_ref() + .and_then(|sl| sl(hash)) + .or_else(|| decoded.recover_signer().ok()) + .ok_or(TxWithBlobsCreateError::InvalidTransactionSignature)?; + + let tx = Recovered::new_unchecked(decoded, signer); + let hashes_len = tx.blob_versioned_hashes().map_or(0, |hashes| hashes.len()); + let fake_sidecar = BlobTransactionSidecar::fake_sidecar(hashes_len); + Ok(TransactionSignedEcRecoveredWithBlobs { tx, blobs_sidecar: Arc::new(BlobTransactionSidecarVariant::Eip4844(fake_sidecar)), diff --git a/crates/rbuilder-primitives/src/serialize.rs b/crates/rbuilder-primitives/src/serialize.rs index abaa2ae32..acdfc02c1 100644 --- a/crates/rbuilder-primitives/src/serialize.rs +++ b/crates/rbuilder-primitives/src/serialize.rs @@ -1,7 +1,7 @@ use super::{ Bundle, BundleRefund, BundleReplacementData, BundleReplacementKey, BundleVersion, MempoolTx, - Order, Refund, RefundConfig, ShareBundle, ShareBundleBody, ShareBundleInner, - ShareBundleReplacementData, ShareBundleReplacementKey, ShareBundleTx, + Order, RawTransactionDecodable, Refund, RefundConfig, ShareBundle, ShareBundleBody, + ShareBundleInner, ShareBundleReplacementData, ShareBundleReplacementKey, ShareBundleTx, TransactionSignedEcRecoveredWithBlobs, TxRevertBehavior, TxWithBlobsCreateError, LAST_BUNDLE_VERSION, }; @@ -18,6 +18,7 @@ use tracing::error; use uuid::Uuid; /// Encoding mode for raw transactions (https://eips.ethereum.org/EIPS/eip-4844) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TxEncoding { /// Canonical encoding, for 4844 is only tx_payload_body NoBlobData, @@ -31,22 +32,20 @@ impl TxEncoding { &self, raw_tx: Bytes, ) -> Result { + // This clone is supposed to be cheap + let res = RawTransactionDecodable::new(raw_tx.clone(), *self).decode_enveloped(); + match self { - TxEncoding::NoBlobData => { - TransactionSignedEcRecoveredWithBlobs::decode_enveloped_with_fake_blobs(raw_tx) - } + TxEncoding::NoBlobData => res, TxEncoding::WithBlobData => { - let raw_tx_clone = raw_tx.clone(); // This clone is supposed to be cheap - let res = - TransactionSignedEcRecoveredWithBlobs::decode_enveloped_with_real_blobs(raw_tx); if let Err(TxWithBlobsCreateError::FailedToDecodeTransaction( Eip2718Error::RlpError(err), )) = res { - if Self::looks_like_canonical_blob_tx(raw_tx_clone) { + if Self::looks_like_canonical_blob_tx(raw_tx) { return Err(TxWithBlobsCreateError::FailedToDecodeTransactionProbablyIs4484Canonical( - err, - )); + err, + )); } } res @@ -91,33 +90,17 @@ where deserialize_vec_from_null_or_string(deserializer) } -fn deserialize_vec_bytes_from_null_or_string<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - deserialize_vec_from_null_or_string(deserializer) -} - -/// Struct to de/serialize json Bundles from bundles APIs and from/db. -/// Does not assume a particular format on txs. +/// Struct to de/serialize JSON bundles data from bundles APIs and from/db, except transactions. +/// To be used long with `RawBundle`. #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, Derivative)] #[derivative(PartialEq, Eq)] #[serde(rename_all = "camelCase")] -pub struct RawBundle { +pub struct RawBundleMetadata { pub version: Option, /// blockNumber (Optional) `String`, a hex encoded block number for which this bundle is valid /// on. If nil or 0, blockNumber will default to the current pending block pub block_number: Option, - /// txs `Array[String]`, A list of signed transactions to execute in an atomic bundle, list can - /// be empty for bundle cancellations - #[serde( - default, - deserialize_with = "deserialize_vec_bytes_from_null_or_string" - )] - pub txs: Vec, /// revertingTxHashes (Optional) `Array[String]`, A list of tx hashes that are allowed to /// revert #[serde(default, deserialize_with = "deserialize_vec_b256_from_null_or_string")] @@ -172,6 +155,112 @@ pub struct RawBundle { pub delayed_refund: Option, } +impl RawBundleMetadata { + /// consistency checks on raw data. + /// uuid takes priority over replacement_nonce + fn decode_replacement_data( + &self, + ) -> Result, RawBundleConvertError> { + let uuid = self.uuid.or(self.replacement_uuid); + + match uuid { + Some(uuid) => { + let replacement_nonce = self + .replacement_nonce + .ok_or(RawBundleConvertError::IncorrectReplacementData)?; + + let signer = self + .signing_address + .ok_or(RawBundleConvertError::IncorrectReplacementData)?; + + Ok(Some(BundleReplacementData { + key: BundleReplacementKey::new(uuid, Some(signer)), + sequence_number: replacement_nonce, + })) + } + None => Ok(None), + } + } + + fn decode_version(&self) -> Result { + if let Some(version) = self.version.as_deref() { + match version { + BUNDLE_VERSION_V1 => Ok(BundleVersion::V1), + BUNDLE_VERSION_V2 => Ok(BundleVersion::V2), + _ => Err(RawBundleConvertError::UnsupportedVersion( + version.to_string(), + )), + } + } else { + Ok(LAST_BUNDLE_VERSION) + } + } + + /// Validates if all fields are valid for the version. + fn validate_fields(&self, version: BundleVersion) -> Result<(), RawBundleConvertError> { + match version { + BundleVersion::V1 => { + // Fields add on V2 + if !self.dropping_tx_hashes.is_empty() { + return Err(RawBundleConvertError::FieldNotSupportedByVersion( + "dropping_tx_hashes".to_owned(), + version, + )); + } + if self.refund_percent.is_some() { + return Err(RawBundleConvertError::FieldNotSupportedByVersion( + "refund_percent".to_owned(), + version, + )); + } + if self.refund_recipient.is_some() { + return Err(RawBundleConvertError::FieldNotSupportedByVersion( + "refund_recipient".to_owned(), + version, + )); + } + if self.refund_tx_hashes.is_some() { + return Err(RawBundleConvertError::FieldNotSupportedByVersion( + "refund_tx_hashes".to_owned(), + version, + )); + } + if self.delayed_refund.is_some() { + return Err(RawBundleConvertError::FieldNotSupportedByVersion( + "delayed_refund".to_owned(), + version, + )); + } + Ok(()) + } + BundleVersion::V2 => Ok(()), + } + } +} + +/// Struct to de/serialize json Bundles from bundles APIs and from/db. +/// Does not assume a particular format on txs. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize, Derivative)] +#[derivative(PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct RawBundle { + #[serde(flatten)] + pub metadata: RawBundleMetadata, + /// txs `Array[String]`, A list of signed transactions to execute in an atomic bundle, list can + /// be empty for bundle cancellations. + #[serde(default, deserialize_with = "deserialize_vec_from_null_or_string")] + pub txs: Vec, +} + +/// A [`RawBundle`] with transactions already decoded and recovered. +struct RawBundleRecovered { + pub metadata: RawBundleMetadata, + /// txs `Array[String]`, A list of signed transactions to execute in an atomic bundle, list can + /// be empty for bundle cancellations. + pub txs: Vec, +} + #[derive(Error, Debug)] pub enum RawBundleConvertError { #[error("Failed to decode transaction, idx: {0}, error: {1}")] @@ -217,16 +306,26 @@ impl RawBundle { } pub fn decode( + self, + encoding: TxEncoding, + ) -> Result { + self.decode_inner(encoding, Option:: Option
>::None) + } + + pub fn decode_with_signer_lookup( + self, + encoding: TxEncoding, + signer_lookup: impl Fn(B256) -> Option
, + ) -> Result { + self.decode_inner(encoding, Some(signer_lookup)) + } + + fn decode_inner( mut self, encoding: TxEncoding, + signer_lookup: Option Option
>, ) -> Result { - let replacement_data = Self::decode_replacement_data( - self.replacement_uuid, - self.uuid, - self.signing_address, - self.replacement_nonce, - )?; - // Check for cancellation + let replacement_data = self.metadata.decode_replacement_data()?; // Check for cancellation if self.txs.is_empty() { match replacement_data { Some(replacement_data) => { @@ -235,45 +334,51 @@ impl RawBundle { None => return Err(RawBundleConvertError::EmptyBundle), } } - let version = Self::decode_version(self.version.clone())?; - self.validate_fields(version.clone())?; - let txs = self - .txs + let version = self.metadata.decode_version()?; + self.metadata.validate_fields(version)?; + + self.metadata.reverting_tx_hashes.sort(); + self.metadata.dropping_tx_hashes.sort(); + + let recovered_txs = std::mem::take(&mut self.txs) .into_iter() .enumerate() .map(|(idx, tx)| { - encoding - .decode(tx) + let decodable = RawTransactionDecodable { + raw: tx, + encoding, + signer_lookup: signer_lookup.as_ref(), + }; + decodable + .decode_enveloped() .map_err(|e| RawBundleConvertError::FailedToDecodeTransaction(idx, e)) }) .collect::, _>>()?; - let refund = Self::parse_refund( - self.refund_percent, - self.refund_recipient, - self.refund_tx_hashes, - self.delayed_refund, - &txs, - )?; - self.reverting_tx_hashes.sort(); - self.dropping_tx_hashes.sort(); + let mut recovered_bundle = self.into_recovered(recovered_txs); - let block = self.block_number.unwrap_or_default().to(); + let refund = recovered_bundle.parse_refund()?; + let block = recovered_bundle + .metadata + .block_number + .unwrap_or_default() + .to(); + let RawBundleRecovered { metadata, txs } = recovered_bundle; let mut bundle = Bundle { block: if block != 0 { Some(block) } else { None }, txs, - reverting_tx_hashes: self.reverting_tx_hashes, + reverting_tx_hashes: metadata.reverting_tx_hashes, hash: Default::default(), uuid: Default::default(), replacement_data, // we assume that 0 timestamp is the same as timestamp not set - min_timestamp: self.min_timestamp, - max_timestamp: self.max_timestamp.filter(|t| *t != 0), - signer: self.signing_address, - refund_identity: self.refund_identity, + min_timestamp: metadata.min_timestamp, + max_timestamp: metadata.max_timestamp.filter(|t| *t != 0), + signer: metadata.signing_address, + refund_identity: metadata.refund_identity, metadata: Default::default(), - dropping_tx_hashes: self.dropping_tx_hashes, + dropping_tx_hashes: metadata.dropping_tx_hashes, refund, version, }; @@ -281,71 +386,76 @@ impl RawBundle { Ok(RawBundleDecodeResult::NewBundle(bundle)) } - /// Validates if all fields are valid for the version. - fn validate_fields(&self, version: BundleVersion) -> Result<(), RawBundleConvertError> { + /// See [TransactionSignedEcRecoveredWithBlobs::envelope_encoded_no_blobs] + pub fn encode_no_blobs(value: Bundle) -> Self { + let replacement_uuid = value.replacement_data.as_ref().map(|r| r.key.key().id); + let replacement_nonce = value.replacement_data.as_ref().map(|r| r.sequence_number); + let signing_address = value.signer.or_else(|| { + value + .replacement_data + .as_ref() + .and_then(|r| r.key.key().signer) + }); + Self { + txs: value + .txs + .into_iter() + .map(|tx| tx.envelope_encoded_no_blobs()) + .collect(), + metadata: RawBundleMetadata { + block_number: value.block.map(U64::from), + reverting_tx_hashes: value.reverting_tx_hashes, + dropping_tx_hashes: value.dropping_tx_hashes, + replacement_uuid, + uuid: replacement_uuid, + signing_address, + refund_identity: value.refund_identity, + min_timestamp: value.min_timestamp, + max_timestamp: value.max_timestamp, + replacement_nonce, + refund_percent: value.refund.as_ref().map(|br| br.percent), + refund_recipient: value.refund.as_ref().map(|br| br.recipient), + refund_tx_hashes: value.refund.as_ref().map(|br| vec![br.tx_hash]), + delayed_refund: value.refund.as_ref().map(|br| br.delayed), + version: Some(Self::encode_version(value.version)), + }, + } + } + + pub fn encode_version(version: BundleVersion) -> String { match version { - BundleVersion::V1 => { - // Fields add on V2 - if !self.dropping_tx_hashes.is_empty() { - return Err(RawBundleConvertError::FieldNotSupportedByVersion( - "dropping_tx_hashes".to_owned(), - version, - )); - } - if self.refund_percent.is_some() { - return Err(RawBundleConvertError::FieldNotSupportedByVersion( - "refund_percent".to_owned(), - version, - )); - } - if self.refund_recipient.is_some() { - return Err(RawBundleConvertError::FieldNotSupportedByVersion( - "refund_recipient".to_owned(), - version, - )); - } - if self.refund_tx_hashes.is_some() { - return Err(RawBundleConvertError::FieldNotSupportedByVersion( - "refund_tx_hashes".to_owned(), - version, - )); - } - if self.delayed_refund.is_some() { - return Err(RawBundleConvertError::FieldNotSupportedByVersion( - "delayed_refund".to_owned(), - version, - )); - } - Ok(()) - } - BundleVersion::V2 => Ok(()), + BundleVersion::V1 => BUNDLE_VERSION_V1.to_string(), + BundleVersion::V2 => BUNDLE_VERSION_V2.to_string(), + } + } + + fn into_recovered(self, txs: Vec) -> RawBundleRecovered { + RawBundleRecovered { + txs, + metadata: self.metadata, } } +} - fn parse_refund( - mut refund_percent: Option, - refund_recipient: Option
, - refund_tx_hashes: Option>, - delayed_refund: Option, - txs: &[TransactionSignedEcRecoveredWithBlobs], - ) -> Result, RawBundleConvertError> { +impl RawBundleRecovered { + fn parse_refund(&mut self) -> Result, RawBundleConvertError> { // Validate refund percent setting. - if let Some(percent) = refund_percent { + if let Some(percent) = self.metadata.refund_percent { if percent >= 100 { return Err(RawBundleConvertError::InvalidRefundPercent(percent)); } if percent == 0 { - refund_percent = None + self.metadata.refund_percent = None } } let mut refund = None; - if let Some(percent) = refund_percent { + if let Some(percent) = self.metadata.refund_percent { // Refund can be configured only if bundle is not empty. // If bundle contains only one transaction, first == last. // If refund_tx_hashes is empty we use the last tx. - if let Some((first_tx, last_tx)) = txs.first().zip(txs.last()) { - let tx_hash = if let Some(refund_tx_hashes) = refund_tx_hashes { + if let Some((first_tx, last_tx)) = self.txs.first().zip(self.txs.last()) { + let tx_hash = if let Some(ref refund_tx_hashes) = self.metadata.refund_tx_hashes { if refund_tx_hashes.len() > 1 { return Err(RawBundleConvertError::MoreThanOneRefundTxHash); } @@ -357,94 +467,17 @@ impl RawBundle { refund = Some(BundleRefund { percent, - recipient: refund_recipient.unwrap_or_else(|| first_tx.signer()), + recipient: self + .metadata + .refund_recipient + .unwrap_or_else(|| first_tx.signer()), tx_hash, - delayed: delayed_refund.unwrap_or_default(), + delayed: self.metadata.delayed_refund.unwrap_or_default(), }); } } Ok(refund) } - - /// consistency checks on raw data. - /// uuid takes priority over replacement_nonce - fn decode_replacement_data( - replacement_uuid: Option, - mut uuid: Option, - signing_address: Option
, - replacement_nonce: Option, - ) -> Result, RawBundleConvertError> { - uuid = uuid.or(replacement_uuid); - - match uuid { - Some(uuid) => { - let replacement_nonce = - replacement_nonce.ok_or(RawBundleConvertError::IncorrectReplacementData)?; - - let signer = - signing_address.ok_or(RawBundleConvertError::IncorrectReplacementData)?; - - Ok(Some(BundleReplacementData { - key: BundleReplacementKey::new(uuid, Some(signer)), - sequence_number: replacement_nonce, - })) - } - None => Ok(None), - } - } - - /// See [TransactionSignedEcRecoveredWithBlobs::envelope_encoded_no_blobs] - pub fn encode_no_blobs(value: Bundle) -> Self { - let replacement_uuid = value.replacement_data.as_ref().map(|r| r.key.key().id); - let replacement_nonce = value.replacement_data.as_ref().map(|r| r.sequence_number); - let signing_address = value.signer.or_else(|| { - value - .replacement_data - .as_ref() - .and_then(|r| r.key.key().signer) - }); - Self { - block_number: value.block.map(U64::from), - txs: value - .txs - .into_iter() - .map(|tx| tx.envelope_encoded_no_blobs()) - .collect(), - reverting_tx_hashes: value.reverting_tx_hashes, - dropping_tx_hashes: value.dropping_tx_hashes, - replacement_uuid, - uuid: replacement_uuid, - signing_address, - refund_identity: value.refund_identity, - min_timestamp: value.min_timestamp, - max_timestamp: value.max_timestamp, - replacement_nonce, - refund_percent: value.refund.as_ref().map(|br| br.percent), - refund_recipient: value.refund.as_ref().map(|br| br.recipient), - refund_tx_hashes: value.refund.as_ref().map(|br| vec![br.tx_hash]), - delayed_refund: value.refund.as_ref().map(|br| br.delayed), - version: Some(Self::encode_version(value.version)), - } - } - - fn decode_version(version: Option) -> Result { - if let Some(version) = version { - match version.as_str() { - BUNDLE_VERSION_V1 => Ok(BundleVersion::V1), - BUNDLE_VERSION_V2 => Ok(BundleVersion::V2), - _ => Err(RawBundleConvertError::UnsupportedVersion(version)), - } - } else { - Ok(LAST_BUNDLE_VERSION) - } - } - - pub fn encode_version(version: BundleVersion) -> String { - match version { - BundleVersion::V1 => BUNDLE_VERSION_V1.to_string(), - BundleVersion::V2 => BUNDLE_VERSION_V2.to_string(), - } - } } /// Struct to de/serialize json Bundles from bundles APIs and from/db. diff --git a/crates/rbuilder/src/backtest/backtest_build_range.rs b/crates/rbuilder/src/backtest/backtest_build_range.rs index 905f4cd8e..20fb65606 100644 --- a/crates/rbuilder/src/backtest/backtest_build_range.rs +++ b/crates/rbuilder/src/backtest/backtest_build_range.rs @@ -230,7 +230,10 @@ where print_backtest_value_diff(&stored_result, &o); } else if cli.store_backtest { backtest_results_storage - .store_backtest_results(time::OffsetDateTime::now_utc(), &[o.clone()]) + .store_backtest_results( + time::OffsetDateTime::now_utc(), + std::slice::from_ref(&o), + ) .await?; print_backtest_value(o); } else { diff --git a/crates/rbuilder/src/backtest/results_store.rs b/crates/rbuilder/src/backtest/results_store.rs index 42fe052c9..3516b9470 100644 --- a/crates/rbuilder/src/backtest/results_store.rs +++ b/crates/rbuilder/src/backtest/results_store.rs @@ -195,7 +195,7 @@ mod tests { .await .expect("create db"); storage - .store_backtest_results(time, &[backtest_value.clone()]) + .store_backtest_results(time, std::slice::from_ref(&backtest_value)) .await .unwrap(); let res = storage diff --git a/crates/rbuilder/src/backtest/store.rs b/crates/rbuilder/src/backtest/store.rs index 50c6b6ebe..b7a4ce999 100644 --- a/crates/rbuilder/src/backtest/store.rs +++ b/crates/rbuilder/src/backtest/store.rs @@ -704,7 +704,7 @@ mod test { use alloy_primitives::{address, hex, Address, Signature, B256, U256, U64}; use alloy_rpc_types::{Block, BlockTransactions, Header, Transaction}; use rbuilder_primitives::{ - serialize::{RawBundle, RawTx}, + serialize::{RawBundle, RawBundleMetadata, RawTx}, BundleReplacementKey, ShareBundleReplacementKey, LAST_BUNDLE_VERSION, }; use reth_primitives::Recovered; @@ -733,24 +733,26 @@ mod test { RawReplaceableOrderPoolCommandWithTimestamp { timestamp_ms: 11, command: RawReplaceableOrderPoolCommand::Order(RawOrder::Bundle(RawBundle { - block_number: Some(U64::from(12)), txs: vec![tx.clone().into()], - reverting_tx_hashes: vec![], - replacement_uuid: Some(uuid::Uuid::from_u128(11)), - signing_address: Some(alloy_primitives::address!( - "0101010101010101010101010101010101010101" - )), - min_timestamp: None, - max_timestamp: Some(100), - replacement_nonce: Some(0), - dropping_tx_hashes: vec![], - uuid: None, - refund_percent: None, - refund_recipient: None, - refund_tx_hashes: None, - delayed_refund: None, - refund_identity: None, - version: Some(RawBundle::encode_version(LAST_BUNDLE_VERSION)), + metadata: RawBundleMetadata { + block_number: Some(U64::from(12)), + reverting_tx_hashes: vec![], + replacement_uuid: Some(uuid::Uuid::from_u128(11)), + signing_address: Some(alloy_primitives::address!( + "0101010101010101010101010101010101010101" + )), + min_timestamp: None, + max_timestamp: Some(100), + replacement_nonce: Some(0), + dropping_tx_hashes: vec![], + uuid: None, + refund_percent: None, + refund_recipient: None, + refund_tx_hashes: None, + delayed_refund: None, + refund_identity: None, + version: Some(RawBundle::encode_version(LAST_BUNDLE_VERSION)), + }, })), } .decode(TxEncoding::WithBlobData) diff --git a/crates/rbuilder/src/live_builder/order_input/txpool_fetcher.rs b/crates/rbuilder/src/live_builder/order_input/txpool_fetcher.rs index aa0b61594..e62032df4 100644 --- a/crates/rbuilder/src/live_builder/order_input/txpool_fetcher.rs +++ b/crates/rbuilder/src/live_builder/order_input/txpool_fetcher.rs @@ -3,7 +3,10 @@ use crate::telemetry::{add_txfetcher_time_to_query, mark_command_received}; use alloy_primitives::FixedBytes; use alloy_provider::{IpcConnect, Provider, ProviderBuilder}; use futures::StreamExt; -use rbuilder_primitives::{MempoolTx, Order, TransactionSignedEcRecoveredWithBlobs}; +use rbuilder_primitives::{ + serialize::TxEncoding, MempoolTx, Order, RawTransactionDecodable, + TransactionSignedEcRecoveredWithBlobs, +}; use std::{pin::pin, time::Instant}; use time::OffsetDateTime; use tokio::{ @@ -105,9 +108,8 @@ async fn get_tx_with_blobs( let Some(response) = provider.get_raw_transaction_by_hash(tx_hash).await? else { return Ok(None); }; - Ok(Some( - TransactionSignedEcRecoveredWithBlobs::decode_enveloped_with_real_blobs(response)?, - )) + let raw_decodable = RawTransactionDecodable::new(response, TxEncoding::WithBlobData); + Ok(Some(raw_decodable.decode_enveloped()?)) } #[cfg(test)] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7855e6d55..b1fa8153a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,4 @@ [toolchain] -channel = "1.88.0" +channel = "stable" +version = "1.88.0" components = ["rustfmt", "clippy"]