Skip to content

Commit 05824e6

Browse files
authored
feat: add support for ssz root calculations (#731)
## 📝 Summary Add support for calculation ssz transactions and withdrawals roots. ## ✅ I have completed the following steps: * [x] Run `make lint` * [x] Run `make test` * [ ] Added tests (if applicable)
1 parent 7c4414c commit 05824e6

File tree

4 files changed

+212
-2
lines changed

4 files changed

+212
-2
lines changed

Cargo.lock

Lines changed: 70 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rbuilder/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ alloy-trie.workspace = true
6060

6161
ethereum_ssz_derive.workspace = true
6262
ethereum_ssz.workspace = true
63+
ssz_types = "0.8.0"
64+
tree_hash = "0.8.0"
65+
tree_hash_derive = "0.8.0"
66+
typenum = "1.17.0"
6367

6468
test_utils = { path = "src/test_utils" }
6569
metrics_macros = { path = "src/telemetry/metrics_macros" }

crates/rbuilder/src/mev_boost/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod error;
33
pub mod fake_mev_boost_relay;
44
pub mod rpc;
55
pub mod sign_payload;
6+
pub mod ssz_roots;
67

78
use crate::mev_boost::bloxroute_grpc::GrpcRelayClient;
89
use rbuilder_primitives::mev_boost::{
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//! SSZ utilities.
2+
3+
use alloy_primitives::{Address, Bytes, B256};
4+
use sha2::{Digest, Sha256};
5+
use ssz_types::{FixedVector, VariableList};
6+
use tree_hash::TreeHash as _;
7+
8+
#[derive(tree_hash_derive::TreeHash)]
9+
struct TreeHashAddress {
10+
inner: FixedVector<u8, typenum::U20>,
11+
}
12+
13+
impl From<Address> for TreeHashAddress {
14+
fn from(address: Address) -> Self {
15+
Self {
16+
inner: FixedVector::from(address.to_vec()),
17+
}
18+
}
19+
}
20+
21+
#[derive(tree_hash_derive::TreeHash)]
22+
struct Withdrawal {
23+
pub index: u64,
24+
pub validator_index: u64,
25+
pub address: TreeHashAddress,
26+
pub amount: u64,
27+
}
28+
29+
type MaxWithdrawalsPerPayload = typenum::U16;
30+
31+
/// Calculate SSZ root for withdrawals.
32+
pub fn calculate_withdrawals_root_ssz(withdrawals: &[alloy_eips::eip4895::Withdrawal]) -> B256 {
33+
let withdrawals: VariableList<Withdrawal, MaxWithdrawalsPerPayload> = VariableList::from(
34+
withdrawals
35+
.iter()
36+
.map(|w| Withdrawal {
37+
index: w.index,
38+
validator_index: w.validator_index,
39+
address: TreeHashAddress::from(w.address),
40+
amount: w.amount,
41+
})
42+
.collect::<Vec<_>>(),
43+
);
44+
B256::from_slice(&withdrawals.tree_hash_root()[..])
45+
}
46+
47+
type MaxBytesPerTransaction = typenum::U1073741824;
48+
type MaxTransactionsPerPayload = typenum::U1048576;
49+
type BinaryTransaction = VariableList<u8, MaxBytesPerTransaction>;
50+
51+
/// Calculate SSZ root for transactions.
52+
pub fn calculate_transactions_root_ssz(transactions: &[Bytes]) -> B256 {
53+
let transactions: VariableList<BinaryTransaction, MaxTransactionsPerPayload> =
54+
VariableList::from(
55+
transactions
56+
.iter()
57+
.map(|bytes| BinaryTransaction::from(bytes.to_vec()))
58+
.collect::<Vec<_>>(),
59+
);
60+
B256::from_slice(&transactions.tree_hash_root()[..])
61+
}
62+
63+
const TREE_DEPTH: usize = 20; // log₂(MAX_TRANSACTIONS_PER_PAYLOAD)
64+
65+
const MAX_CHUNK_COUNT: usize = 1 << TREE_DEPTH;
66+
67+
/// Generate SSZ proof for target transaction.
68+
pub fn generate_transaction_proof_ssz(transactions: &[Bytes], target: usize) -> Vec<B256> {
69+
generate_transaction_proof_ssz_with_buffers(
70+
transactions,
71+
target,
72+
&mut Vec::new(),
73+
&mut Vec::new(),
74+
)
75+
}
76+
77+
/// Generate SSZ proof for target transaction with reusable buffer.
78+
pub fn generate_transaction_proof_ssz_with_buffers(
79+
transactions: &[Bytes],
80+
target: usize,
81+
current_buf: &mut Vec<B256>,
82+
next_buf: &mut Vec<B256>,
83+
) -> Vec<B256> {
84+
// Compute all leaf hashes and fill remaining slots with 0 hashes.
85+
// SSZ always pads to the maximum possible size defined by the type
86+
current_buf.clear();
87+
for idx in 0..MAX_CHUNK_COUNT {
88+
let leaf = transactions
89+
.get(idx)
90+
.map(ssz_leaf_root)
91+
.unwrap_or(B256::ZERO);
92+
current_buf.insert(idx, leaf);
93+
}
94+
95+
// Build the merkle tree bottom-up and collect the proof
96+
let mut branch = Vec::new();
97+
let (current_level, next_level) = (current_buf, next_buf);
98+
let mut current_index = target;
99+
100+
// Build the complete tree to depth TREE_DEPTH (20 levels)
101+
for _level in 0..TREE_DEPTH {
102+
// Get the sibling at this level
103+
let sibling_index = current_index ^ 1;
104+
branch.push(current_level[sibling_index]);
105+
106+
// Build next level up
107+
next_level.clear();
108+
for i in (0..current_level.len()).step_by(2) {
109+
let left = current_level[i];
110+
let right = current_level[i + 1];
111+
next_level.push(sha_pair(&left, &right));
112+
}
113+
114+
std::mem::swap(current_level, next_level);
115+
current_index /= 2;
116+
117+
// Stop when we reach the root
118+
if current_level.len() == 1 {
119+
break;
120+
}
121+
}
122+
123+
branch
124+
}
125+
126+
#[inline]
127+
fn ssz_leaf_root(data: &Bytes) -> B256 {
128+
B256::from_slice(&BinaryTransaction::from(data.to_vec()).tree_hash_root()[..])
129+
}
130+
131+
#[inline]
132+
fn sha_pair(a: &B256, b: &B256) -> B256 {
133+
let mut h = Sha256::new();
134+
h.update(a);
135+
h.update(b);
136+
B256::from_slice(&h.finalize())
137+
}

0 commit comments

Comments
 (0)