Skip to content
This repository was archived by the owner on Mar 26, 2025. It is now read-only.

Commit 6f62b9d

Browse files
authored
chore: Adds ZenithEthBundle type to bundle module (#71)
* 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
1 parent 7908238 commit 6f62b9d

File tree

3 files changed

+284
-4
lines changed

3 files changed

+284
-4
lines changed

Cargo.toml

+1-4
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,14 @@ repository = "https://github.com/init4tech/zenith"
1111
license = "AGPL-3.0"
1212

1313
[dependencies]
14+
alloy = { version = "=0.7.3", features = ["full", "json-rpc", "signer-aws", "rpc-types-mev"] }
1415
alloy-primitives = { version = "0.8.11", features = ["serde", "tiny-keccak"] }
1516
alloy-sol-types = { version = "0.8.11", features = ["json"] }
16-
1717
alloy-rlp = { version = "0.3.4" }
18-
19-
alloy = { version = "=0.7.3", features = ["full", "json-rpc", "signer-aws"] }
2018
alloy-contract = { version = "=0.7.3", features = ["pubsub"] }
2119

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

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

src/bundle.rs

+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
use alloy::{
2+
eips::{eip2718::Encodable2718, BlockNumberOrTag},
3+
rpc::types::mev::{EthCallBundle, EthCallBundleResponse, EthSendBundle},
4+
};
5+
use alloy_primitives::{keccak256, Address, Bytes, B256, U256};
6+
use serde::{Deserialize, Serialize};
7+
use std::collections::BTreeMap;
8+
9+
use crate::SignedOrder;
10+
11+
/// Wraps a flashbots style EthSendBundle with host fills to make a Zenith compatible bundle
12+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13+
#[serde(rename_all = "camelCase")]
14+
pub struct ZenithEthBundle {
15+
/// The bundle of transactions to simulate. Same structure as a Flashbots [EthSendBundle] bundle.
16+
/// see <https://github.com/alloy-rs/alloy/blob/main/crates/rpc-types-mev/src/eth_calls.rs#L121-L139>
17+
#[serde(flatten)]
18+
pub bundle: EthSendBundle,
19+
/// Host fills to be applied with the bundle, represented as a signed permit2 order.
20+
pub host_fills: Option<SignedOrder>,
21+
}
22+
23+
impl ZenithEthBundle {
24+
/// Returns the transactions in this bundle.
25+
pub fn txs(&self) -> &[Bytes] {
26+
&self.bundle.txs
27+
}
28+
29+
/// Returns the block number for this bundle.
30+
pub const fn block_number(&self) -> u64 {
31+
self.bundle.block_number
32+
}
33+
34+
/// Returns the minimum timestamp for this bundle.
35+
pub const fn min_timestamp(&self) -> Option<u64> {
36+
self.bundle.min_timestamp
37+
}
38+
39+
/// Returns the maximum timestamp for this bundle.
40+
pub const fn max_timestamp(&self) -> Option<u64> {
41+
self.bundle.max_timestamp
42+
}
43+
44+
/// Returns the reverting tx hashes for this bundle.
45+
pub fn reverting_tx_hashes(&self) -> &[B256] {
46+
self.bundle.reverting_tx_hashes.as_slice()
47+
}
48+
49+
/// Returns the replacement uuid for this bundle.
50+
pub fn replacement_uuid(&self) -> Option<&str> {
51+
self.bundle.replacement_uuid.as_deref()
52+
}
53+
}
54+
55+
/// Response for `zenith_sendBundle`
56+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57+
#[serde(rename_all = "camelCase")]
58+
pub struct ZenithEthBundleResponse {
59+
/// The bundle hash of the sent bundle.
60+
///
61+
/// This is calculated as keccak256(tx_hashes) where tx_hashes are the concatenated transaction hashes.
62+
pub bundle_hash: B256,
63+
}
64+
65+
/// Bundle of transactions for `zenith_callBundle`
66+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67+
#[serde(rename_all = "camelCase")]
68+
pub struct ZenithCallBundle {
69+
/// The bundle of transactions to simulate. Same structure as a Flashbots [EthCallBundle] bundle.
70+
/// see <https://github.com/alloy-rs/alloy/blob/main/crates/rpc-types-mev/src/eth_calls.rs#L13-L33>
71+
#[serde(flatten)]
72+
pub bundle: EthCallBundle,
73+
/// Host fills to be applied to the bundle for simulation. The mapping corresponds
74+
/// to asset => user => amount.
75+
pub host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
76+
}
77+
78+
impl ZenithCallBundle {
79+
/// Returns the host fills for this bundle.
80+
pub const fn host_fills(&self) -> &BTreeMap<Address, BTreeMap<Address, U256>> {
81+
&self.host_fills
82+
}
83+
84+
/// Returns the transactions in this bundle.
85+
pub fn txs(&self) -> &[Bytes] {
86+
&self.bundle.txs
87+
}
88+
89+
/// Returns the block number for this bundle.
90+
pub const fn block_number(&self) -> u64 {
91+
self.bundle.block_number
92+
}
93+
94+
/// Returns the state block number for this bundle.
95+
pub const fn state_block_number(&self) -> BlockNumberOrTag {
96+
self.bundle.state_block_number
97+
}
98+
99+
/// Returns the timestamp for this bundle.
100+
pub const fn timestamp(&self) -> Option<u64> {
101+
self.bundle.timestamp
102+
}
103+
104+
/// Returns the gas limit for this bundle.
105+
pub const fn gas_limit(&self) -> Option<u64> {
106+
self.bundle.gas_limit
107+
}
108+
109+
/// Returns the difficulty for this bundle.
110+
pub const fn difficulty(&self) -> Option<U256> {
111+
self.bundle.difficulty
112+
}
113+
114+
/// Returns the base fee for this bundle.
115+
pub const fn base_fee(&self) -> Option<u128> {
116+
self.bundle.base_fee
117+
}
118+
119+
/// Creates a new bundle from the given [`Encodable2718`] transactions.
120+
pub fn from_2718_and_host_fills<I, T>(
121+
txs: I,
122+
host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
123+
) -> Self
124+
where
125+
I: IntoIterator<Item = T>,
126+
T: Encodable2718,
127+
{
128+
Self::from_raw_txs_and_host_fills(txs.into_iter().map(|tx| tx.encoded_2718()), host_fills)
129+
}
130+
131+
/// Creates a new bundle with the given transactions and host fills.
132+
pub fn from_raw_txs_and_host_fills<I, T>(
133+
txs: I,
134+
host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
135+
) -> Self
136+
where
137+
I: IntoIterator<Item = T>,
138+
T: Into<Bytes>,
139+
{
140+
Self {
141+
bundle: EthCallBundle {
142+
txs: txs.into_iter().map(Into::into).collect(),
143+
..Default::default()
144+
},
145+
host_fills,
146+
}
147+
}
148+
149+
/// Adds an [`Encodable2718`] transaction to the bundle.
150+
pub fn append_2718_tx(self, tx: impl Encodable2718) -> Self {
151+
self.append_raw_tx(tx.encoded_2718())
152+
}
153+
154+
/// Adds an EIP-2718 envelope to the bundle.
155+
pub fn append_raw_tx(mut self, tx: impl Into<Bytes>) -> Self {
156+
self.bundle.txs.push(tx.into());
157+
self
158+
}
159+
160+
/// Adds multiple [`Encodable2718`] transactions to the bundle.
161+
pub fn extend_2718_txs<I, T>(self, tx: I) -> Self
162+
where
163+
I: IntoIterator<Item = T>,
164+
T: Encodable2718,
165+
{
166+
self.extend_raw_txs(tx.into_iter().map(|tx| tx.encoded_2718()))
167+
}
168+
169+
/// Adds multiple calls to the block.
170+
pub fn extend_raw_txs<I, T>(mut self, txs: I) -> Self
171+
where
172+
I: IntoIterator<Item = T>,
173+
T: Into<Bytes>,
174+
{
175+
self.bundle.txs.extend(txs.into_iter().map(Into::into));
176+
self
177+
}
178+
179+
/// Sets the block number for the bundle.
180+
pub const fn with_block_number(mut self, block_number: u64) -> Self {
181+
self.bundle.block_number = block_number;
182+
self
183+
}
184+
185+
/// Sets the state block number for the bundle.
186+
pub fn with_state_block_number(
187+
mut self,
188+
state_block_number: impl Into<BlockNumberOrTag>,
189+
) -> Self {
190+
self.bundle.state_block_number = state_block_number.into();
191+
self
192+
}
193+
194+
/// Sets the timestamp for the bundle.
195+
pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
196+
self.bundle.timestamp = Some(timestamp);
197+
self
198+
}
199+
200+
/// Sets the gas limit for the bundle.
201+
pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
202+
self.bundle.gas_limit = Some(gas_limit);
203+
self
204+
}
205+
206+
/// Sets the difficulty for the bundle.
207+
pub const fn with_difficulty(mut self, difficulty: U256) -> Self {
208+
self.bundle.difficulty = Some(difficulty);
209+
self
210+
}
211+
212+
/// Sets the base fee for the bundle.
213+
pub const fn with_base_fee(mut self, base_fee: u128) -> Self {
214+
self.bundle.base_fee = Some(base_fee);
215+
self
216+
}
217+
218+
/// Make a bundle hash from the given deserialized transaction array and host fills from this bundle.
219+
/// The hash is calculated as keccak256(tx_preimage + host_preimage).
220+
/// The tx_preimage is calculated as `keccak(tx_hash1 + tx_hash2 + ... + tx_hashn)`.
221+
/// The host_preimage is calculated as
222+
/// `keccak(NUM_OF_ASSETS_LE + asset1 + NUM_OF_FILLS_LE + asset1_user1 + user1_amount2 + ... + asset1_usern + asset1_amountn + ...)`.
223+
/// For the number of users/fills and amounts in the host_preimage, the amounts are serialized as little-endian U256 slice.
224+
pub fn bundle_hash(&self) -> B256 {
225+
let mut hasher = alloy_primitives::Keccak256::new();
226+
227+
// Concatenate the transaction hashes, to then hash them. This is the tx_preimage.
228+
for tx in self.bundle.txs.iter() {
229+
// Calculate the tx hash (keccak256(encoded_signed_tx)) and append it to the tx_bytes.
230+
hasher.update(keccak256(tx).as_slice());
231+
}
232+
let tx_preimage = hasher.finalize();
233+
234+
// Now, let's build the host_preimage. We do it in steps:
235+
// 1. Prefix the number of assets, encoded as a little-endian U256 slice.
236+
// 2. For each asset:
237+
// 3. Concatenate the asset address.
238+
// 4. Prefix the number of fills.
239+
// 5. For each fill, concatenate the user and amount, the latter encoded as a little-endian U256 slice.
240+
let mut hasher = alloy_primitives::Keccak256::new();
241+
242+
// Prefix the list of users with the number of assets.
243+
hasher.update(U256::from(self.host_fills.len()).as_le_slice());
244+
245+
for (asset, fills) in self.host_fills.iter() {
246+
// Concatenate the asset address.
247+
hasher.update(asset.as_slice());
248+
249+
// Prefix the list of fills with the number of fills
250+
hasher.update(U256::from(fills.len()).as_le_slice());
251+
252+
for (user, amount) in fills.iter() {
253+
// Concatenate the user address and amount for each fill.
254+
hasher.update(user.as_slice());
255+
hasher.update(amount.as_le_slice());
256+
}
257+
}
258+
259+
// Hash the host pre-image.
260+
let host_preimage = hasher.finalize();
261+
262+
let mut pre_image = alloy_primitives::Keccak256::new();
263+
pre_image.update(tx_preimage.as_slice());
264+
pre_image.update(host_preimage.as_slice());
265+
266+
// Hash both tx and host hashes to get the final bundle hash.
267+
pre_image.finalize()
268+
}
269+
}
270+
271+
/// Response for `zenith_callBundle`
272+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
273+
#[serde(rename_all = "camelCase")]
274+
pub struct ZenithCallBundleResponse {
275+
/// The flattened "vanilla" response which comes from `eth_callBundle`
276+
#[serde(flatten)]
277+
pub response: EthCallBundleResponse,
278+
}

src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ pub use block::{decode_txns, encode_txns, Alloy2718Coder, Coder, ZenithBlock, Ze
2222
mod orders;
2323
pub use orders::{AggregateOrders, SignedOrder};
2424

25+
mod bundle;
26+
pub use bundle::{
27+
ZenithCallBundle, ZenithCallBundleResponse, ZenithEthBundle, ZenithEthBundleResponse,
28+
};
29+
2530
mod req;
2631
pub use req::SignRequest;
2732

0 commit comments

Comments
 (0)