Skip to content

Commit e18fee5

Browse files
authored
Merge pull request #3 from EIPs-CodeLab/feat/builder-registry-withdrawals
feat: implement builder registry and withdrawal flows
2 parents 5868737 + c2fc28d commit e18fee5

3 files changed

Lines changed: 240 additions & 0 deletions

File tree

src/beacon_chain/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ pub mod constants;
22
pub mod containers;
33
pub mod process_payload_attestation;
44
pub mod process_payload_bid;
5+
pub mod registry;
56
pub mod types;
67
pub mod withdrawals;

src/beacon_chain/registry.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! Builder registry and withdrawal flows (consensus-specs inspired).
2+
//!
3+
//! This module keeps the state surface minimal while enforcing:
4+
//! - bounded registry size (`BUILDER_REGISTRY_LIMIT`)
5+
//! - bounded pending withdrawals (`BUILDER_PENDING_WITHDRAWALS_LIMIT`)
6+
//! - withdrawability gating by epoch (`withdrawable_epoch`)
7+
//!
8+
//! It does not implement deposits; tests inject builders directly via the
9+
//! state trait to keep the scope small.
10+
11+
use crate::beacon_chain::{
12+
constants::{
13+
BUILDER_PENDING_WITHDRAWALS_LIMIT, BUILDER_REGISTRY_LIMIT,
14+
MAX_BUILDERS_PER_WITHDRAWALS_SWEEP,
15+
},
16+
containers::{Builder, BuilderPendingWithdrawal},
17+
types::{BuilderIndex, Epoch, Gwei},
18+
};
19+
use thiserror::Error;
20+
21+
#[derive(Debug, Error)]
22+
pub enum RegistryError {
23+
#[error("builder registry full (limit {0})")]
24+
RegistryFull(u64),
25+
26+
#[error("builder {0} already exists")]
27+
BuilderExists(BuilderIndex),
28+
29+
#[error("unknown builder {0}")]
30+
UnknownBuilder(BuilderIndex),
31+
32+
#[error("builder {index} not withdrawable until epoch {required}, current {current}")]
33+
NotWithdrawable {
34+
index: BuilderIndex,
35+
required: Epoch,
36+
current: Epoch,
37+
},
38+
39+
#[error("builder {index} balance {balance} insufficient for withdrawal {amount}")]
40+
InsufficientBalance {
41+
index: BuilderIndex,
42+
balance: Gwei,
43+
amount: Gwei,
44+
},
45+
46+
#[error("pending withdrawals queue full (limit {0})")]
47+
PendingQueueFull(u64),
48+
}
49+
50+
pub trait RegistryState {
51+
fn builder_count(&self) -> u64;
52+
fn get_builder(&self, index: BuilderIndex) -> Option<Builder>;
53+
fn insert_builder(&mut self, index: BuilderIndex, builder: Builder);
54+
fn debit_builder_balance(&mut self, index: BuilderIndex, amount: Gwei);
55+
56+
fn push_pending_withdrawal(&mut self, w: BuilderPendingWithdrawal);
57+
fn pending_withdrawals_len(&self) -> u64;
58+
fn pop_pending_withdrawals(&mut self, max: u64) -> Vec<BuilderPendingWithdrawal>;
59+
}
60+
61+
/// Register a builder if space permits.
62+
pub fn register_builder<S: RegistryState>(
63+
state: &mut S,
64+
index: BuilderIndex,
65+
builder: Builder,
66+
) -> Result<(), RegistryError> {
67+
if state.builder_count() >= BUILDER_REGISTRY_LIMIT {
68+
return Err(RegistryError::RegistryFull(BUILDER_REGISTRY_LIMIT));
69+
}
70+
if state.get_builder(index).is_some() {
71+
return Err(RegistryError::BuilderExists(index));
72+
}
73+
state.insert_builder(index, builder);
74+
Ok(())
75+
}
76+
77+
/// Request a withdrawal of builder balance into the execution layer.
78+
pub fn request_builder_withdrawal<S: RegistryState>(
79+
state: &mut S,
80+
index: BuilderIndex,
81+
amount: Gwei,
82+
current_epoch: Epoch,
83+
) -> Result<BuilderPendingWithdrawal, RegistryError> {
84+
let builder = state
85+
.get_builder(index)
86+
.ok_or(RegistryError::UnknownBuilder(index))?;
87+
88+
if builder.withdrawable_epoch > current_epoch {
89+
return Err(RegistryError::NotWithdrawable {
90+
index,
91+
required: builder.withdrawable_epoch,
92+
current: current_epoch,
93+
});
94+
}
95+
if builder.balance < amount {
96+
return Err(RegistryError::InsufficientBalance {
97+
index,
98+
balance: builder.balance,
99+
amount,
100+
});
101+
}
102+
if state.pending_withdrawals_len() >= BUILDER_PENDING_WITHDRAWALS_LIMIT {
103+
return Err(RegistryError::PendingQueueFull(
104+
BUILDER_PENDING_WITHDRAWALS_LIMIT,
105+
));
106+
}
107+
108+
let withdrawal = BuilderPendingWithdrawal {
109+
fee_recipient: builder.execution_address,
110+
amount,
111+
builder_index: index,
112+
};
113+
114+
state.debit_builder_balance(index, amount);
115+
state.push_pending_withdrawal(withdrawal.clone());
116+
Ok(withdrawal)
117+
}
118+
119+
/// Pop up to `MAX_BUILDERS_PER_WITHDRAWALS_SWEEP` pending withdrawals for inclusion.
120+
pub fn sweep_pending_withdrawals<S: RegistryState>(state: &mut S) -> Vec<BuilderPendingWithdrawal> {
121+
state.pop_pending_withdrawals(MAX_BUILDERS_PER_WITHDRAWALS_SWEEP)
122+
}

