## 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.