Skip to content

Commit b0b4a87

Browse files
authored
fix(ingress): max txs per bundle in validation (#114)
Closes #113
1 parent ce1c660 commit b0b4a87

File tree

10 files changed

+123
-30
lines changed

10 files changed

+123
-30
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "buildernet-orderflow-proxy"
3-
version = "1.1.1"
3+
version = "1.1.2"
44
edition = "2021"
55
publish = false
66
build = "build.rs"

benches/validation.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use alloy_signer_local::PrivateKeySigner;
66
use buildernet_orderflow_proxy::{
77
consts::FLASHBOTS_SIGNATURE_HEADER,
88
ingress::maybe_verify_signature,
9-
primitives::{SystemBundle, SystemBundleMetadata, UtcInstant},
9+
primitives::{SystemBundleDecoder, SystemBundleMetadata, UtcInstant},
1010
priority::Priority,
1111
utils::testutils::Random,
1212
};
@@ -81,6 +81,8 @@ pub fn bench_validation(c: &mut Criterion) {
8181
group.bench_function(BenchmarkId::from_parameter(size), |b| {
8282
let mut rng = StdRng::seed_from_u64(12);
8383

84+
let decoder = SystemBundleDecoder::default();
85+
8486
// We use iter_batched here so we have an owned value for the benchmarked function (second
8587
// closure)
8688
b.iter_batched(
@@ -92,7 +94,7 @@ pub fn bench_validation(c: &mut Criterion) {
9294
signer: input.signer,
9395
priority: Priority::Medium,
9496
};
95-
let result = SystemBundle::try_decode(input.raw_bundle, metadata).unwrap();
97+
let result = decoder.try_decode(input.raw_bundle, metadata).unwrap();
9698
black_box(result);
9799
}
98100
},

src/cli.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ use alloy_primitives::Address;
44
use alloy_signer_local::PrivateKeySigner;
55
use clap::{Args, Parser, ValueHint};
66

7-
use crate::indexer::{
8-
click::{
9-
default_disk_backup_database_path, MAX_DISK_BACKUP_SIZE_BYTES, MAX_MEMORY_BACKUP_SIZE_BYTES,
7+
use crate::{
8+
indexer::{
9+
click::{
10+
default_disk_backup_database_path, MAX_DISK_BACKUP_SIZE_BYTES,
11+
MAX_MEMORY_BACKUP_SIZE_BYTES,
12+
},
13+
BUNDLE_RECEIPTS_TABLE_NAME, BUNDLE_TABLE_NAME,
1014
},
11-
BUNDLE_RECEIPTS_TABLE_NAME, BUNDLE_TABLE_NAME,
15+
SystemBundleDecoder,
1216
};
1317

1418
/// The maximum request size in bytes (10 MiB).
@@ -199,6 +203,10 @@ pub struct OrderflowIngressArgs {
199203
#[clap(long, default_value_t = MAX_REQUEST_SIZE_BYTES)]
200204
pub max_request_size: usize,
201205

206+
/// The maximum number of raw transactions per bundle.
207+
#[clap(long, default_value_t = SystemBundleDecoder::DEFAULT_MAX_TXS_PER_BUNDLE)]
208+
pub max_txs_per_bundle: usize,
209+
202210
/// Enable rate limiting.
203211
#[clap(long, default_value_t = false)]
204212
pub enable_rate_limiting: bool,
@@ -249,6 +257,7 @@ impl Default for OrderflowIngressArgs {
249257
builder_name: String::from("buildernet"),
250258
builder_hub_url: None,
251259
flashbots_signer: None,
260+
max_txs_per_bundle: 100,
252261
enable_rate_limiting: false,
253262
metrics: None,
254263
orderflow_signer: None,

src/indexer/click/models.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ pub(crate) mod tests {
383383
click::models::{BundleReceiptRow, BundleRow},
384384
tests::bundle_receipt_example,
385385
},
386-
primitives::{BundleReceipt, SystemBundle},
386+
primitives::{BundleReceipt, SystemBundleDecoder},
387387
priority::Priority,
388388
};
389389

@@ -458,9 +458,9 @@ pub(crate) mod tests {
458458

459459
assert_eq!(system_bundle.raw_bundle, Arc::new(raw_bundle_round_trip.clone()));
460460

461+
let decoder = SystemBundleDecoder::default();
461462
let system_bundle_round_trip =
462-
SystemBundle::try_decode(raw_bundle_round_trip, system_bundle.metadata.clone())
463-
.unwrap();
463+
decoder.try_decode(raw_bundle_round_trip, system_bundle.metadata.clone()).unwrap();
464464

465465
assert_eq!(system_bundle, system_bundle_round_trip);
466466
}
@@ -473,9 +473,10 @@ pub(crate) mod tests {
473473
let raw_bundle_round_trip: RawBundle = bundle_row.into();
474474

475475
assert_eq!(system_bundle.raw_bundle, Arc::new(raw_bundle_round_trip.clone()));
476+
477+
let decoder = SystemBundleDecoder::default();
476478
let system_bundle_round_trip =
477-
SystemBundle::try_decode(raw_bundle_round_trip, system_bundle.metadata.clone())
478-
.unwrap();
479+
decoder.try_decode(raw_bundle_round_trip, system_bundle.metadata.clone()).unwrap();
479480

480481
assert_eq!(system_bundle, system_bundle_round_trip);
481482
}

src/indexer/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ pub(crate) mod tests {
169169
use time::UtcDateTime;
170170

171171
use crate::{
172-
primitives::{BundleReceipt, SystemBundle, SystemBundleMetadata, UtcInstant},
172+
primitives::{
173+
BundleReceipt, SystemBundle, SystemBundleDecoder, SystemBundleMetadata, UtcInstant,
174+
},
173175
priority::Priority,
174176
};
175177

@@ -231,7 +233,8 @@ pub(crate) mod tests {
231233

232234
let metadata = SystemBundleMetadata { signer, received_at, priority: Priority::Medium };
233235

234-
SystemBundle::try_decode(bundle, metadata).unwrap()
236+
let decoder = SystemBundleDecoder::default();
237+
decoder.try_decode(bundle, metadata).unwrap()
235238
}
236239

237240
/// An example cancel bundle to use for testing.
@@ -241,7 +244,8 @@ pub(crate) mod tests {
241244
let received_at = UtcInstant::now();
242245

243246
let metadata = SystemBundleMetadata { signer, received_at, priority: Priority::Medium };
244-
SystemBundle::try_decode(bundle, metadata).unwrap()
247+
let decoder = SystemBundleDecoder::default();
248+
decoder.try_decode(bundle, metadata).unwrap()
245249
}
246250

247251
pub(crate) fn bundle_receipt_example() -> BundleReceipt {

src/ingress/error.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use crate::{jsonrpc::JsonRpcError, validation::ValidationError};
1+
use crate::{
2+
jsonrpc::JsonRpcError, primitives::SystemBundleDecodingError, validation::ValidationError,
3+
};
24
use alloy_consensus::crypto::RecoveryError;
35
use alloy_eips::eip2718::Eip2718Error;
4-
use rbuilder_primitives::serialize::{RawBundleConvertError, RawShareBundleConvertError};
6+
use rbuilder_primitives::serialize::RawShareBundleConvertError;
57

68
#[derive(Debug, thiserror::Error)]
79
pub enum IngressError {
@@ -16,7 +18,7 @@ pub enum IngressError {
1618
Decode2718(#[from] Eip2718Error),
1719
/// Bundle decoding error.
1820
#[error(transparent)]
19-
BundleDecode(#[from] RawBundleConvertError),
21+
SystemBundleDecoding(#[from] SystemBundleDecodingError),
2022
/// MEV Share bundle decoding error.
2123
#[error(transparent)]
2224
ShareBundleDecode(#[from] RawShareBundleConvertError),
@@ -26,6 +28,8 @@ pub enum IngressError {
2628
/// Serde error.
2729
#[error(transparent)]
2830
Serde(#[from] serde_json::Error),
31+
#[error("too many transactions in bundle")]
32+
TooManyTransactions,
2933
}
3034

3135
impl IngressError {
@@ -35,8 +39,9 @@ impl IngressError {
3539
Self::EmptyRawTransaction |
3640
Self::Validation(_) |
3741
Self::Decode2718(_) |
38-
Self::BundleDecode(_) |
42+
Self::SystemBundleDecoding(_) |
3943
Self::ShareBundleDecode(_) |
44+
Self::TooManyTransactions |
4045
Self::Recovery(_) => JsonRpcError::InvalidParams,
4146
Self::Serde(_) => JsonRpcError::ParseError,
4247
}

src/ingress/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use crate::{
1515
},
1616
primitives::{
1717
decode_transaction, BundleHash as _, BundleReceipt, DecodedBundle, DecodedShareBundle,
18-
EthResponse, EthereumTransaction, Samplable, SystemBundle, SystemBundleMetadata,
19-
SystemMevShareBundle, SystemTransaction, UtcInstant,
18+
EthResponse, EthereumTransaction, Samplable, SystemBundle, SystemBundleDecoder,
19+
SystemBundleMetadata, SystemMevShareBundle, SystemTransaction, UtcInstant,
2020
},
2121
priority::{pqueue::PriorityQueues, Priority},
2222
rate_limit::CounterOverTime,
@@ -62,6 +62,7 @@ pub struct OrderflowIngress {
6262
pub rate_limit_count: u64,
6363
pub score_lookback_s: u64,
6464
pub score_bucket_s: u64,
65+
pub system_bundle_decoder: SystemBundleDecoder,
6566
pub spam_thresholds: SpamThresholds,
6667
pub pqueues: PriorityQueues,
6768
pub entities: DashMap<Entity, EntityData>,
@@ -605,11 +606,12 @@ impl OrderflowIngress {
605606
self.record_queue_capacity_metrics(priority);
606607

607608
// Decode and validate the bundle.
609+
let decoder = self.system_bundle_decoder;
608610
let bundle = self
609611
.pqueues
610612
.spawn_with_priority(priority, move || {
611613
let metadata = SystemBundleMetadata { signer, received_at, priority };
612-
SystemBundle::try_decode_with_lookup(bundle, metadata, lookup)
614+
decoder.try_decode_with_lookup(bundle, metadata, lookup)
613615
})
614616
.await
615617
.inspect_err(|e| {

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::{
99
metrics::{
1010
BuilderHubMetrics, IngressHandlerMetricsExt, IngressSystemMetrics, IngressUserMetrics,
1111
},
12+
primitives::SystemBundleDecoder,
1213
runner::CliContext,
1314
statics::LOCAL_PEER_STORE,
1415
tasks::TaskExecutor,
@@ -205,6 +206,7 @@ pub async fn run_with_listeners(
205206
rate_limit_count: args.rate_limit_count,
206207
score_lookback_s: args.score_lookback_s,
207208
score_bucket_s: args.score_bucket_s,
209+
system_bundle_decoder: SystemBundleDecoder { max_txs_per_bundle: args.max_txs_per_bundle },
208210
spam_thresholds: SpamThresholds::default(),
209211
flashbots_signer: args.flashbots_signer,
210212
pqueues: Default::default(),

src/primitives/mod.rs

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ pub struct SystemBundle {
5959
pub metadata: SystemBundleMetadata,
6060
}
6161

62+
#[derive(Debug, thiserror::Error)]
63+
pub enum SystemBundleDecodingError {
64+
#[error(transparent)]
65+
RawBundleConvertError(#[from] RawBundleConvertError),
66+
#[error("bundle contains too many transactions")]
67+
TooManyTransactions,
68+
}
69+
6270
/// Decoded bundle type. Either a new, full bundle or an empty replacement bundle.
6371
#[allow(clippy::large_enum_variant)]
6472
#[derive(PartialEq, Eq, Clone, Debug)]
@@ -241,33 +249,56 @@ impl BundleHash for RawShareBundle {
241249
}
242250
}
243251

244-
impl SystemBundle {
252+
/// Decoder for system bundles with additional constraints.
253+
#[derive(Debug, Clone, Copy)]
254+
pub struct SystemBundleDecoder {
255+
/// Maximum number of transactions allowed in a bundle.
256+
pub max_txs_per_bundle: usize,
257+
}
258+
259+
impl Default for SystemBundleDecoder {
260+
fn default() -> Self {
261+
Self { max_txs_per_bundle: Self::DEFAULT_MAX_TXS_PER_BUNDLE }
262+
}
263+
}
264+
265+
impl SystemBundleDecoder {
266+
/// The maximum number of transactions allowed in a bundle received via `eth_sendBundle`.
267+
pub const DEFAULT_MAX_TXS_PER_BUNDLE: usize = 100;
268+
245269
/// Create a new system bundle from a raw bundle and additional data.
246270
/// Returns an error if the raw bundle fails to decode.
247271
pub fn try_decode(
272+
&self,
248273
bundle: RawBundle,
249274
metadata: SystemBundleMetadata,
250-
) -> Result<Self, RawBundleConvertError> {
251-
Self::try_decode_inner(bundle, metadata, None::<fn(B256) -> Option<Address>>)
275+
) -> Result<SystemBundle, SystemBundleDecodingError> {
276+
self.try_decode_inner(bundle, metadata, None::<fn(B256) -> Option<Address>>)
252277
}
253278

254279
/// Create a new system bundle from a raw bundle and additional data, using a signer lookup
255280
/// function for the transaction signers.
256281
pub fn try_decode_with_lookup(
282+
&self,
257283
bundle: RawBundle,
258284
metadata: SystemBundleMetadata,
259285
lookup: impl Fn(B256) -> Option<Address>,
260-
) -> Result<Self, RawBundleConvertError> {
261-
Self::try_decode_inner(bundle, metadata, Some(lookup))
286+
) -> Result<SystemBundle, SystemBundleDecodingError> {
287+
self.try_decode_inner(bundle, metadata, Some(lookup))
262288
}
263289

264290
/// Create a new system bundle from a raw bundle and additional data, using a signer lookup
265291
/// function for the transaction signers. Returns an error if the raw bundle fails to decode.
266292
fn try_decode_inner(
293+
&self,
267294
mut bundle: RawBundle,
268295
metadata: SystemBundleMetadata,
269296
lookup: Option<impl Fn(B256) -> Option<Address>>,
270-
) -> Result<Self, RawBundleConvertError> {
297+
) -> Result<SystemBundle, SystemBundleDecodingError> {
298+
if bundle.txs.len() > self.max_txs_per_bundle {
299+
return Err(SystemBundleDecodingError::TooManyTransactions);
300+
}
301+
271302
let raw_bundle_hash = bundle.bundle_hash();
272303
// Set the bundle hash in the metadata.
273304
bundle.metadata.bundle_hash = Some(raw_bundle_hash);
@@ -282,14 +313,16 @@ impl SystemBundle {
282313
bundle.signer = Some(metadata.signer);
283314
}
284315

285-
Ok(Self {
316+
Ok(SystemBundle {
286317
raw_bundle: Arc::new(bundle),
287318
decoded_bundle: Arc::new(decoded),
288319
bundle_hash: raw_bundle_hash,
289320
metadata,
290321
})
291322
}
323+
}
292324

325+
impl SystemBundle {
293326
/// Returns `true` if the bundle is a replacement.
294327
pub fn is_replacement(&self) -> bool {
295328
matches!(self.decoded_bundle.as_ref(), DecodedBundle::EmptyReplacement(_))
@@ -761,4 +794,39 @@ mod tests {
761794
let json = serde_json::to_value(response).unwrap();
762795
assert_eq!(json, json!(hash));
763796
}
797+
798+
#[test]
799+
fn too_many_txs_error() {
800+
let decoder = SystemBundleDecoder::default();
801+
let raw_bundle = RawBundle {
802+
txs: vec![Bytes::from(vec![0u8; 8]); decoder.max_txs_per_bundle + 1],
803+
metadata: RawBundleMetadata {
804+
version: None,
805+
block_number: None,
806+
reverting_tx_hashes: vec![],
807+
dropping_tx_hashes: vec![],
808+
replacement_uuid: None,
809+
uuid: None,
810+
signing_address: None,
811+
refund_identity: None,
812+
min_timestamp: None,
813+
max_timestamp: None,
814+
replacement_nonce: None,
815+
refund_percent: None,
816+
refund_recipient: None,
817+
refund_tx_hashes: None,
818+
delayed_refund: None,
819+
bundle_hash: None,
820+
},
821+
};
822+
let metadata = SystemBundleMetadata {
823+
signer: Address::ZERO,
824+
received_at: UtcInstant::now(),
825+
priority: Priority::Medium,
826+
};
827+
828+
// This should be the first reason decoding of such garbage data fails.
829+
let result = decoder.try_decode(raw_bundle.clone(), metadata.clone());
830+
assert!(matches!(result, Err(SystemBundleDecodingError::TooManyTransactions)));
831+
}
764832
}

0 commit comments

Comments
 (0)