From a114a0b2ae74d1082f157d433acd843ce708ea48 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 23 Oct 2025 14:49:41 -0400 Subject: [PATCH 01/14] 25m gas per bundle --- crates/ingress-rpc/src/validation.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 9d5f4ba..6247877 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -13,8 +13,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tips_core::Bundle; use tracing::warn; -// TODO: make this configurable -const MAX_BUNDLE_GAS: u64 = 30_000_000; +const MAX_BUNDLE_GAS: u64 = 25_000_000; /// Account info for a given address pub struct AccountInfo { @@ -518,8 +517,8 @@ mod tests { let signer = PrivateKeySigner::random(); let mut encoded_txs = vec![]; - // Create transactions that collectively exceed MAX_BUNDLE_GAS (30M) - // Each transaction uses 4M gas, so 8 transactions = 32M gas > 30M limit + // Create transactions that collectively exceed MAX_BUNDLE_GAS (25M) + // Each transaction uses 4M gas, so 8 transactions = 32M gas > 25M limit let gas = 4_000_000; let mut total_gas = 0u64; for _ in 0..8 { From a591c878f8dd6c265516b8abad8cde9eb66d2114 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 23 Oct 2025 14:51:00 -0400 Subject: [PATCH 02/14] max 3 txs for now --- crates/ingress-rpc/src/validation.rs | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 6247877..144f4b9 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -190,6 +190,14 @@ pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> { ); } + // Can only provide 3 transactions at once + if bundle.txs.len() > 3 { + return Err( + EthApiError::InvalidParams("Bundle can only contain 3 transactions".into()) + .into_rpc_err(), + ); + } + Ok(()) } @@ -561,4 +569,52 @@ mod tests { assert!(error_message.contains("Bundle gas limit exceeds maximum allowed")); } } + + #[tokio::test] + async fn test_err_bundle_too_many_transactions() { + let signer = PrivateKeySigner::random(); + let mut encoded_txs = vec![]; + + let gas = 4_000_000; + let mut total_gas = 0u64; + for _ in 0..4 { + let mut tx = TxEip1559 { + chain_id: 1, + nonce: 0, + gas_limit: gas, + max_fee_per_gas: 200000u128, + max_priority_fee_per_gas: 100000u128, + to: Address::random().into(), + value: U256::from(1000000u128), + access_list: Default::default(), + input: bytes!("").clone(), + }; + total_gas = total_gas.saturating_add(gas); + + let signature = signer.sign_transaction_sync(&mut tx).unwrap(); + let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature)); + + // Encode the transaction + let mut encoded = vec![]; + envelope.encode_2718(&mut encoded); + encoded_txs.push(Bytes::from(encoded)); + } + + let bundle = EthSendBundle { + txs: encoded_txs, + block_number: 0, + min_timestamp: None, + max_timestamp: None, + reverting_tx_hashes: vec![], + ..Default::default() + }; + + // Test should fail due to exceeding gas limit + let result = validate_bundle(&bundle, total_gas); + assert!(result.is_err()); + if let Err(e) = result { + let error_message = format!("{e:?}"); + assert!(error_message.contains("Bundle can only contain 3 transactions")); + } + } } From 9b6388a317dc2ecfe39818c1d75d3207b4352aeb Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 23 Oct 2025 14:53:21 -0400 Subject: [PATCH 03/14] partial tx not supported --- crates/ingress-rpc/src/validation.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 144f4b9..65fbc31 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -198,6 +198,14 @@ pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> { ); } + // Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty + if !bundle.dropping_tx_hashes.is_empty() { + return Err(EthApiError::InvalidParams( + "Partial transaction dropping is not supported".into(), + ) + .into_rpc_err()); + } + Ok(()) } @@ -617,4 +625,20 @@ mod tests { assert!(error_message.contains("Bundle can only contain 3 transactions")); } } + + #[tokio::test] + async fn test_err_bundle_partial_transaction_dropping_not_supported() { + let bundle = EthSendBundle { + txs: vec![], + dropping_tx_hashes: vec![B256::random()], + ..Default::default() + }; + assert_eq!( + validate_bundle(&bundle, 0), + Err( + EthApiError::InvalidParams("Partial transaction dropping is not supported".into()) + .into_rpc_err() + ) + ); + } } From 9bafe8fbfb7ff4840926f1c6d1913e7855305f9f Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 23 Oct 2025 14:58:33 -0400 Subject: [PATCH 04/14] extra fields must be empty --- Cargo.lock | 1 + Cargo.toml | 2 ++ crates/ingress-rpc/Cargo.toml | 1 + crates/ingress-rpc/src/validation.rs | 21 +++++++++++++++++++++ 4 files changed, 25 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1f42420..056739e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6932,6 +6932,7 @@ dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-provider", + "alloy-serde", "alloy-signer-local", "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 2f4008a..cb8ff2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ alloy-primitives = { version = "1.3.1", default-features = false, features = [ alloy-rpc-types = { version = "1.0.35", default-features = false } alloy-consensus = { version = "1.0.35" } alloy-provider = { version = "1.0.35" } +alloy-rpc-types-mev = "1.0.35" +alloy-serde = "1.0.41" # op-alloy op-alloy-network = { version = "0.21.0", default-features = false } diff --git a/crates/ingress-rpc/Cargo.toml b/crates/ingress-rpc/Cargo.toml index e1c0e2f..9eaf45a 100644 --- a/crates/ingress-rpc/Cargo.toml +++ b/crates/ingress-rpc/Cargo.toml @@ -35,3 +35,4 @@ op-revm.workspace = true revm-context-interface.workspace = true alloy-signer-local.workspace = true reth-optimism-evm.workspace = true +alloy-serde.workspace = true diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 65fbc31..b98ea6c 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -206,6 +206,11 @@ pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> { .into_rpc_err()); } + // extra_fields must be empty + if !bundle.extra_fields.is_empty() { + return Err(EthApiError::InvalidParams("extra_fields must be empty".into()).into_rpc_err()); + } + Ok(()) } @@ -217,11 +222,13 @@ mod tests { use alloy_consensus::{TxEip1559, TxEip4844, TxEip7702}; use alloy_primitives::Bytes; use alloy_primitives::{bytes, keccak256}; + use alloy_serde::OtherFields; use alloy_signer_local::PrivateKeySigner; use op_alloy_consensus::OpTxEnvelope; use op_alloy_network::TxSignerSync; use op_alloy_network::eip2718::Encodable2718; use revm_context_interface::transaction::{AccessList, AccessListItem}; + use std::collections::BTreeMap; use std::time::{SystemTime, UNIX_EPOCH}; fn create_account(nonce: u64, balance: U256) -> AccountInfo { @@ -641,4 +648,18 @@ mod tests { ) ); } + + #[tokio::test] + async fn test_err_bundle_extra_fields_not_empty() { + let mut extra_fields = OtherFields::new(BTreeMap::new()); + let _ = extra_fields.insert_value("test".to_string(), "test".to_string()); + let bundle = EthSendBundle { + extra_fields, + ..Default::default() + }; + assert_eq!( + validate_bundle(&bundle, 0), + Err(EthApiError::InvalidParams("extra_fields must be empty".into()).into_rpc_err()) + ); + } } From 4632e71092b152cd06b59a9cb30719fda3b12001 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 23 Oct 2025 14:59:59 -0400 Subject: [PATCH 05/14] no refunds --- crates/ingress-rpc/src/validation.rs | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index b98ea6c..28fbeb9 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -211,6 +211,16 @@ pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> { return Err(EthApiError::InvalidParams("extra_fields must be empty".into()).into_rpc_err()); } + // refunds are not initially supported + if bundle.refund_percent.is_some() + || bundle.refund_recipient.is_some() + || !bundle.refund_tx_hashes.is_empty() + { + return Err( + EthApiError::InvalidParams("refunds are not initially supported".into()).into_rpc_err(), + ); + } + Ok(()) } @@ -662,4 +672,49 @@ mod tests { Err(EthApiError::InvalidParams("extra_fields must be empty".into()).into_rpc_err()) ); } + + #[tokio::test] + async fn test_err_bundle_refund_percent_not_empty() { + let bundle = EthSendBundle { + refund_percent: Some(100), + ..Default::default() + }; + assert_eq!( + validate_bundle(&bundle, 0), + Err( + EthApiError::InvalidParams("refunds are not initially supported".into()) + .into_rpc_err() + ) + ); + } + + #[tokio::test] + async fn test_err_bundle_refund_recipient_not_empty() { + let bundle = EthSendBundle { + refund_recipient: Some(Address::random()), + ..Default::default() + }; + assert_eq!( + validate_bundle(&bundle, 0), + Err( + EthApiError::InvalidParams("refunds are not initially supported".into()) + .into_rpc_err() + ) + ); + } + + #[tokio::test] + async fn test_err_bundle_refund_tx_hashes_not_empty() { + let bundle = EthSendBundle { + refund_tx_hashes: vec![B256::random()], + ..Default::default() + }; + assert_eq!( + validate_bundle(&bundle, 0), + Err( + EthApiError::InvalidParams("refunds are not initially supported".into()) + .into_rpc_err() + ) + ); + } } From 002e5a21379faaf50b8d970235b7ea18eeb1b1d8 Mon Sep 17 00:00:00 2001 From: William Law Date: Thu, 23 Oct 2025 15:11:58 -0400 Subject: [PATCH 06/14] add reverting_tx_check --- crates/ingress-rpc/src/service.rs | 4 +- crates/ingress-rpc/src/validation.rs | 94 +++++++++++++++++++++++++--- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/crates/ingress-rpc/src/service.rs b/crates/ingress-rpc/src/service.rs index 44c46d0..7d9fc7b 100644 --- a/crates/ingress-rpc/src/service.rs +++ b/crates/ingress-rpc/src/service.rs @@ -177,11 +177,13 @@ where } let mut total_gas = 0u64; + let mut tx_hashes = Vec::new(); for tx_data in &bundle.txs { let transaction = self.validate_tx(tx_data).await?; total_gas = total_gas.saturating_add(transaction.gas_limit()); + tx_hashes.push(transaction.tx_hash()); } - validate_bundle(&bundle, total_gas)?; + validate_bundle(&bundle, total_gas, tx_hashes)?; let bundle_with_metadata = BundleWithMetadata::load(bundle) .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 28fbeb9..d3f4919 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -165,7 +165,16 @@ pub async fn validate_tx( /// Helper function to validate propeties of a bundle. A bundle is valid if it satisfies the following criteria: /// - The bundle's max_timestamp is not more than 1 hour in the future /// - The bundle's gas limit is not greater than the maximum allowed gas limit -pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> { +/// - The bundle can only contain 3 transactions at once +/// - Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty +/// - extra_fields must be empty +/// - refunds are not initially supported (refund_percent, refund_recipient, refund_tx_hashes must be empty) +/// - revert protection is not supported, all transaction hashes must be in `reverting_tx_hashes` +pub fn validate_bundle( + bundle: &Bundle, + bundle_gas: u64, + tx_hashes: Vec, +) -> RpcResult<()> { // Don't allow bundles to be submitted over 1 hour into the future // TODO: make the window configurable let valid_timestamp_window = SystemTime::now() @@ -221,6 +230,16 @@ pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> { ); } + // revert protection: all transaction hashes must be in `reverting_tx_hashes` + for tx_hash in &tx_hashes { + if !bundle.reverting_tx_hashes.contains(tx_hash) { + return Err(EthApiError::InvalidParams( + "Transaction hash not found in reverting_tx_hashes".into(), + ) + .into_rpc_err()); + } + } + Ok(()) } @@ -537,7 +556,7 @@ mod tests { ..Default::default() }; assert_eq!( - validate_bundle(&bundle, 0), + validate_bundle(&bundle, 0, vec![]), Err(EthApiError::InvalidParams( "Bundle cannot be more than 1 hour in the future".into() ) @@ -549,6 +568,7 @@ mod tests { async fn test_err_bundle_max_gas_limit_too_high() { let signer = PrivateKeySigner::random(); let mut encoded_txs = vec![]; + let mut tx_hashes = vec![]; // Create transactions that collectively exceed MAX_BUNDLE_GAS (25M) // Each transaction uses 4M gas, so 8 transactions = 32M gas > 25M limit @@ -570,6 +590,8 @@ mod tests { let signature = signer.sign_transaction_sync(&mut tx).unwrap(); let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature)); + let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash(); + tx_hashes.push(tx_hash); // Encode the transaction let mut encoded = vec![]; @@ -587,7 +609,7 @@ mod tests { }; // Test should fail due to exceeding gas limit - let result = validate_bundle(&bundle, total_gas); + let result = validate_bundle(&bundle, total_gas, tx_hashes); assert!(result.is_err()); if let Err(e) = result { let error_message = format!("{e:?}"); @@ -599,6 +621,7 @@ mod tests { async fn test_err_bundle_too_many_transactions() { let signer = PrivateKeySigner::random(); let mut encoded_txs = vec![]; + let mut tx_hashes = vec![]; let gas = 4_000_000; let mut total_gas = 0u64; @@ -618,6 +641,8 @@ mod tests { let signature = signer.sign_transaction_sync(&mut tx).unwrap(); let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature)); + let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash(); + tx_hashes.push(tx_hash); // Encode the transaction let mut encoded = vec![]; @@ -635,7 +660,7 @@ mod tests { }; // Test should fail due to exceeding gas limit - let result = validate_bundle(&bundle, total_gas); + let result = validate_bundle(&bundle, total_gas, tx_hashes); assert!(result.is_err()); if let Err(e) = result { let error_message = format!("{e:?}"); @@ -651,7 +676,7 @@ mod tests { ..Default::default() }; assert_eq!( - validate_bundle(&bundle, 0), + validate_bundle(&bundle, 0, vec![]), Err( EthApiError::InvalidParams("Partial transaction dropping is not supported".into()) .into_rpc_err() @@ -668,7 +693,7 @@ mod tests { ..Default::default() }; assert_eq!( - validate_bundle(&bundle, 0), + validate_bundle(&bundle, 0, vec![]), Err(EthApiError::InvalidParams("extra_fields must be empty".into()).into_rpc_err()) ); } @@ -680,7 +705,7 @@ mod tests { ..Default::default() }; assert_eq!( - validate_bundle(&bundle, 0), + validate_bundle(&bundle, 0, vec![]), Err( EthApiError::InvalidParams("refunds are not initially supported".into()) .into_rpc_err() @@ -695,7 +720,7 @@ mod tests { ..Default::default() }; assert_eq!( - validate_bundle(&bundle, 0), + validate_bundle(&bundle, 0, vec![]), Err( EthApiError::InvalidParams("refunds are not initially supported".into()) .into_rpc_err() @@ -710,11 +735,62 @@ mod tests { ..Default::default() }; assert_eq!( - validate_bundle(&bundle, 0), + validate_bundle(&bundle, 0, vec![]), Err( EthApiError::InvalidParams("refunds are not initially supported".into()) .into_rpc_err() ) ); } + + #[tokio::test] + async fn test_err_bundle_not_all_tx_hashes_in_reverting_tx_hashes() { + let signer = PrivateKeySigner::random(); + let mut encoded_txs = vec![]; + let mut tx_hashes = vec![]; + + let gas = 4_000_000; + let mut total_gas = 0u64; + for _ in 0..4 { + let mut tx = TxEip1559 { + chain_id: 1, + nonce: 0, + gas_limit: gas, + max_fee_per_gas: 200000u128, + max_priority_fee_per_gas: 100000u128, + to: Address::random().into(), + value: U256::from(1000000u128), + access_list: Default::default(), + input: bytes!("").clone(), + }; + total_gas = total_gas.saturating_add(gas); + + let signature = signer.sign_transaction_sync(&mut tx).unwrap(); + let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature)); + let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash(); + tx_hashes.push(tx_hash); + + // Encode the transaction + let mut encoded = vec![]; + envelope.encode_2718(&mut encoded); + encoded_txs.push(Bytes::from(encoded)); + } + + let bundle = EthSendBundle { + txs: encoded_txs, + block_number: 0, + min_timestamp: None, + max_timestamp: None, + reverting_tx_hashes: tx_hashes[..2].to_vec(), + ..Default::default() + }; + + // Test should fail due to exceeding gas limit + let result = validate_bundle(&bundle, total_gas, tx_hashes); + assert!(result.is_err()); + if let Err(e) = result { + let error_message = format!("{e:?}"); + assert!(error_message.contains("Bundle can only contain 3 transactions")); + } + } } From 87fcf0f8ec88a62121a984769ff67f748a4acd0d Mon Sep 17 00:00:00 2001 From: William Law Date: Fri, 24 Oct 2025 09:35:51 -0400 Subject: [PATCH 07/14] comments --- crates/ingress-rpc/src/validation.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index d3f4919..6a36579 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -234,7 +234,8 @@ pub fn validate_bundle( for tx_hash in &tx_hashes { if !bundle.reverting_tx_hashes.contains(tx_hash) { return Err(EthApiError::InvalidParams( - "Transaction hash not found in reverting_tx_hashes".into(), + "Revert protection is not supported. reverting_tx_hashes must include all hashes" + .into(), ) .into_rpc_err()); } From 2cc6a914f6a94a21824f84a84f8b5d02e3eae421 Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 27 Oct 2025 13:40:56 -0400 Subject: [PATCH 08/14] use bundlewmetadata --- crates/ingress-rpc/src/service.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/ingress-rpc/src/service.rs b/crates/ingress-rpc/src/service.rs index 7d9fc7b..5e0f945 100644 --- a/crates/ingress-rpc/src/service.rs +++ b/crates/ingress-rpc/src/service.rs @@ -176,18 +176,17 @@ where ); } + let bundle_with_metadata = BundleWithMetadata::load(bundle.clone()) + .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; + let mut total_gas = 0u64; - let mut tx_hashes = Vec::new(); + let tx_hashes = bundle_with_metadata.txn_hashes(); for tx_data in &bundle.txs { let transaction = self.validate_tx(tx_data).await?; total_gas = total_gas.saturating_add(transaction.gas_limit()); - tx_hashes.push(transaction.tx_hash()); } validate_bundle(&bundle, total_gas, tx_hashes)?; - let bundle_with_metadata = BundleWithMetadata::load(bundle) - .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; - Ok(bundle_with_metadata) } } From 749af6f03957df9d6992401259bbfa4b53ede8e7 Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 27 Oct 2025 13:42:21 -0400 Subject: [PATCH 09/14] no more empty fields --- crates/ingress-rpc/src/validation.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 6a36579..43634de 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -215,11 +215,6 @@ pub fn validate_bundle( .into_rpc_err()); } - // extra_fields must be empty - if !bundle.extra_fields.is_empty() { - return Err(EthApiError::InvalidParams("extra_fields must be empty".into()).into_rpc_err()); - } - // refunds are not initially supported if bundle.refund_percent.is_some() || bundle.refund_recipient.is_some() @@ -685,20 +680,6 @@ mod tests { ); } - #[tokio::test] - async fn test_err_bundle_extra_fields_not_empty() { - let mut extra_fields = OtherFields::new(BTreeMap::new()); - let _ = extra_fields.insert_value("test".to_string(), "test".to_string()); - let bundle = EthSendBundle { - extra_fields, - ..Default::default() - }; - assert_eq!( - validate_bundle(&bundle, 0, vec![]), - Err(EthApiError::InvalidParams("extra_fields must be empty".into()).into_rpc_err()) - ); - } - #[tokio::test] async fn test_err_bundle_refund_percent_not_empty() { let bundle = EthSendBundle { From 77d4aca7c4e983c52704150e59d7a685e734d085 Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 27 Oct 2025 13:42:44 -0400 Subject: [PATCH 10/14] no refund --- crates/ingress-rpc/src/validation.rs | 57 ---------------------------- 1 file changed, 57 deletions(-) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 43634de..8d7ce69 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -167,8 +167,6 @@ pub async fn validate_tx( /// - The bundle's gas limit is not greater than the maximum allowed gas limit /// - The bundle can only contain 3 transactions at once /// - Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty -/// - extra_fields must be empty -/// - refunds are not initially supported (refund_percent, refund_recipient, refund_tx_hashes must be empty) /// - revert protection is not supported, all transaction hashes must be in `reverting_tx_hashes` pub fn validate_bundle( bundle: &Bundle, @@ -215,16 +213,6 @@ pub fn validate_bundle( .into_rpc_err()); } - // refunds are not initially supported - if bundle.refund_percent.is_some() - || bundle.refund_recipient.is_some() - || !bundle.refund_tx_hashes.is_empty() - { - return Err( - EthApiError::InvalidParams("refunds are not initially supported".into()).into_rpc_err(), - ); - } - // revert protection: all transaction hashes must be in `reverting_tx_hashes` for tx_hash in &tx_hashes { if !bundle.reverting_tx_hashes.contains(tx_hash) { @@ -680,51 +668,6 @@ mod tests { ); } - #[tokio::test] - async fn test_err_bundle_refund_percent_not_empty() { - let bundle = EthSendBundle { - refund_percent: Some(100), - ..Default::default() - }; - assert_eq!( - validate_bundle(&bundle, 0, vec![]), - Err( - EthApiError::InvalidParams("refunds are not initially supported".into()) - .into_rpc_err() - ) - ); - } - - #[tokio::test] - async fn test_err_bundle_refund_recipient_not_empty() { - let bundle = EthSendBundle { - refund_recipient: Some(Address::random()), - ..Default::default() - }; - assert_eq!( - validate_bundle(&bundle, 0, vec![]), - Err( - EthApiError::InvalidParams("refunds are not initially supported".into()) - .into_rpc_err() - ) - ); - } - - #[tokio::test] - async fn test_err_bundle_refund_tx_hashes_not_empty() { - let bundle = EthSendBundle { - refund_tx_hashes: vec![B256::random()], - ..Default::default() - }; - assert_eq!( - validate_bundle(&bundle, 0, vec![]), - Err( - EthApiError::InvalidParams("refunds are not initially supported".into()) - .into_rpc_err() - ) - ); - } - #[tokio::test] async fn test_err_bundle_not_all_tx_hashes_in_reverting_tx_hashes() { let signer = PrivateKeySigner::random(); From ebe392d176fdb7644e049e6110ed46dd7b630566 Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 27 Oct 2025 13:43:37 -0400 Subject: [PATCH 11/14] remove unused dep + fix test --- crates/ingress-rpc/src/validation.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 8d7ce69..d708315 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -235,13 +235,11 @@ mod tests { use alloy_consensus::{TxEip1559, TxEip4844, TxEip7702}; use alloy_primitives::Bytes; use alloy_primitives::{bytes, keccak256}; - use alloy_serde::OtherFields; use alloy_signer_local::PrivateKeySigner; use op_alloy_consensus::OpTxEnvelope; use op_alloy_network::TxSignerSync; use op_alloy_network::eip2718::Encodable2718; use revm_context_interface::transaction::{AccessList, AccessListItem}; - use std::collections::BTreeMap; use std::time::{SystemTime, UNIX_EPOCH}; fn create_account(nonce: u64, balance: U256) -> AccountInfo { @@ -634,7 +632,7 @@ mod tests { encoded_txs.push(Bytes::from(encoded)); } - let bundle = EthSendBundle { + let bundle = Bundle { txs: encoded_txs, block_number: 0, min_timestamp: None, @@ -654,7 +652,7 @@ mod tests { #[tokio::test] async fn test_err_bundle_partial_transaction_dropping_not_supported() { - let bundle = EthSendBundle { + let bundle = Bundle { txs: vec![], dropping_tx_hashes: vec![B256::random()], ..Default::default() @@ -701,7 +699,7 @@ mod tests { encoded_txs.push(Bytes::from(encoded)); } - let bundle = EthSendBundle { + let bundle = Bundle { txs: encoded_txs, block_number: 0, min_timestamp: None, From 8038f21abb08d20dc31b32f420f9636d2c5e151a Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 27 Oct 2025 13:44:26 -0400 Subject: [PATCH 12/14] fmt --- Cargo.lock | 2 -- crates/ingress-rpc/Cargo.toml | 2 -- crates/ingress-rpc/src/validation.rs | 6 +----- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 056739e..837f78d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6932,7 +6932,6 @@ dependencies = [ "alloy-consensus", "alloy-primitives", "alloy-provider", - "alloy-serde", "alloy-signer-local", "anyhow", "async-trait", @@ -6951,7 +6950,6 @@ dependencies = [ "tips-core", "tokio", "tracing", - "tracing-subscriber 0.3.20", "url", ] diff --git a/crates/ingress-rpc/Cargo.toml b/crates/ingress-rpc/Cargo.toml index 9eaf45a..120aa1e 100644 --- a/crates/ingress-rpc/Cargo.toml +++ b/crates/ingress-rpc/Cargo.toml @@ -19,7 +19,6 @@ op-alloy-network.workspace = true alloy-provider.workspace = true tokio.workspace = true tracing.workspace = true -tracing-subscriber.workspace = true anyhow.workspace = true clap.workspace = true url.workspace = true @@ -35,4 +34,3 @@ op-revm.workspace = true revm-context-interface.workspace = true alloy-signer-local.workspace = true reth-optimism-evm.workspace = true -alloy-serde.workspace = true diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index d708315..5c141d1 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -168,11 +168,7 @@ pub async fn validate_tx( /// - The bundle can only contain 3 transactions at once /// - Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty /// - revert protection is not supported, all transaction hashes must be in `reverting_tx_hashes` -pub fn validate_bundle( - bundle: &Bundle, - bundle_gas: u64, - tx_hashes: Vec, -) -> RpcResult<()> { +pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64, tx_hashes: Vec) -> RpcResult<()> { // Don't allow bundles to be submitted over 1 hour into the future // TODO: make the window configurable let valid_timestamp_window = SystemTime::now() From 8aa0598896d24259a5890f8310059d736a9b282d Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 27 Oct 2025 13:49:50 -0400 Subject: [PATCH 13/14] compare using set --- crates/ingress-rpc/src/validation.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs index 5c141d1..4ff4946 100644 --- a/crates/ingress-rpc/src/validation.rs +++ b/crates/ingress-rpc/src/validation.rs @@ -9,6 +9,7 @@ use op_alloy_network::Optimism; use op_revm::{OpSpecId, l1block::L1BlockInfo}; use reth_optimism_evm::extract_l1_info_from_tx; use reth_rpc_eth_types::{EthApiError, RpcInvalidTransactionError, SignError}; +use std::collections::HashSet; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tips_core::Bundle; use tracing::warn; @@ -210,14 +211,14 @@ pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64, tx_hashes: Vec) - } // revert protection: all transaction hashes must be in `reverting_tx_hashes` - for tx_hash in &tx_hashes { - if !bundle.reverting_tx_hashes.contains(tx_hash) { - return Err(EthApiError::InvalidParams( - "Revert protection is not supported. reverting_tx_hashes must include all hashes" - .into(), - ) - .into_rpc_err()); - } + let reverting_tx_hashes_set: HashSet<_> = bundle.reverting_tx_hashes.iter().collect(); + let tx_hashes_set: HashSet<_> = tx_hashes.iter().collect(); + if reverting_tx_hashes_set != tx_hashes_set { + return Err(EthApiError::InvalidParams( + "Revert protection is not supported. reverting_tx_hashes must include all hashes" + .into(), + ) + .into_rpc_err()); } Ok(()) From 6954138b5b3fd853aaaa1e7f127ba154d601390b Mon Sep 17 00:00:00 2001 From: William Law Date: Mon, 27 Oct 2025 13:51:43 -0400 Subject: [PATCH 14/14] nit --- crates/ingress-rpc/src/service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ingress-rpc/src/service.rs b/crates/ingress-rpc/src/service.rs index 5e0f945..dafd085 100644 --- a/crates/ingress-rpc/src/service.rs +++ b/crates/ingress-rpc/src/service.rs @@ -178,9 +178,9 @@ where let bundle_with_metadata = BundleWithMetadata::load(bundle.clone()) .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; + let tx_hashes = bundle_with_metadata.txn_hashes(); let mut total_gas = 0u64; - let tx_hashes = bundle_with_metadata.txn_hashes(); for tx_data in &bundle.txs { let transaction = self.validate_tx(tx_data).await?; total_gas = total_gas.saturating_add(transaction.gas_limit());