Skip to content

Commit 7e24aba

Browse files
authored
Add reverting hashes + min block number to Bundle (#115)
* Implement bundle reverting hashes * Merge main * Fix * Add min_block_number * fix litn * Fix * Fix comment * Mark invalid * Rename reverting_hashes * Remove Option * Revert "Remove Option" This reverts commit 4087d35.
1 parent 3e9ffeb commit 7e24aba

File tree

6 files changed

+180
-28
lines changed

6 files changed

+180
-28
lines changed

crates/op-rbuilder/src/builders/context.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use alloy_consensus::{Eip658Value, Transaction, TxEip1559};
1+
use alloy_consensus::{
2+
conditional::BlockConditionalAttributes, Eip658Value, Transaction, TxEip1559,
3+
};
24
use alloy_eips::{eip7623::TOTAL_COST_FLOOR_PER_TOKEN, Encodable2718, Typed2718};
35
use alloy_op_evm::block::receipt_builder::OpReceiptBuilder;
46
use alloy_primitives::{Address, Bytes, TxKind, U256};
@@ -20,6 +22,7 @@ use reth_optimism_node::OpPayloadBuilderAttributes;
2022
use reth_optimism_payload_builder::{config::OpDAConfig, error::OpPayloadBuilderError};
2123
use reth_optimism_primitives::{OpReceipt, OpTransactionSigned};
2224
use reth_optimism_txpool::{
25+
conditional::MaybeConditionalTransaction,
2326
estimated_da_size::DataAvailabilitySized,
2427
interop::{is_valid_interop, MaybeInteropTransaction},
2528
};
@@ -342,17 +345,37 @@ impl OpPayloadBuilderCtx {
342345
.da_tx_size_limit
343346
.set(tx_da_limit.map_or(-1.0, |v| v as f64));
344347

348+
let block_attr = BlockConditionalAttributes {
349+
number: self.block_number(),
350+
timestamp: self.attributes().timestamp(),
351+
};
352+
345353
while let Some(tx) = best_txs.next(()) {
346354
let interop = tx.interop_deadline();
347-
let exclude_reverting_txs = tx.exclude_reverting_txs();
355+
let reverted_hashes = tx.reverted_hashes().clone();
356+
let conditional = tx.conditional().cloned();
357+
348358
let tx_da_size = tx.estimated_da_size();
349359
let tx = tx.into_consensus();
350360
let tx_hash = tx.tx_hash();
351361

362+
// exclude reverting transaction if:
363+
// - the transaction comes from a bundle and the hash **is not** in reverted hashes
364+
let exclude_reverting_txs =
365+
reverted_hashes.is_some() && !reverted_hashes.unwrap().contains(&tx_hash);
366+
352367
let log_txn = |result: TxnExecutionResult| {
353368
info!(target: "payload_builder", tx_hash = ?tx_hash, tx_da_size = ?tx_da_size, exclude_reverting_txs = ?exclude_reverting_txs, result = %result, "Considering transaction");
354369
};
355370

371+
if let Some(conditional) = conditional {
372+
// TODO: ideally we should get this from the txpool stream
373+
if !conditional.matches_block_attributes(&block_attr) {
374+
best_txs.mark_invalid(tx.signer(), tx.nonce());
375+
continue;
376+
}
377+
}
378+
356379
// TODO: remove this condition and feature once we are comfortable enabling interop for everything
357380
if cfg!(feature = "interop") {
358381
// We skip invalid cross chain txs, they would be removed on the next block update in

crates/op-rbuilder/src/primitives/bundle.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,30 @@ pub struct Bundle {
99
#[serde(rename = "txs")]
1010
pub transactions: Vec<Bytes>,
1111

12+
#[serde(rename = "reverting_tx_hashes")]
13+
pub reverting_hashes: Vec<B256>,
14+
1215
#[serde(
1316
default,
1417
rename = "maxBlockNumber",
1518
with = "alloy_serde::quantity::opt",
1619
skip_serializing_if = "Option::is_none"
1720
)]
1821
pub block_number_max: Option<u64>,
22+
23+
#[serde(
24+
default,
25+
rename = "minBlockNumber",
26+
with = "alloy_serde::quantity::opt",
27+
skip_serializing_if = "Option::is_none"
28+
)]
29+
pub block_number_min: Option<u64>,
1930
}
2031

