Skip to content

Commit f04c52c

Browse files
authored
feat: extend validate_bundle criteria (#35)
* 25m gas per bundle * max 3 txs for now * partial tx not supported * extra fields must be empty * no refunds * add reverting_tx_check * comments * use bundlewmetadata * no more empty fields * no refund * remove unused dep + fix test * fmt * compare using set * nit
1 parent 4306216 commit f04c52c

File tree

5 files changed

+165
-13
lines changed

5 files changed

+165
-13
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ alloy-primitives = { version = "1.3.1", default-features = false, features = [
2828
alloy-rpc-types = { version = "1.0.35", default-features = false }
2929
alloy-consensus = { version = "1.0.35" }
3030
alloy-provider = { version = "1.0.35" }
31+
alloy-rpc-types-mev = "1.0.35"
32+
alloy-serde = "1.0.41"
3133

3234
# op-alloy
3335
op-alloy-network = { version = "0.21.0", default-features = false }

crates/ingress-rpc/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ op-alloy-network.workspace = true
1919
alloy-provider.workspace = true
2020
tokio.workspace = true
2121
tracing.workspace = true
22-
tracing-subscriber.workspace = true
2322
anyhow.workspace = true
2423
clap.workspace = true
2524
url.workspace = true

crates/ingress-rpc/src/service.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,16 @@ where
176176
);
177177
}
178178

179+
let bundle_with_metadata = BundleWithMetadata::load(bundle.clone())
180+
.map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?;
181+
let tx_hashes = bundle_with_metadata.txn_hashes();
182+
179183
let mut total_gas = 0u64;
180184
for tx_data in &bundle.txs {
181185
let transaction = self.validate_tx(tx_data).await?;
182186
total_gas = total_gas.saturating_add(transaction.gas_limit());
183187
}
184-
validate_bundle(&bundle, total_gas)?;
185-
186-
let bundle_with_metadata = BundleWithMetadata::load(bundle)
187-
.map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?;
188+
validate_bundle(&bundle, total_gas, tx_hashes)?;
188189

189190
Ok(bundle_with_metadata)
190191
}

crates/ingress-rpc/src/validation.rs

Lines changed: 158 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ use op_alloy_network::Optimism;
99
use op_revm::{OpSpecId, l1block::L1BlockInfo};
1010
use reth_optimism_evm::extract_l1_info_from_tx;
1111
use reth_rpc_eth_types::{EthApiError, RpcInvalidTransactionError, SignError};
12+
use std::collections::HashSet;
1213
use std::time::{Duration, SystemTime, UNIX_EPOCH};
1314
use tips_core::Bundle;
1415
use tracing::warn;
1516

16-
// TODO: make this configurable
17-
const MAX_BUNDLE_GAS: u64 = 30_000_000;
17+
const MAX_BUNDLE_GAS: u64 = 25_000_000;
1818

1919
/// Account info for a given address
2020
pub struct AccountInfo {
@@ -166,7 +166,10 @@ pub async fn validate_tx<T: Transaction>(
166166
/// Helper function to validate propeties of a bundle. A bundle is valid if it satisfies the following criteria:
167167
/// - The bundle's max_timestamp is not more than 1 hour in the future
168168
/// - The bundle's gas limit is not greater than the maximum allowed gas limit
169-
pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> {
169+
/// - The bundle can only contain 3 transactions at once
170+
/// - Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty
171+
/// - revert protection is not supported, all transaction hashes must be in `reverting_tx_hashes`
172+
pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64, tx_hashes: Vec<B256>) -> RpcResult<()> {
170173
// Don't allow bundles to be submitted over 1 hour into the future
171174
// TODO: make the window configurable
172175
let valid_timestamp_window = SystemTime::now()
@@ -191,6 +194,33 @@ pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64) -> RpcResult<()> {
191194
);
192195
}
193196

197+
// Can only provide 3 transactions at once
198+
if bundle.txs.len() > 3 {
199+
return Err(
200+
EthApiError::InvalidParams("Bundle can only contain 3 transactions".into())
201+
.into_rpc_err(),
202+
);
203+
}
204+
205+
// Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty
206+
if !bundle.dropping_tx_hashes.is_empty() {
207+
return Err(EthApiError::InvalidParams(
208+
"Partial transaction dropping is not supported".into(),
209+
)
210+
.into_rpc_err());
211+
}
212+
213+
// revert protection: all transaction hashes must be in `reverting_tx_hashes`
214+
let reverting_tx_hashes_set: HashSet<_> = bundle.reverting_tx_hashes.iter().collect();
215+
let tx_hashes_set: HashSet<_> = tx_hashes.iter().collect();
216+
if reverting_tx_hashes_set != tx_hashes_set {
217+
return Err(EthApiError::InvalidParams(
218+
"Revert protection is not supported. reverting_tx_hashes must include all hashes"
219+
.into(),
220+
)
221+
.into_rpc_err());
222+
}
223+
194224
Ok(())
195225
}
196226

