## Final report
### Project Abstract
During my time in the Ethereum Protocol Fellowship, I worked on implementing EIP-7732 (Enshrined Proposer-Builder Separation) in Prysm and eventually in Nimbus. Currently validators heavily relies on third-party relays for block building, with about 90% of blocks being produced through mev-boost. While this system works, it introduces centralization risks and trust requirements that go against Ethereum's principles.
Our implementation aims to solve this by moving PBS into the protocol layer itself. The core idea is simple: make the exchange between proposers and builders trustless, while ensuring proposers get paid and builders' payloads become canonical when they act honestly.
You can find the original project proposal [here](https://github.com/ethereum/protocol-fellows/edit/main/projects/epbs.md).
### Status Report
When we started, we were working with a traditional block-auction design. The approach seemed straightforward - builders would bid for slots, proposers would select bids, and a Payload Timeliness Committee (PTC) would ensure everything happened on schedule. However, as we dug deeper into implementation, we discovered several challenges that led to significant design changes.
A major shift came with Francesco's proposal for an all-in-one fork choice rule. Instead of complex boost mechanisms, we moved to a simpler system using an Availability Committee (AC). Here's how the implementation evolved:
The initial design used a Payload Timeliness Committee (PTC) with complex boosting mechanisms. Here's how we implemented the PTC voting:
```golang
// Initial PTC implementation
func (node *Node) processPTCVote(vote PayloadAttestationMessage) error {
// Basic vote processing
for i := uint64(0); i < fieldparams.PTCSize; i++ {
if vote.AggregationBits.BitAt(i) {
node.ptcVote[i] = vote.PayloadStatus
}
}
return nil
}
```
This caused issues with equivocations since validators could vote multiple times. We fixed this in PR [#14308](https://github.com/prysmaticlabs/prysm/pull/14308):
```golang
// Updated implementation with equivocation protection
func (node *Node) UpdateVotesOnPayloadAttestation(vote PayloadAttestationMessage) error {
for i := uint64(0); i < fieldparams.PTCSize; i++ {
// Only set vote if not previously set
if !vote.AggregationBits.BitAt(i) {
continue
}
if node.ptcVote[i] == primitives.PTCStatus(0) {
node.ptcVote[i] = vote.PayloadStatus
}
}
return nil
}
```
#### Fork Choice Modifications
One of our biggest challenges was handling blocks that could exist in two states - with or without payload. Thanks to the new simpler fork-choice rule design by francesco, we implemented a dual-branch system.
This involved handling the dual-branch nature of blocks. Each beacon block can exist in two states - with or without its payload. Here's how we implemented this:
```golang
type BlockNode struct {
root Root
parent *BlockNode
children map[Root]*BlockNode
state BeaconState
// New fields for ePBS
withPayload bool
ptcVotes []Vote
}
```
This seemingly simple addition required careful consideration of fork choice rules, especially when dealing with attestations that could apply to both versions of a block.
I've written a more detailed explanation on All-in-one fork-choice design [here](https://hackmd.io/@kira50/HyFDBzozkl).
```golang
type ForkChoice struct {
store map[Root]*Node
// New fields for dual-branch handling
emptyBlocks map[Root]bool
fullBlocks map[Root]bool
}
func (f *ForkChoice) ProcessBlock(block *BeaconBlock) error {
empty := createEmptyBranch(block)
full := createFullBranch(block)
// Process based on AC votes
if hasACMajority(block) {
return f.processFull(full)
}
return f.processEmpty(empty)
}
```
#### Caching and Memory Optimization
We faced significant memory issues with PTC votes storage. Initially, we stored votes in each fork choice node:
```golang
type Node struct {
root Root
parent *Node
children map[Root]*Node
ptcVotes [PTCSize]Vote // Memory intensive
}
```
Potuz suggested a better approach using a separate cache:
```golang
type VoteCache struct {
sync.RWMutex
votes map[Root][]Vote
}
func (vc *VoteCache) Get(blockRoot Root) []Vote {
vc.RLock()
defer vc.RUnlock()
return vc.votes[blockRoot]
}
```
#### Withdrawal Handling
A particularly tricky challenge was handling withdrawals with empty blocks. The original implementation failed when processing withdrawals after empty blocks:
```golang
func get_expected_withdrawals(state BeaconState) error {
if !is_parent_full(state) {
return ErrIndeterminateWithdrawals
}
// Process withdrawals
}
```
We implemented two solutions:
1. Client-side caching:
```golang
type BeaconNode struct {
withdrawalCache map[Root][]Withdrawal
}
```
2. State-level tracking:
```golang
type BeaconState struct {
WithdrawalsCache []Withdrawal
LatestWithdrawalsRoot Root
}
```
#### Builder Security
We tackled builder security with a comprehensive verification system:
```golang
func verifyBuilderBid(bid *SignedBuilderBid) error {
// Check builder's stake
if bid.BuilderBalance < MinBuilderStake {
return ErrInsufficientStake
}
// Verify bid doesn't exceed balance
if bid.Value > bid.BuilderBalance {
return ErrBidTooHigh
}
// Additional safety checks
return verifySignature(bid)
}
```
Throughout the fellowship, I collaborated closely with other fellows and mentors. Terence's work on sync mechanisms (PR [#14363](https://github.com/prysmaticlabs/prysm/pull/14363)) complemented my PTC implementation. This collaborative environment led to better discussions and solutions, especially when dealing with challenging aspects like withdrawal handling and builder security.
### Future Work and Lessons Learned
While we've made significant progress, there's still work to be done. We need to implement client interoperability, with other clients like Nimbus, teku. The builder's blacklist feature remains to be implemented, and we're still optimizing memory consumption in fork choice.
Working with core developers at Devcon was particularly enlightening. Discussions with EF researchers helped me understand the broader implications of our work, especially how ePBS fits into Ethereum's scaling roadmap.
The EPF program provided an excellent environment for learning protocol development. The asynchronous communication with mentors, especially Potuz and Terence, helped navigate complex technical challenges. The ability to work directly on core protocol changes while receiving guidance from experienced developers was invaluable.
Moving forward, I plan to continue working on ePBS implementation. The experience gained during this fellowship has prepared me well for tackling complex protocol development challenges. I'm particularly interested in helping with the integration of FOCIL with our current implementation in future.
Looking back, this fellowship wasn't just about writing code - it was about understanding how protocol development works in practice, learning to collaborate with developers across different clients, and contributing to Ethereum's evolution. The challenges we faced and solved together have given me a deep appreciation for the complexity and importance of protocol-level improvements.
I'm grateful to the EPF program, my mentors, and fellow contributors for this opportunity. As we continue working towards shippinh ePBS, I look forward to remaining actively involved in Ethereum protocol development.