Skip to content

Commit 3e92994

Browse files
committed
add reverting_tx_check
1 parent 431619d commit 3e92994

File tree

2 files changed

+84
-10
lines changed

2 files changed

+84
-10
lines changed

crates/ingress-rpc/src/service.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,13 @@ where
173173
}
174174

175175
let mut total_gas = 0u64;
176+
let mut tx_hashes = Vec::new();
176177
for tx_data in &bundle.txs {
177178
let transaction = self.validate_tx(tx_data).await?;
178179
total_gas = total_gas.saturating_add(transaction.gas_limit());
180+
tx_hashes.push(transaction.tx_hash());
179181
}
180182

181-
validate_bundle(bundle, total_gas)
183+
validate_bundle(bundle, total_gas, tx_hashes)
182184
}
183185
}

crates/ingress-rpc/src/validation.rs

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,12 @@ pub async fn validate_tx<T: Transaction>(
169169
/// - Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty
170170
/// - extra_fields must be empty
171171
/// - refunds are not initially supported (refund_percent, refund_recipient, refund_tx_hashes must be empty)
172-
pub fn validate_bundle(bundle: &EthSendBundle, bundle_gas: u64) -> RpcResult<()> {
172+
/// - revert protection is not supported, all transaction hashes must be in `reverting_tx_hashes`
173+
pub fn validate_bundle(
174+
bundle: &EthSendBundle,
175+
bundle_gas: u64,
176+
tx_hashes: Vec<B256>,
177+
) -> RpcResult<()> {
173178
// Don't allow bundles to be submitted over 1 hour into the future
174179
// TODO: make the window configurable
175180
let valid_timestamp_window = SystemTime::now()
@@ -225,6 +230,16 @@ pub fn validate_bundle(bundle: &EthSendBundle, bundle_gas: u64) -> RpcResult<()>
225230
);
226231
}
227232

233+
// revert protection: all transaction hashes must be in `reverting_tx_hashes`
234+
for tx_hash in &tx_hashes {
235+
if !bundle.reverting_tx_hashes.contains(tx_hash) {
236+
return Err(EthApiError::InvalidParams(
237+
"Transaction hash not found in reverting_tx_hashes".into(),
238+
)
239+
.into_rpc_err());
240+
}
241+
}
242+
228243
Ok(())
229244
}
230245

