Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ codegen-units = 1

[workspace.dependencies]
# local
base-bundles = { path = "crates/bundles" }
base-fbal = { path = "crates/fbal" }
base-reth-cli = { path = "crates/cli" }
base-reth-rpc = { path = "crates/rpc" }
Expand Down Expand Up @@ -103,6 +104,7 @@ alloy-trie = "0.9.1"
alloy-eips = "1.0.41"
alloy-serde = "1.0.41"
alloy-genesis = "1.0.41"
alloy-signer-local = "1.0.41"
alloy-hardforks = "0.4.4"
alloy-provider = "1.0.41"
alloy-contract = "1.0.41"
Expand All @@ -116,12 +118,13 @@ alloy-rpc-types-eth = "1.0.41"
alloy-rpc-types-engine = "1.0.41"

# op-alloy
alloy-op-evm = { version = "0.23.3", default-features = false }
op-alloy-flz = "0.13.1"
op-alloy-network = "0.22.0"
op-alloy-rpc-types = "0.22.0"
op-alloy-consensus = "0.22.0"
op-alloy-rpc-jsonrpsee = "0.22.0"
op-alloy-rpc-types-engine = "0.22.0"
alloy-op-evm = { version = "0.23.3", default-features = false }

# tokio
tokio = "1.48.0"
Expand Down
39 changes: 39 additions & 0 deletions crates/bundles/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "base-bundles"
description = "Bundle types for transaction bundles"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[lints]
workspace = true

[features]
test-utils = [ "dep:alloy-signer-local", "dep:op-alloy-rpc-types" ]

[dependencies]
# alloy
alloy-serde.workspace = true
alloy-primitives = { workspace = true, features = ["serde"] }
alloy-consensus = { workspace = true, features = ["std"] }
alloy-provider = { workspace = true, features = ["reqwest"] }

# op-alloy
op-alloy-flz.workspace = true
op-alloy-consensus = { workspace = true, features = ["std", "k256", "serde"] }

# misc
uuid = { workspace = true, features = ["v4", "serde"] }
serde = { workspace = true, features = ["std", "derive"] }

# test-utils (optional)
alloy-signer-local = { workspace = true, optional = true }
op-alloy-rpc-types = { workspace = true, features = ["std"], optional = true }

[dev-dependencies]
serde_json = { workspace = true, features = ["std"] }
alloy-signer-local.workspace = true
op-alloy-rpc-types = { workspace = true, features = ["std"] }
56 changes: 56 additions & 0 deletions crates/bundles/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# `base-bundles`

<a href="https://github.com/base/node-reth/actions/workflows/ci.yml"><img src="https://github.com/base/node-reth/actions/workflows/ci.yml/badge.svg?label=ci" alt="CI"></a>
<a href="https://github.com/base/node-reth/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-d1d1f6.svg?label=license&labelColor=2a2f35" alt="MIT License"></a>

Types for transaction bundles used in Base's flashblocks infrastructure. Provides types for raw bundles, parsed bundles with decoded transactions, accepted bundles with metering data, and bundle cancellation.

## Overview

- **`Bundle`**: Raw bundle type for API requests, mirrors the `eth_sendBundle` format with support for flashblock targeting.
- **`ParsedBundle`**: Decoded bundle with recovered transaction signers, created from raw bundles.
- **`AcceptedBundle`**: Validated and metered bundle ready for inclusion, includes simulation results.
- **`MeterBundleResponse`**: Simulation response containing gas usage, coinbase diff, and per-transaction results.
- **`CancelBundle`**: Request type for cancelling a bundle by its replacement UUID.

## Usage

Add the dependency to your `Cargo.toml`:

```toml
[dependencies]
base-bundles = { git = "https://github.com/base/node-reth" }
```

Parse and validate a bundle:

```rust,ignore
use base_bundles::{Bundle, ParsedBundle, AcceptedBundle, MeterBundleResponse};

// Decode a raw bundle into a parsed bundle with recovered signers
let bundle: Bundle = serde_json::from_str(json)?;
let parsed: ParsedBundle = bundle.try_into()?;

// After metering, create an accepted bundle
let meter_response: MeterBundleResponse = simulate_bundle(&parsed);
let accepted = AcceptedBundle::new(parsed, meter_response);
```

Use bundle extension traits for utility methods:

```rust,ignore
use base_bundles::{ParsedBundle, BundleExtensions};

let parsed: ParsedBundle = bundle.try_into()?;

// Compute bundle hash, get transaction hashes, senders, gas limits, and DA size
let hash = parsed.bundle_hash();
let tx_hashes = parsed.txn_hashes();
let senders = parsed.senders();
let total_gas = parsed.gas_limit();
let da_bytes = parsed.da_size();
```

## License

Licensed under the MIT license, as found in [`LICENSE`](../../LICENSE).
179 changes: 179 additions & 0 deletions crates/bundles/src/accepted.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
//! Accepted bundle type that has been validated and metered.

use alloy_consensus::transaction::Recovered;
use alloy_primitives::TxHash;
use op_alloy_consensus::OpTxEnvelope;
use uuid::Uuid;

use crate::{MeterBundleResponse, ParsedBundle};