tests/unit/registry_test.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use eip_7732::beacon_chain::{
2+
constants::{
3+
BUILDER_PENDING_WITHDRAWALS_LIMIT, BUILDER_REGISTRY_LIMIT,
4+
MAX_BUILDERS_PER_WITHDRAWALS_SWEEP,
5+
},
6+
containers::{Builder, BuilderPendingWithdrawal},
7+
registry::{register_builder, request_builder_withdrawal, sweep_pending_withdrawals, RegistryError, RegistryState},
8+
types::*,
9+
};
10+
use std::collections::{HashMap, VecDeque};
11+
12+
struct MockRegistry {
13+
builders: HashMap<BuilderIndex, Builder>,
14+
pending: VecDeque<BuilderPendingWithdrawal>,
15+
pending_len_override: Option<u64>,
16+
}
17+
18+
impl MockRegistry {
19+
fn new() -> Self {
20+
Self { builders: HashMap::new(), pending: VecDeque::new(), pending_len_override: None }
21+
}
22+
fn builder(index: BuilderIndex, balance: Gwei, withdrawable_epoch: Epoch) -> Builder {
23+
Builder {
24+
pubkey: [0u8; 48],
25+
version: 0,
26+
execution_address: [0u8; 20],
27+
balance,
28+
deposit_epoch: 0,
29+
withdrawable_epoch,
30+
}
31+
}
32+
}
33+
34+
impl RegistryState for MockRegistry {
35+
fn builder_count(&self) -> u64 { self.builders.len() as u64 }
36+
fn get_builder(&self, index: BuilderIndex) -> Option<Builder> {
37+
self.builders.get(&index).cloned()
38+
}
39+
fn insert_builder(&mut self, index: BuilderIndex, builder: Builder) {
40+
self.builders.insert(index, builder);
41+
}
42+
fn debit_builder_balance(&mut self, index: BuilderIndex, amount: Gwei) {
43+
if let Some(b) = self.builders.get_mut(&index) {
44+
b.balance -= amount;
45+
}
46+
}
47+
fn push_pending_withdrawal(&mut self, w: BuilderPendingWithdrawal) {
48+
self.pending.push_back(w);
49+
}
50+
fn pending_withdrawals_len(&self) -> u64 {
51+
self.pending_len_override.unwrap_or(self.pending.len() as u64)
52+
}
53+
fn pop_pending_withdrawals(&mut self, max: u64) -> Vec<BuilderPendingWithdrawal> {
54+
let mut out = Vec::new();
55+
for _ in 0..max {
56+
if let Some(w) = self.pending.pop_front() {
57+
out.push(w);
58+
} else {
59+
break;
60+
}
61+
}
62+
out
63+
}
64+
}
65+
66+
#[test]
67+
fn register_builder_succeeds() {
68+
let mut reg = MockRegistry::new();
69+
let b = MockRegistry::builder(1, 10_000, 5);
70+
assert!(register_builder(&mut reg, 1, b).is_ok());
71+
assert_eq!(reg.builder_count(), 1);
72+
}
73+
74+
#[test]
75+
fn register_builder_duplicate_rejected() {
76+
let mut reg = MockRegistry::new();
77+
let b = MockRegistry::builder(1, 10_000, 5);
78+
register_builder(&mut reg, 1, b.clone()).unwrap();
79+
let err = register_builder(&mut reg, 1, b).unwrap_err();
80+
assert!(matches!(err, RegistryError::BuilderExists(1)));
81+
}
82+
83+
#[test]
84+
fn withdrawal_not_withdrawable_yet() {
85+
let mut reg = MockRegistry::new();
86+
register_builder(&mut reg, 1, MockRegistry::builder(1, 10_000, 10)).unwrap();
87+
let err = request_builder_withdrawal(&mut reg, 1, 1_000, 5).unwrap_err();
88+
assert!(matches!(err, RegistryError::NotWithdrawable { .. }));
89+
}
90+
91+
#[test]
92+
fn withdrawal_insufficient_balance() {
93+
let mut reg = MockRegistry::new();
94+
register_builder(&mut reg, 1, MockRegistry::builder(1, 500, 0)).unwrap();
95+
let err = request_builder_withdrawal(&mut reg, 1, 1_000, 0).unwrap_err();
96+
assert!(matches!(err, RegistryError::InsufficientBalance { .. }));
97+
}
98+
99+
#[test]
100+
fn pending_queue_limit_enforced() {
101+
let mut reg = MockRegistry::new();
102+
register_builder(&mut reg, 1, MockRegistry::builder(1, 10_000, 0)).unwrap();
103+
reg.pending_len_override = Some(BUILDER_PENDING_WITHDRAWALS_LIMIT);
104+
let err = request_builder_withdrawal(&mut reg, 1, 1, 0).unwrap_err();
105+
assert!(matches!(err, RegistryError::PendingQueueFull(_)));
106+
}
107+
108+
#[test]
109+
fn sweep_respects_max_builders_per_sweep() {
110+
let mut reg = MockRegistry::new();
111+
register_builder(&mut reg, 1, MockRegistry::builder(1, 10_000, 0)).unwrap();
112+
for _ in 0..(MAX_BUILDERS_PER_WITHDRAWALS_SWEEP + 2) {
113+
request_builder_withdrawal(&mut reg, 1, 1, 0).unwrap();
114+
}
115+
let swept = sweep_pending_withdrawals(&mut reg);
116+
assert_eq!(swept.len() as u64, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP);
117+
}

0 commit comments

Comments
 (0)