@@ -541,7 +556,7 @@ mod tests {
541556
..Default::default()
542557
};
543558
assert_eq!(
544-
validate_bundle(&bundle, 0),
559+
validate_bundle(&bundle, 0, vec![]),
545560
Err(EthApiError::InvalidParams(
546561
"Bundle cannot be more than 1 hour in the future".into()
547562
)
@@ -553,6 +568,7 @@ mod tests {
553568
async fn test_err_bundle_max_gas_limit_too_high() {
554569
let signer = PrivateKeySigner::random();
555570
let mut encoded_txs = vec![];
571+
let mut tx_hashes = vec![];
556572

557573
// Create transactions that collectively exceed MAX_BUNDLE_GAS (25M)
558574
// Each transaction uses 4M gas, so 8 transactions = 32M gas > 25M limit
@@ -574,6 +590,8 @@ mod tests {
574590

575591
let signature = signer.sign_transaction_sync(&mut tx).unwrap();
576592
let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature));
593+
let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash();
594+
tx_hashes.push(tx_hash);
577595

578596
// Encode the transaction
579597
let mut encoded = vec![];
@@ -591,7 +609,7 @@ mod tests {
591609
};
592610

593611
// Test should fail due to exceeding gas limit
594-
let result = validate_bundle(&bundle, total_gas);
612+
let result = validate_bundle(&bundle, total_gas, tx_hashes);
595613
assert!(result.is_err());
596614
if let Err(e) = result {
597615
let error_message = format!("{e:?}");
@@ -603,6 +621,7 @@ mod tests {
603621
async fn test_err_bundle_too_many_transactions() {
604622
let signer = PrivateKeySigner::random();
605623
let mut encoded_txs = vec![];
624+
let mut tx_hashes = vec![];
606625

607626
let gas = 4_000_000;
608627
let mut total_gas = 0u64;
@@ -622,6 +641,8 @@ mod tests {
622641

623642
let signature = signer.sign_transaction_sync(&mut tx).unwrap();
624643
let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature));
644+
let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash();
645+
tx_hashes.push(tx_hash);
625646

626647
// Encode the transaction
627648
let mut encoded = vec![];
@@ -639,7 +660,7 @@ mod tests {
639660
};
640661

641662
// Test should fail due to exceeding gas limit
642-
let result = validate_bundle(&bundle, total_gas);
663+
let result = validate_bundle(&bundle, total_gas, tx_hashes);
643664
assert!(result.is_err());
644665
if let Err(e) = result {
645666
let error_message = format!("{e:?}");
@@ -655,7 +676,7 @@ mod tests {
655676
..Default::default()
656677
};
657678
assert_eq!(
658-
validate_bundle(&bundle, 0),
679+
validate_bundle(&bundle, 0, vec![]),
659680
Err(
660681
EthApiError::InvalidParams("Partial transaction dropping is not supported".into())
661682
.into_rpc_err()
@@ -672,7 +693,7 @@ mod tests {
672693
..Default::default()
673694
};
674695
assert_eq!(
675-
validate_bundle(&bundle, 0),
696+
validate_bundle(&bundle, 0, vec![]),
676697
Err(EthApiError::InvalidParams("extra_fields must be empty".into()).into_rpc_err())
677698
);
678699
}
@@ -684,7 +705,7 @@ mod tests {
684705
..Default::default()
685706
};
686707
assert_eq!(
687-
validate_bundle(&bundle, 0),
708+
validate_bundle(&bundle, 0, vec![]),
688709
Err(
689710
EthApiError::InvalidParams("refunds are not initially supported".into())
690711
.into_rpc_err()
@@ -699,7 +720,7 @@ mod tests {
699720
..Default::default()
700721
};
701722
assert_eq!(
702-
validate_bundle(&bundle, 0),
723+
validate_bundle(&bundle, 0, vec![]),
703724
Err(
704725
EthApiError::InvalidParams("refunds are not initially supported".into())
705726
.into_rpc_err()
@@ -714,11 +735,62 @@ mod tests {
714735
..Default::default()
715736
};
716737
assert_eq!(
717-
validate_bundle(&bundle, 0),
738+
validate_bundle(&bundle, 0, vec![]),
718739
Err(
719740
EthApiError::InvalidParams("refunds are not initially supported".into())
720741
.into_rpc_err()
721742
)
722743
);
723744
}
745+
746+
#[tokio::test]
747+
async fn test_err_bundle_not_all_tx_hashes_in_reverting_tx_hashes() {
748+
let signer = PrivateKeySigner::random();
749+
let mut encoded_txs = vec![];
750+
let mut tx_hashes = vec![];
751+
752+
let gas = 4_000_000;
753+
let mut total_gas = 0u64;
754+
for _ in 0..4 {
755+
let mut tx = TxEip1559 {
756+
chain_id: 1,
757+
nonce: 0,
758+
gas_limit: gas,
759+
max_fee_per_gas: 200000u128,
760+
max_priority_fee_per_gas: 100000u128,
761+
to: Address::random().into(),
762+
value: U256::from(1000000u128),
763+
access_list: Default::default(),
764+
input: bytes!("").clone(),
765+
};
766+
total_gas = total_gas.saturating_add(gas);
767+
768+
let signature = signer.sign_transaction_sync(&mut tx).unwrap();
769+
let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature));
770+
let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash();
771+
tx_hashes.push(tx_hash);
772+
773+
// Encode the transaction
774+
let mut encoded = vec![];
775+
envelope.encode_2718(&mut encoded);
776+
encoded_txs.push(Bytes::from(encoded));
777+
}
778+
779+
let bundle = EthSendBundle {
780+
txs: encoded_txs,
781+
block_number: 0,
782+
min_timestamp: None,
783+
max_timestamp: None,
784+
reverting_tx_hashes: tx_hashes[..2].to_vec(),
785+
..Default::default()
786+
};
787+
788+
// Test should fail due to exceeding gas limit
789+
let result = validate_bundle(&bundle, total_gas, tx_hashes);
790+
assert!(result.is_err());
791+
if let Err(e) = result {
792+
let error_message = format!("{e:?}");
793+
assert!(error_message.contains("Bundle can only contain 3 transactions"));
794+
}
795+
}
724796
}

0 commit comments

Comments
 (0)