Skip to content

Commit

Permalink
chore: Adds ZenithEthBundle type to bundle module (#71)
Browse files Browse the repository at this point in the history
* adds SignetEthBundle type

This change copies over the SignetEthBundle type from signet-types.
This colocation allows other crates depending on this one to craft
bundles without depending on signet-node.

- copies SignetEthBundle type from signet-node into the bundle module
- exposes bundle module for external use

* adds more bundle types

- adds the SignetCallBundle types and all of the response types

* align around zenith naming instead of signet

* get alloy versions in agreement again

* adds the supporting methods for Signet bundle types

- adds the Signet bundle impls for Zenith
  • Loading branch information
dylanlott authored Jan 8, 2025
1 parent 7908238 commit 6f62b9d
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 4 deletions.
5 changes: 1 addition & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,14 @@ repository = "https://github.com/init4tech/zenith"
license = "AGPL-3.0"

[dependencies]
alloy = { version = "=0.7.3", features = ["full", "json-rpc", "signer-aws", "rpc-types-mev"] }
alloy-primitives = { version = "0.8.11", features = ["serde", "tiny-keccak"] }
alloy-sol-types = { version = "0.8.11", features = ["json"] }

alloy-rlp = { version = "0.3.4" }

alloy = { version = "=0.7.3", features = ["full", "json-rpc", "signer-aws"] }
alloy-contract = { version = "=0.7.3", features = ["pubsub"] }

serde = { version = "1.0.197", features = ["derive"] }

[dev-dependencies]
serde_json = "1.0.94"
tokio = { version = "1.37.0", features = ["macros"] }

278 changes: 278 additions & 0 deletions src/bundle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
use alloy::{
eips::{eip2718::Encodable2718, BlockNumberOrTag},
rpc::types::mev::{EthCallBundle, EthCallBundleResponse, EthSendBundle},
};
use alloy_primitives::{keccak256, Address, Bytes, B256, U256};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

use crate::SignedOrder;

/// Wraps a flashbots style EthSendBundle with host fills to make a Zenith compatible bundle
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ZenithEthBundle {
/// The bundle of transactions to simulate. Same structure as a Flashbots [EthSendBundle] bundle.
/// see <https://github.com/alloy-rs/alloy/blob/main/crates/rpc-types-mev/src/eth_calls.rs#L121-L139>
#[serde(flatten)]
pub bundle: EthSendBundle,
/// Host fills to be applied with the bundle, represented as a signed permit2 order.
pub host_fills: Option<SignedOrder>,
}

impl ZenithEthBundle {
/// Returns the transactions in this bundle.
pub fn txs(&self) -> &[Bytes] {
&self.bundle.txs
}

/// Returns the block number for this bundle.
pub const fn block_number(&self) -> u64 {
self.bundle.block_number
}

/// Returns the minimum timestamp for this bundle.
pub const fn min_timestamp(&self) -> Option<u64> {
self.bundle.min_timestamp
}

/// Returns the maximum timestamp for this bundle.
pub const fn max_timestamp(&self) -> Option<u64> {
self.bundle.max_timestamp
}

/// Returns the reverting tx hashes for this bundle.
pub fn reverting_tx_hashes(&self) -> &[B256] {
self.bundle.reverting_tx_hashes.as_slice()
}

/// Returns the replacement uuid for this bundle.
pub fn replacement_uuid(&self) -> Option<&str> {
self.bundle.replacement_uuid.as_deref()
}
}

/// Response for `zenith_sendBundle`
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ZenithEthBundleResponse {
/// The bundle hash of the sent bundle.
///
/// This is calculated as keccak256(tx_hashes) where tx_hashes are the concatenated transaction hashes.
pub bundle_hash: B256,
}

/// Bundle of transactions for `zenith_callBundle`
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ZenithCallBundle {
/// The bundle of transactions to simulate. Same structure as a Flashbots [EthCallBundle] bundle.
/// see <https://github.com/alloy-rs/alloy/blob/main/crates/rpc-types-mev/src/eth_calls.rs#L13-L33>
#[serde(flatten)]
pub bundle: EthCallBundle,
/// Host fills to be applied to the bundle for simulation. The mapping corresponds
/// to asset => user => amount.
pub host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
}

impl ZenithCallBundle {
/// Returns the host fills for this bundle.
pub const fn host_fills(&self) -> &BTreeMap<Address, BTreeMap<Address, U256>> {
&self.host_fills
}

/// Returns the transactions in this bundle.
pub fn txs(&self) -> &[Bytes] {
&self.bundle.txs
}

/// Returns the block number for this bundle.
pub const fn block_number(&self) -> u64 {
self.bundle.block_number
}

/// Returns the state block number for this bundle.
pub const fn state_block_number(&self) -> BlockNumberOrTag {
self.bundle.state_block_number
}

/// Returns the timestamp for this bundle.
pub const fn timestamp(&self) -> Option<u64> {
self.bundle.timestamp
}

/// Returns the gas limit for this bundle.
pub const fn gas_limit(&self) -> Option<u64> {
self.bundle.gas_limit
}

/// Returns the difficulty for this bundle.
pub const fn difficulty(&self) -> Option<U256> {
self.bundle.difficulty
}

/// Returns the base fee for this bundle.
pub const fn base_fee(&self) -> Option<u128> {
self.bundle.base_fee
}

/// Creates a new bundle from the given [`Encodable2718`] transactions.
pub fn from_2718_and_host_fills<I, T>(
txs: I,
host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
) -> Self
where
I: IntoIterator<Item = T>,
T: Encodable2718,
{
Self::from_raw_txs_and_host_fills(txs.into_iter().map(|tx| tx.encoded_2718()), host_fills)
}

/// Creates a new bundle with the given transactions and host fills.
pub fn from_raw_txs_and_host_fills<I, T>(
txs: I,
host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Bytes>,
{
Self {
bundle: EthCallBundle {
txs: txs.into_iter().map(Into::into).collect(),
..Default::default()
},
host_fills,
}
}

/// Adds an [`Encodable2718`] transaction to the bundle.
pub fn append_2718_tx(self, tx: impl Encodable2718) -> Self {
self.append_raw_tx(tx.encoded_2718())
}

/// Adds an EIP-2718 envelope to the bundle.
pub fn append_raw_tx(mut self, tx: impl Into<Bytes>) -> Self {
self.bundle.txs.push(tx.into());
self
}

/// Adds multiple [`Encodable2718`] transactions to the bundle.
pub fn extend_2718_txs<I, T>(self, tx: I) -> Self
where
I: IntoIterator<Item = T>,
T: Encodable2718,
{
self.extend_raw_txs(tx.into_iter().map(|tx| tx.encoded_2718()))
}

/// Adds multiple calls to the block.
pub fn extend_raw_txs<I, T>(mut self, txs: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Bytes>,
{
self.bundle.txs.extend(txs.into_iter().map(Into::into));
self
}

/// Sets the block number for the bundle.
pub const fn with_block_number(mut self, block_number: u64) -> Self {
self.bundle.block_number = block_number;
self
}

/// Sets the state block number for the bundle.
pub fn with_state_block_number(
mut self,
state_block_number: impl Into<BlockNumberOrTag>,
) -> Self {
self.bundle.state_block_number = state_block_number.into();
self
}

/// Sets the timestamp for the bundle.
pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
self.bundle.timestamp = Some(timestamp);
self
}

/// Sets the gas limit for the bundle.
pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
self.bundle.gas_limit = Some(gas_limit);
self
}

/// Sets the difficulty for the bundle.
pub const fn with_difficulty(mut self, difficulty: U256) -> Self {
self.bundle.difficulty = Some(difficulty);
self
}

/// Sets the base fee for the bundle.
pub const fn with_base_fee(mut self, base_fee: u128) -> Self {
self.bundle.base_fee = Some(base_fee);
self
}

/// Make a bundle hash from the given deserialized transaction array and host fills from this bundle.
/// The hash is calculated as keccak256(tx_preimage + host_preimage).
/// The tx_preimage is calculated as `keccak(tx_hash1 + tx_hash2 + ... + tx_hashn)`.
/// The host_preimage is calculated as
/// `keccak(NUM_OF_ASSETS_LE + asset1 + NUM_OF_FILLS_LE + asset1_user1 + user1_amount2 + ... + asset1_usern + asset1_amountn + ...)`.
/// For the number of users/fills and amounts in the host_preimage, the amounts are serialized as little-endian U256 slice.
pub fn bundle_hash(&self) -> B256 {
let mut hasher = alloy_primitives::Keccak256::new();

// Concatenate the transaction hashes, to then hash them. This is the tx_preimage.
for tx in self.bundle.txs.iter() {
// Calculate the tx hash (keccak256(encoded_signed_tx)) and append it to the tx_bytes.
hasher.update(keccak256(tx).as_slice());
}
let tx_preimage = hasher.finalize();

// Now, let's build the host_preimage. We do it in steps:
// 1. Prefix the number of assets, encoded as a little-endian U256 slice.
// 2. For each asset:
// 3. Concatenate the asset address.
// 4. Prefix the number of fills.
// 5. For each fill, concatenate the user and amount, the latter encoded as a little-endian U256 slice.
let mut hasher = alloy_primitives::Keccak256::new();

// Prefix the list of users with the number of assets.
hasher.update(U256::from(self.host_fills.len()).as_le_slice());

for (asset, fills) in self.host_fills.iter() {
// Concatenate the asset address.
hasher.update(asset.as_slice());

// Prefix the list of fills with the number of fills
hasher.update(U256::from(fills.len()).as_le_slice());

for (user, amount) in fills.iter() {
// Concatenate the user address and amount for each fill.
hasher.update(user.as_slice());
hasher.update(amount.as_le_slice());
}
}

// Hash the host pre-image.
let host_preimage = hasher.finalize();

let mut pre_image = alloy_primitives::Keccak256::new();
pre_image.update(tx_preimage.as_slice());
pre_image.update(host_preimage.as_slice());

// Hash both tx and host hashes to get the final bundle hash.
pre_image.finalize()
}
}

/// Response for `zenith_callBundle`
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ZenithCallBundleResponse {
/// The flattened "vanilla" response which comes from `eth_callBundle`
#[serde(flatten)]
pub response: EthCallBundleResponse,
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ pub use block::{decode_txns, encode_txns, Alloy2718Coder, Coder, ZenithBlock, Ze
mod orders;
pub use orders::{AggregateOrders, SignedOrder};

mod bundle;
pub use bundle::{
ZenithCallBundle, ZenithCallBundleResponse, ZenithEthBundle, ZenithEthBundleResponse,
};

mod req;
pub use req::SignRequest;

Expand Down

0 comments on commit 6f62b9d

Please sign in to comment.