Skip to content

Commit 0d5fae5

Browse files
authored
clean up bundle/revert protection tests (#50)
1 parent 209c0f2 commit 0d5fae5

File tree

5 files changed

+288
-393
lines changed

5 files changed

+288
-393
lines changed

src/bundle.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,13 @@ impl Bundle<Flashblocks> for FlashblocksBundle {
180180
&self,
181181
block: &SealedHeader<types::Header<Flashblocks>>,
182182
) -> bool {
183+
// Empty bundles are never eligible
183184
if self.transactions().is_empty() {
184-
// empty bundles are never eligible
185+
return true;
186+
}
187+
188+
// Only single transaction bundles are supported
189+
if self.transactions().len() > 1 {
185190
return true;
186191
}
187192

@@ -192,6 +197,14 @@ impl Bundle<Flashblocks> for FlashblocksBundle {
192197
return true;
193198
}
194199

200+
// min_block_number can't be larger than max_block_number
201+
if let Some(min_block_number) = self.min_block_number
202+
&& let Some(max_block_number) = self.max_block_number
203+
&& min_block_number > max_block_number
204+
{
205+
return true;
206+
}
207+
195208
if self
196209
.max_timestamp
197210
.is_some_and(|max_ts| max_ts < block.timestamp())

src/tests/bundle.rs

Lines changed: 273 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,181 @@
11
use {
22
crate::{
33
Flashblocks,
4-
tests::{assert_has_sequencer_tx, random_valid_bundle},
4+
bundle::FlashblocksBundle,
5+
tests::assert_has_sequencer_tx,
56
},
7+
jsonrpsee::core::ClientError,
8+
rand::{Rng, rng},
69
rblib::{
7-
alloy::{consensus::Transaction, primitives::U256},
10+
alloy::{
11+
consensus::Transaction,
12+
network::{TransactionBuilder, TxSignerSync},
13+
optimism::{consensus::OpTxEnvelope, rpc_types::OpTransactionRequest},
14+
primitives::{Address, U256},
15+
signers::local::PrivateKeySigner,
16+
},
817
pool::{BundleResult, BundlesApiClient},
918
prelude::*,
19+
reth::{
20+
core::primitives::SignedTransaction,
21+
optimism::primitives::OpTransactionSigned,
22+
primitives::Recovered,
23+
},
1024
test_utils::*,
1125
},
12-
tracing::debug,
1326
};
1427

28+
macro_rules! assert_ineligible {
29+
($result:expr) => {
30+
let result = $result;
31+
assert!(
32+
result.is_err(),
33+
"Expected error for this bundle, got {result:?}"
34+
);
35+
36+
let Err(ClientError::Call(error)) = result else {
37+
panic!("Expected Call error, got {result:?}");
38+
};
39+
40+
assert_eq!(
41+
error.code(),
42+
jsonrpsee::types::ErrorCode::InvalidParams.code()
43+
);
44+
45+
assert_eq!(error.message(), "bundle is ineligible for inclusion");
46+
};
47+
}
48+
49+
pub fn transfer_tx(
50+
signer: &PrivateKeySigner,
51+
nonce: u64,
52+
value: U256,
53+
) -> Recovered<OpTxEnvelope> {
54+
let mut tx = OpTransactionRequest::default()
55+
.with_nonce(nonce)
56+
.with_to(Address::random())
57+
.value(value)
58+
.with_gas_price(1_000_000_000)
59+
.with_gas_limit(21_000)
60+
.with_max_priority_fee_per_gas(1_000_000)
61+
.with_max_fee_per_gas(2_000_000)
62+
.build_unsigned()
63+
.expect("valid transaction request");
64+
65+
let sig = signer
66+
.sign_transaction_sync(&mut tx)
67+
.expect("signing should succeed");
68+
69+
OpTransactionSigned::new_unhashed(tx, sig) //
70+
.with_signer(signer.address())
71+
}
72+
73+
pub fn transfer_tx_compact(
74+
signer: u32,
75+
nonce: u64,
76+
value: u64,
77+
) -> Recovered<OpTxEnvelope> {
78+
let signer = FundedAccounts::signer(signer);
79+
transfer_tx(&signer, nonce, U256::from(value))
80+
}
81+
82+
/// Will generate a random bundle with a given number of valid transactions.
83+
/// Transaction will be sending `1_000_000` + index wei to a random address.
84+
pub fn random_valid_bundle(tx_count: usize) -> FlashblocksBundle {
85+
random_bundle_with_reverts(tx_count, 0)
86+
}
87+
88+
/// Non-reverting transactions amount value is `1_000_000` + index wei.
89+
/// Reverting transactions amount value is `2_000_000` + index wei.
90+
pub fn random_bundle_with_reverts(
91+
non_reverting: usize,
92+
reverting: usize,
93+
) -> FlashblocksBundle {
94+
const SIGNERS_COUNT: usize = FundedAccounts::len();
95+
let mut txs = Vec::new();
96+
let mut nonces = [0u64; SIGNERS_COUNT];
97+
98+
// first valid transactions
99+
for i in 0..non_reverting {
100+
let signer = rng().random_range(0..SIGNERS_COUNT);
101+
let nonce = nonces[signer];
102+
let amount = 1_000_000 + i as u64;
103+
104+
#[expect(clippy::cast_possible_truncation)]
105+
let tx = transfer_tx_compact(signer as u32, nonce, amount);
106+
txs.push(tx);
107+
nonces[signer] += 1;
108+
}
109+
110+
// then reverting transactions
111+
for i in 0..reverting {
112+
let signer = rng().random_range(0..SIGNERS_COUNT);
113+
let nonce = nonces[signer];
114+
nonces[signer] += 1;
115+
116+
#[expect(clippy::cast_possible_truncation)]
117+
let signer = FundedAccounts::signer(signer as u32);
118+
let amount = 2_000_000 + i as u64;
119+
let mut tx = OpTransactionRequest::default()
120+
.with_nonce(nonce)
121+
.value(U256::from(amount))
122+
.reverting()
123+
.with_gas_price(1_000_000_000)
124+
.with_gas_limit(100_000)
125+
.with_max_priority_fee_per_gas(1_000_000)
126+
.with_max_fee_per_gas(2_000_000)
127+
.build_unsigned()
128+
.expect("valid transaction request");
129+
130+
let sig = signer
131+
.sign_transaction_sync(&mut tx)
132+
.expect("signing should succeed");
133+
134+
let tx = OpTransactionSigned::new_unhashed(tx, sig) //
135+
.with_signer(signer.address());
136+
txs.push(tx);
137+
}
138+
139+
FlashblocksBundle::with_transactions(txs)
140+
}
141+
15142
#[tokio::test]
16-
async fn one_valid_tx_included() -> eyre::Result<()> {
143+
async fn empty_bundle_rejected() -> eyre::Result<()> {
144+
let (node, _) = Flashblocks::test_node().await?;
145+
146+
let empty_bundle = FlashblocksBundle::with_transactions(vec![]);
147+
let result = BundlesApiClient::<Flashblocks>::send_bundle(
148+
&node.rpc_client().await?,
149+
empty_bundle,
150+
)
151+
.await;
152+
153+
assert_ineligible!(result);
154+
155+
Ok(())
156+
}
157+
158+
/// This bundle should be rejected by because we only support bundles with one
159+
/// transaction
160+
#[tokio::test]
161+
async fn bundle_with_two_txs_rejected() -> eyre::Result<()> {
162+
let (node, _) = Flashblocks::test_node().await?;
163+
164+
let bundle_with_two_txs = random_valid_bundle(2);
165+
166+
let result = BundlesApiClient::<Flashblocks>::send_bundle(
167+
&node.rpc_client().await?,
168+
bundle_with_two_txs,
169+
)
170+
.await;
171+
172+
assert_ineligible!(result);
173+
174+
Ok(())
175+
}
176+
177+
#[tokio::test]
178+
async fn valid_tx_included() -> eyre::Result<()> {
17179
let (node, _) = Flashblocks::test_node().await?;
18180

19181
let bundle_with_one_tx = random_valid_bundle(1);
@@ -28,7 +190,6 @@ async fn one_valid_tx_included() -> eyre::Result<()> {
28190
assert_eq!(result, BundleResult { bundle_hash });
29191

30192
let block = node.next_block().await?;
31-
debug!("Built block: {block:#?}");
32193

33194
assert_eq!(block.number(), 1);
34195
assert_has_sequencer_tx!(&block);
@@ -39,38 +200,101 @@ async fn one_valid_tx_included() -> eyre::Result<()> {
39200
}
40201

41202
#[tokio::test]
42-
async fn two_valid_txs_included() -> eyre::Result<()> {
203+
async fn reverted_tx_not_included() -> eyre::Result<()> {
43204
let (node, _) = Flashblocks::test_node().await?;
44205

45-
let bundle_with_two_txs = random_valid_bundle(2);
46-
let bundle_hash = bundle_with_two_txs.hash();
206+
let bundle_with_reverts = random_bundle_with_reverts(0, 1);
47207

48-
let result = BundlesApiClient::<Flashblocks>::send_bundle(
208+
BundlesApiClient::<Flashblocks>::send_bundle(
49209
&node.rpc_client().await?,
50-
bundle_with_two_txs,
210+
bundle_with_reverts.clone(),
51211
)
52212
.await?;
53213

54-
assert_eq!(result, BundleResult { bundle_hash });
55-
56214
let block = node.next_block().await?;
57-
debug!("Built block: {block:#?}");
58215

59216
assert_eq!(block.number(), 1);
217+
assert_eq!(block.tx_count(), 1); // only sequencer deposit tx
218+
60219
assert_has_sequencer_tx!(&block);
61-
assert_eq!(block.tx_count(), 3); // sequencer deposit tx + 2 bundle txs
62-
assert_eq!(block.tx(1).unwrap().value(), U256::from(1_000_000));
63-
assert_eq!(block.tx(2).unwrap().value(), U256::from(1_000_001));
64220

65221
Ok(())
66222
}
67223

224+
/// Bundles that will never be eligible for inclusion in any future block
225+
/// should be rejected by the RPC before making it to the orders pool.
226+
#[tokio::test]
227+
async fn max_block_number_in_past() -> eyre::Result<()> {
228+
let (node, _) = Flashblocks::test_node().await?;
229+
230+
let block = node.next_block().await?;
231+
assert_eq!(block.number(), 1);
232+
233+
let block = node.next_block().await?;
234+
assert_eq!(block.number(), 2);
235+
236+
let mut bundle = random_valid_bundle(1);
237+
bundle.max_block_number = Some(1);
238+
239+
let result = BundlesApiClient::<Flashblocks>::send_bundle(
240+
&node.rpc_client().await?,
241+
bundle,
242+
)
243+
.await;
244+
245+
assert_ineligible!(result);
246+
247+
Ok(())
248+
}
249+
250+
/// This bundle should be rejected because its `max_timestamp` is in the past
251+
/// and it will never be eligible for inclusion in any future block.
68252
#[tokio::test]
69-
async fn min_block_timestamp_constraint() -> eyre::Result<()> {
253+
async fn max_block_timestamp_in_past() -> eyre::Result<()> {
254+
// node at genesis, block 0
255+
let (node, _) = Flashblocks::test_node().await?;
256+
let genesis_timestamp = node.config().chain.genesis_timestamp();
257+
let mut bundle = random_valid_bundle(1);
258+
bundle.max_timestamp = Some(genesis_timestamp.saturating_sub(1));
259+
260+
let result = BundlesApiClient::<Flashblocks>::send_bundle(
261+
&node.rpc_client().await?,
262+
bundle,
263+
)
264+
.await;
265+
266+
assert_ineligible!(result);
267+
268+
Ok(())
269+
}
270+
271+
#[tokio::test]
272+
async fn min_block_greater_than_max_block() -> eyre::Result<()> {
273+
// node at genesis, block 0
274+
let (node, _) = Flashblocks::test_node().await?;
275+
let mut bundle = random_valid_bundle(1);
276+
bundle.min_block_number = Some(2);
277+
bundle.max_block_number = Some(1);
278+
279+
let result = BundlesApiClient::<Flashblocks>::send_bundle(
280+
&node.rpc_client().await?,
281+
bundle,
282+
)
283+
.await;
284+
285+
assert_ineligible!(result);
286+
287+
Ok(())
288+
}
289+
290+
/// Test that a bundle with the `min_block_number` param set to a future block
291+
/// isn't included until that block.
292+
#[tokio::test]
293+
async fn min_block_number_in_future() -> eyre::Result<()> {
70294
let (node, _) = Flashblocks::test_node().await?;
71295

72296
let mut bundle_with_one_tx = random_valid_bundle(1);
73-
bundle_with_one_tx.min_block_number = Some(3);
297+
bundle_with_one_tx.min_block_number = Some(2);
74298
let bundle_hash = bundle_with_one_tx.hash();
75299
let txhash = bundle_with_one_tx.transactions()[0].tx_hash();
76300

@@ -89,15 +313,40 @@ async fn min_block_timestamp_constraint() -> eyre::Result<()> {
89313

90314
let block = node.next_block().await?; // block 2
91315
assert_eq!(block.number(), 2);
92-
assert_eq!(block.tx_count(), 1); // only sequencer tx
93-
assert_has_sequencer_tx!(&block);
94-
95-
let block = node.next_block().await?; // block 3
96-
assert_eq!(block.number(), 3);
97-
assert_eq!(block.tx_count(), 2); // sequencer tx + 1 bundle tx
316+
assert_eq!(block.tx_count(), 2); // sequencer tx + bundle tx
98317
assert_has_sequencer_tx!(&block);
99318

100319
assert!(block.includes(txhash));
101320

102321
Ok(())
103322
}
323+
324+
#[tokio::test]
325+
async fn when_disabled_reverted_txs_are_included() -> eyre::Result<()> {
326+
let (node, _) = Flashblocks::test_node_with_revert_protection_off().await?;
327+
328+
// create a bundle with one valid and one reverting tx
329+
let mut bundle_with_reverts = random_bundle_with_reverts(0, 1);
330+
let txs = bundle_with_reverts.transactions().to_vec();
331+
332+
// mark the transaction (reverting) in the bundle as allowed to revert
333+
// and optional (i.e. it can be removed from the bundle)
334+
bundle_with_reverts.reverting_tx_hashes = vec![txs[0].tx_hash()];
335+
bundle_with_reverts.dropping_tx_hashes = vec![txs[0].tx_hash()];
336+
337+
BundlesApiClient::<Flashblocks>::send_bundle(
338+
&node.rpc_client().await?,
339+
bundle_with_reverts.clone(),
340+
)
341+
.await?;
342+
343+
let block = node.next_block().await?;
344+
345+
assert_eq!(block.number(), 1);
346+
assert_eq!(block.tx_count(), 2);
347+
348+
assert_has_sequencer_tx!(&block);
349+
assert!(block.includes(txs[0].tx_hash()));
350+
351+
Ok(())
352+
}

0 commit comments

Comments
 (0)