/// `AcceptedBundle` is the type that is sent over the wire after validation.
///
/// This represents a bundle that has been decoded, validated, and metered.
#[derive(Debug, Clone)]
pub struct AcceptedBundle {
/// Unique identifier for this bundle instance.
pub uuid: Uuid,

/// Decoded and recovered transactions.
pub txs: Vec<Recovered<OpTxEnvelope>>,

/// The target block number for inclusion.
pub block_number: u64,

/// Minimum flashblock number for inclusion.
pub flashblock_number_min: Option<u64>,

/// Maximum flashblock number for inclusion.
pub flashblock_number_max: Option<u64>,

/// Minimum timestamp for inclusion.
pub min_timestamp: Option<u64>,

/// Maximum timestamp for inclusion.
pub max_timestamp: Option<u64>,

/// Transaction hashes that are allowed to revert.
pub reverting_tx_hashes: Vec<TxHash>,

/// UUID for bundle replacement (if this bundle replaces another).
pub replacement_uuid: Option<Uuid>,

/// Transaction hashes that should be dropped from the pool.
pub dropping_tx_hashes: Vec<TxHash>,

/// Metering response from bundle simulation.
pub meter_bundle_response: MeterBundleResponse,
}

impl AcceptedBundle {
/// Creates a new accepted bundle from a parsed bundle and metering response.
pub fn new(bundle: ParsedBundle, meter_bundle_response: MeterBundleResponse) -> Self {
Self {
uuid: bundle.replacement_uuid.unwrap_or_else(Uuid::new_v4),
txs: bundle.txs,
block_number: bundle.block_number,
flashblock_number_min: bundle.flashblock_number_min,
flashblock_number_max: bundle.flashblock_number_max,
min_timestamp: bundle.min_timestamp,
max_timestamp: bundle.max_timestamp,
reverting_tx_hashes: bundle.reverting_tx_hashes,
replacement_uuid: bundle.replacement_uuid,
dropping_tx_hashes: bundle.dropping_tx_hashes,
meter_bundle_response,
}
}

/// Returns the unique identifier of this bundle.
pub const fn uuid(&self) -> &Uuid {
&self.uuid
}
}

impl From<AcceptedBundle> for ParsedBundle {
fn from(accepted_bundle: AcceptedBundle) -> Self {
Self {
txs: accepted_bundle.txs,
block_number: accepted_bundle.block_number,
flashblock_number_min: accepted_bundle.flashblock_number_min,
flashblock_number_max: accepted_bundle.flashblock_number_max,
min_timestamp: accepted_bundle.min_timestamp,
max_timestamp: accepted_bundle.max_timestamp,
reverting_tx_hashes: accepted_bundle.reverting_tx_hashes,
replacement_uuid: accepted_bundle.replacement_uuid,
dropping_tx_hashes: accepted_bundle.dropping_tx_hashes,
}
}
}

#[cfg(test)]
mod tests {
use alloy_primitives::U256;
use alloy_provider::network::eip2718::Encodable2718;
use alloy_signer_local::PrivateKeySigner;

use super::*;
use crate::{
Bundle,
test_utils::{create_test_meter_bundle_response, create_transaction},
};

#[test]
fn test_accepted_bundle_new() {
let alice = PrivateKeySigner::random();
let bob = PrivateKeySigner::random();

let tx = create_transaction(alice.clone(), 1, bob.address(), U256::from(10_000));
let tx_hash = tx.tx_hash();
let tx_bytes = tx.encoded_2718();

let bundle = Bundle { txs: vec![tx_bytes.into()], block_number: 100, ..Default::default() };

let parsed: ParsedBundle = bundle.try_into().unwrap();
let meter_response = create_test_meter_bundle_response();
let accepted = AcceptedBundle::new(parsed, meter_response);

assert!(!accepted.uuid().is_nil());
assert!(accepted.replacement_uuid.is_none());
assert_eq!(accepted.txs.len(), 1);
assert_eq!(accepted.txs[0].tx_hash(), tx_hash);
assert_eq!(accepted.block_number, 100);
}

#[test]
fn test_accepted_bundle_with_replacement_uuid() {
let alice = PrivateKeySigner::random();
let bob = PrivateKeySigner::random();

let tx = create_transaction(alice.clone(), 1, bob.address(), U256::from(10_000));
let tx_bytes = tx.encoded_2718();

let uuid = Uuid::new_v4();
let bundle = Bundle {
txs: vec![tx_bytes.into()],
block_number: 100,
replacement_uuid: Some(uuid.to_string()),
..Default::default()
};

let parsed: ParsedBundle = bundle.try_into().unwrap();
let meter_response = create_test_meter_bundle_response();
let accepted = AcceptedBundle::new(parsed, meter_response);

assert_eq!(*accepted.uuid(), uuid);
assert_eq!(accepted.replacement_uuid, Some(uuid));
}

#[test]
fn test_accepted_bundle_to_parsed_bundle() {
let alice = PrivateKeySigner::random();
let bob = PrivateKeySigner::random();

let tx = create_transaction(alice.clone(), 1, bob.address(), U256::from(10_000));
let tx_hash = tx.tx_hash();
let tx_bytes = tx.encoded_2718();

let bundle = Bundle {
txs: vec![tx_bytes.into()],
block_number: 100,
flashblock_number_min: Some(1),
flashblock_number_max: Some(5),
min_timestamp: Some(1000),
max_timestamp: Some(2000),
..Default::default()
};

let parsed: ParsedBundle = bundle.try_into().unwrap();
let meter_response = create_test_meter_bundle_response();
let accepted = AcceptedBundle::new(parsed, meter_response);

let back_to_parsed: ParsedBundle = accepted.into();
assert_eq!(back_to_parsed.txs.len(), 1);
assert_eq!(back_to_parsed.txs[0].tx_hash(), tx_hash);
assert_eq!(back_to_parsed.block_number, 100);
assert_eq!(back_to_parsed.flashblock_number_min, Some(1));
assert_eq!(back_to_parsed.flashblock_number_max, Some(5));
assert_eq!(back_to_parsed.min_timestamp, Some(1000));
assert_eq!(back_to_parsed.max_timestamp, Some(2000));
}
}
Loading