2132
impl Bundle {
2233
pub fn conditional(&self) -> TransactionConditional {
2334
TransactionConditional {
24-
block_number_min: None,
35+
block_number_min: self.block_number_min,
2536
block_number_max: self.block_number_max,
2637
known_accounts: Default::default(),
2738
timestamp_max: None,

crates/op-rbuilder/src/revert_protection.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ where
106106
};
107107

108108
if let Some(block_number_max) = bundle.block_number_max {
109+
if let Some(block_number_min) = bundle.block_number_min {
110+
if block_number_min > block_number_max {
111+
return Err(EthApiError::InvalidParams(
112+
"block_number_min is greater than block_number_max".into(),
113+
)
114+
.into());
115+
}
116+
}
117+
109118
// The max block cannot be a past block
110119
if block_number_max <= last_block_number {
111120
return Err(
@@ -128,7 +137,7 @@ where
128137
let mut pool_transaction: FBPooledTransaction =
129138
OpPooledTransaction::from_pooled(recovered).into();
130139

131-
pool_transaction.set_exclude_reverting_txs(true);
140+
pool_transaction.set_reverted_hashes(bundle.reverting_hashes.clone());
132141
pool_transaction.set_conditional(bundle.conditional());
133142

134143
let hash = self

crates/op-rbuilder/src/tests/framework/txs.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use super::FUNDED_PRIVATE_KEYS;
1818
#[derive(Clone, Copy, Default)]
1919
pub struct BundleOpts {
2020
pub block_number_max: Option<u64>,
21+
pub block_number_min: Option<u64>,
2122
}
2223

2324
#[derive(Clone)]
@@ -28,6 +29,7 @@ pub struct TransactionBuilder {
2829
base_fee: Option<u128>,
2930
tx: TxEip1559,
3031
bundle_opts: Option<BundleOpts>,
32+
with_reverted_hash: bool,
3133
key: Option<u64>,
3234
}
3335

@@ -44,6 +46,7 @@ impl TransactionBuilder {
4446
..Default::default()
4547
},
4648
bundle_opts: None,
49+
with_reverted_hash: false,
4750
key: None,
4851
}
4952
}
@@ -93,6 +96,11 @@ impl TransactionBuilder {
9396
self
9497
}
9598

99+
pub fn with_reverted_hash(mut self) -> Self {
100+
self.with_reverted_hash = true;
101+
self
102+
}
103+
96104
pub fn with_revert(mut self) -> Self {
97105
self.tx.input = hex!("60006000fd").into();
98106
self
@@ -145,16 +153,24 @@ impl TransactionBuilder {
145153
}
146154

147155
pub async fn send(self) -> eyre::Result<PendingTransactionBuilder<Optimism>> {
156+
let with_reverted_hash = self.with_reverted_hash;
148157
let bundle_opts = self.bundle_opts;
149158
let provider = self.provider.clone();
150159
let transaction = self.build().await;
160+
let txn_hash = transaction.tx_hash();
151161
let transaction_encoded = transaction.encoded_2718();
152162

153163
if let Some(bundle_opts) = bundle_opts {
154164
// Send the transaction as a bundle with the bundle options
155165
let bundle = Bundle {
156166
transactions: vec![transaction_encoded.into()],
167+
reverting_hashes: if with_reverted_hash {
168+
vec![txn_hash]
169+
} else {
170+
vec![]
171+
},
157172
block_number_max: bundle_opts.block_number_max,
173+
block_number_min: bundle_opts.block_number_min,
158174
};
159175

160176
let result: BundleResult = provider

crates/op-rbuilder/src/tests/vanilla/revert.rs

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async fn revert_protection_monitor_transaction_gc() -> eyre::Result<()> {
2727
.with_revert()
2828
.with_bundle(BundleOpts {
2929
block_number_max: Some(i),
30+
block_number_min: None,
3031
})
3132
.send()
3233
.await?;
@@ -123,42 +124,106 @@ async fn revert_protection_bundle() -> eyre::Result<()> {
123124
assert!(block_generated.includes(*valid_bundle.tx_hash()));
124125

125126
let bundle_opts = BundleOpts {
126-
block_number_max: Some(4),
127+
block_number_max: Some(5),
128+
block_number_min: None,
127129
};
128130

129-
let reverted_bundle = harness
131+
let reverted_bundle_0 = harness
130132
.create_transaction()
131133
.with_revert()
134+
.with_reverted_hash()
132135
.with_bundle(bundle_opts)
133136
.send()
134137
.await?;
135138

136-
// Test 2: Bundle reverts. It is not included in the block
139+
// Test 2: Bundle reverts. It is included in the block since the transaction
140+
// includes the reverted_hashes field
137141
let block_generated = generator.generate_block().await?; // Block 3
138-
assert!(block_generated.not_includes(*reverted_bundle.tx_hash()));
142+
assert!(block_generated.includes(*reverted_bundle_0.tx_hash()));
143+
144+
let reverted_bundle_1 = harness
145+
.create_transaction()
146+
.with_revert()
147+
.with_bundle(bundle_opts)
148+
.send()
149+
.await?;
150+
151+
// Test 3: Bundle reverts. It is not included in the block since it reverts
152+
// and the hash is not in the reverted_hashes field.
153+
let block_generated = generator.generate_block().await?; // Block 4
154+
assert!(block_generated.not_includes(*reverted_bundle_1.tx_hash()));
139155

140156
// After the block the transaction is still pending in the pool
141157
assert!(harness
142-
.check_tx_in_pool(*reverted_bundle.tx_hash())
158+
.check_tx_in_pool(*reverted_bundle_1.tx_hash())
143159
.await?
144160
.is_pending());
145161

146162
// Test 3: Chain progresses beyond the bundle range. The transaction is dropped from the pool
147-
generator.generate_block().await?; // Block 4
163+
generator.generate_block().await?; // Block 5
148164
assert!(harness
149-
.check_tx_in_pool(*reverted_bundle.tx_hash())
165+
.check_tx_in_pool(*reverted_bundle_1.tx_hash())
150166
.await?
151167
.is_pending());
152168

153-
generator.generate_block().await?; // Block 5
169+
generator.generate_block().await?; // Block 6
154170
assert!(harness
155-
.check_tx_in_pool(*reverted_bundle.tx_hash())
171+
.check_tx_in_pool(*reverted_bundle_1.tx_hash())
156172
.await?
157173
.is_dropped());
158174

159175
Ok(())
160176
}
161177

178+
/// Test the behaviour of the revert protection bundle with a min block number.
179+
#[tokio::test]
180+
async fn revert_protection_bundle_min_block_number() -> eyre::Result<()> {
181+
let harness = TestHarnessBuilder::new("revert_protection_bundle_min_block_number")
182+
.with_revert_protection()
183+
.build()
184+
.await?;
185+
186+
let mut generator = harness.block_generator().await?;
187+
188+
// The bundle is valid when the min block number is equal to the current block
189+
let bundle_with_min_block = harness
190+
.create_transaction()
191+
.with_revert() // the transaction reverts but it is included in the block
192+
.with_reverted_hash()
193+
.with_bundle(BundleOpts {
194+
block_number_max: None,
195+
block_number_min: Some(2),
196+
})
197+
.send()
198+
.await?;
199+
200+
let block = generator.generate_block().await?; // Block 1, bundle still not valid
201+
assert!(block.not_includes(*bundle_with_min_block.tx_hash()));
202+
203+
let block = generator.generate_block().await?; // Block 2, bundle is valid
204+
assert!(block.includes(*bundle_with_min_block.tx_hash()));
205+
206+
// Send a bundle with a match of min and max block number
207+
let bundle_with_min_and_max_block = harness
208+
.create_transaction()
209+
.with_revert()
210+
.with_reverted_hash()
211+
.with_bundle(BundleOpts {
212+
block_number_max: Some(4),
213+
block_number_min: Some(4),
214+
})
215+
.send()
216+
.await?;
217+
218+
let block = generator.generate_block().await?; // Block 3, bundle still not valid
219+
assert!(block.not_includes(*bundle_with_min_and_max_block.tx_hash()));
220+
221+
let block = generator.generate_block().await?; // Block 4, bundle is valid
222+
assert!(block.includes(*bundle_with_min_and_max_block.tx_hash()));
223+
224+
Ok(())
225+
}
226+
162227
/// Test the range limits for the revert protection bundle.
163228
#[tokio::test]
164229
async fn revert_protection_bundle_range_limits() -> eyre::Result<()> {
@@ -176,31 +241,54 @@ async fn revert_protection_bundle_range_limits() -> eyre::Result<()> {
176241
async fn send_bundle(
177242
harness: &TestHarness,
178243
block_number_max: u64,
244+
block_number_min: Option<u64>,
179245
) -> eyre::Result<PendingTransactionBuilder<Optimism>> {
180246
harness
181247
.create_transaction()
182248
.with_bundle(BundleOpts {
183249
block_number_max: Some(block_number_max),
250+
block_number_min: block_number_min,
184251
})
185252
.send()
186253
.await
187254
}
188255

189256
// Max block cannot be a past block
190-
assert!(send_bundle(&harness, 1).await.is_err());
257+
assert!(send_bundle(&harness, 1, None).await.is_err());
191258

192259
// Bundles are valid if their max block in in between the current block and the max block range
193-
let next_valid_block = 3;
260+
let current_block = 2;
261+
let next_valid_block = current_block + 1;
194262

195263
for i in next_valid_block..next_valid_block + MAX_BLOCK_RANGE_BLOCKS {
196-
assert!(send_bundle(&harness, i).await.is_ok());
264+
assert!(send_bundle(&harness, i, None).await.is_ok());
197265
}
198266

199267
// A bundle with a block out of range is invalid
268+
assert!(send_bundle(
269+
&harness,
270+
next_valid_block + MAX_BLOCK_RANGE_BLOCKS + 1,
271+
None
272+
)
273+
.await
274+
.is_err());
275+
276+
// A bundle with a min block number higher than the max block is invalid
277+
assert!(send_bundle(&harness, 1, Some(2)).await.is_err());
278+
279+
// A bundle with a min block number lower or equal to the current block is valid
280+
assert!(send_bundle(&harness, next_valid_block, Some(current_block))
281+
.await
282+
.is_ok());
283+
assert!(send_bundle(&harness, next_valid_block, Some(0))
284+
.await
285+
.is_ok());
286+
287+
// A bundle with a min block equal to max block is valid
200288
assert!(
201-
send_bundle(&harness, next_valid_block + MAX_BLOCK_RANGE_BLOCKS + 1)
289+
send_bundle(&harness, next_valid_block, Some(next_valid_block))
202290
.await
203-
.is_err()
291+
.is_ok()
204292
);
205293

206294
Ok(())
@@ -249,6 +337,7 @@ async fn revert_protection_check_transaction_receipt_status_message() -> eyre::R
249337
.with_revert()
250338
.with_bundle(BundleOpts {
251339
block_number_max: Some(3),
340+
block_number_min: None,
252341
})
253342
.send()
254343
.await?;

0 commit comments

Comments
 (0)