@@ -505,7 +535,7 @@ mod tests {
505535
..Default::default()
506536
};
507537
assert_eq!(
508-
validate_bundle(&bundle, 0),
538+
validate_bundle(&bundle, 0, vec![]),
509539
Err(EthApiError::InvalidParams(
510540
"Bundle cannot be more than 1 hour in the future".into()
511541
)
@@ -517,9 +547,10 @@ mod tests {
517547
async fn test_err_bundle_max_gas_limit_too_high() {
518548
let signer = PrivateKeySigner::random();
519549
let mut encoded_txs = vec![];
550+
let mut tx_hashes = vec![];
520551

521-
// Create transactions that collectively exceed MAX_BUNDLE_GAS (30M)
522-
// Each transaction uses 4M gas, so 8 transactions = 32M gas > 30M limit
552+
// Create transactions that collectively exceed MAX_BUNDLE_GAS (25M)
553+
// Each transaction uses 4M gas, so 8 transactions = 32M gas > 25M limit
523554
let gas = 4_000_000;
524555
let mut total_gas = 0u64;
525556
for _ in 0..8 {
@@ -538,6 +569,8 @@ mod tests {
538569

539570
let signature = signer.sign_transaction_sync(&mut tx).unwrap();
540571
let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature));
572+
let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash();
573+
tx_hashes.push(tx_hash);
541574

542575
// Encode the transaction
543576
let mut encoded = vec![];
@@ -555,11 +588,129 @@ mod tests {
555588
};
556589

557590
// Test should fail due to exceeding gas limit
558-
let result = validate_bundle(&bundle, total_gas);
591+
let result = validate_bundle(&bundle, total_gas, tx_hashes);
559592
assert!(result.is_err());
560593
if let Err(e) = result {
561594
let error_message = format!("{e:?}");
562595
assert!(error_message.contains("Bundle gas limit exceeds maximum allowed"));
563596
}
564597
}
598+
599+
#[tokio::test]
600+
async fn test_err_bundle_too_many_transactions() {
601+
let signer = PrivateKeySigner::random();
602+
let mut encoded_txs = vec![];
603+
let mut tx_hashes = vec![];
604+
605+
let gas = 4_000_000;
606+
let mut total_gas = 0u64;
607+
for _ in 0..4 {
608+
let mut tx = TxEip1559 {
609+
chain_id: 1,
610+
nonce: 0,
611+
gas_limit: gas,
612+
max_fee_per_gas: 200000u128,
613+
max_priority_fee_per_gas: 100000u128,
614+
to: Address::random().into(),
615+
value: U256::from(1000000u128),
616+
access_list: Default::default(),
617+
input: bytes!("").clone(),
618+
};
619+
total_gas = total_gas.saturating_add(gas);
620+
621+
let signature = signer.sign_transaction_sync(&mut tx).unwrap();
622+
let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature));
623+
let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash();
624+
tx_hashes.push(tx_hash);
625+
626+
// Encode the transaction
627+
let mut encoded = vec![];
628+
envelope.encode_2718(&mut encoded);
629+
encoded_txs.push(Bytes::from(encoded));
630+
}
631+
632+
let bundle = Bundle {
633+
txs: encoded_txs,
634+
block_number: 0,
635+
min_timestamp: None,
636+
max_timestamp: None,
637+
reverting_tx_hashes: vec![],
638+
..Default::default()
639+
};
640+
641+
// Test should fail due to exceeding gas limit
642+
let result = validate_bundle(&bundle, total_gas, tx_hashes);
643+
assert!(result.is_err());
644+
if let Err(e) = result {
645+
let error_message = format!("{e:?}");
646+
assert!(error_message.contains("Bundle can only contain 3 transactions"));
647+
}
648+
}
649+
650+
#[tokio::test]
651+
async fn test_err_bundle_partial_transaction_dropping_not_supported() {
652+
let bundle = Bundle {
653+
txs: vec![],
654+
dropping_tx_hashes: vec![B256::random()],
655+
..Default::default()
656+
};
657+
assert_eq!(
658+
validate_bundle(&bundle, 0, vec![]),
659+
Err(
660+
EthApiError::InvalidParams("Partial transaction dropping is not supported".into())
661+
.into_rpc_err()
662+
)
663+
);
664+
}
665+
666+
#[tokio::test]
667+
async fn test_err_bundle_not_all_tx_hashes_in_reverting_tx_hashes() {
668+
let signer = PrivateKeySigner::random();
669+
let mut encoded_txs = vec![];
670+
let mut tx_hashes = vec![];
671+
672+
let gas = 4_000_000;
673+
let mut total_gas = 0u64;
674+
for _ in 0..4 {
675+
let mut tx = TxEip1559 {
676+
chain_id: 1,
677+
nonce: 0,
678+
gas_limit: gas,
679+
max_fee_per_gas: 200000u128,
680+
max_priority_fee_per_gas: 100000u128,
681+
to: Address::random().into(),
682+
value: U256::from(1000000u128),
683+
access_list: Default::default(),
684+
input: bytes!("").clone(),
685+
};
686+
total_gas = total_gas.saturating_add(gas);
687+
688+
let signature = signer.sign_transaction_sync(&mut tx).unwrap();
689+
let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature));
690+
let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash();
691+
tx_hashes.push(tx_hash);
692+
693+
// Encode the transaction
694+
let mut encoded = vec![];
695+
envelope.encode_2718(&mut encoded);
696+
encoded_txs.push(Bytes::from(encoded));
697+
}
698+
699+
let bundle = Bundle {
700+
txs: encoded_txs,
701+
block_number: 0,
702+
min_timestamp: None,
703+
max_timestamp: None,
704+
reverting_tx_hashes: tx_hashes[..2].to_vec(),
705+
..Default::default()
706+
};
707+
708+
// Test should fail due to exceeding gas limit
709+
let result = validate_bundle(&bundle, total_gas, tx_hashes);
710+
assert!(result.is_err());
711+
if let Err(e) = result {
712+
let error_message = format!("{e:?}");
713+
assert!(error_message.contains("Bundle can only contain 3 transactions"));
714+
}
715+
}
565716
}

0 commit comments

Comments
 (0)