Skip to content

feat(deposits): Migration to EIP-6110 style processing#2794

Merged
calbera merged 68 commits into
mainfrom
tmp-6110-poc
May 5, 2026
Merged

feat(deposits): Migration to EIP-6110 style processing#2794
calbera merged 68 commits into
mainfrom
tmp-6110-poc

Conversation

@calbera

@calbera calbera commented May 28, 2025

Copy link
Copy Markdown
Contributor

EIP-6110 style deposits

3 requirements

  1. Given an 8192 limit of deposits per block, we need to guarantee that an EL doesn't include more than 8192 deposits in 1 block.

    • This is guaranteed with the current gas limit of 30 million.
    • 1 deposit on Berachain costs at least 45,000 gas (sample deposit)
    • 8192 deposits would cost 45,000 * 8192 = 368,640.000 gas
    • As long as gas per block stays under 368 million gas, we are good ✅
      • If we do increase gas, we can also increase the maximum amount of 8192 accordingly.
  2. Given the way deposits are taken from EL deposit contract events and then processed in CL, as is we will lose deposits from the 2 blocks right before the Fulu fork (which starts using 6110) is activated.

    • Currently the way the system works:
      • in finalize_block N, we store any new EL deposits from EL block N-1
      • in build_block N+1, we include these new EL deposits from EL block N-1
    • With 6110:
      • in build_block N+1, the EL will now include the deposits events that are in EL block N+1 itself
    • So if Fulu activates at block N+1 (previously for this block N+1 we would have included deposits from EL block N-1). Now with new fork, we will just include for block N+1 the deposits from EL block N+1.
      • We added a special case around the fork activation that retrieves the previous EL blocks' deposits to specifically include them ✅
        • in block N's finalizeBlock we retrieve N-1's deposits (in postFinalizeBlockOps which is the standard procedure before Fulu)
        • in block N+1's building or verifying procedures, we catchup and retrieve block N's deposits
        • in verifying block N+1, there is no deposit limit per block as to exhaust any outstanding deposits still in the queue. Note block N+1 will also include the 6110 deposit requests for N+1's deposits, which by itself follows the 8192 deposit limit.
  3. Forking on the EL to change the deposit event signature to Berachain's: 0x68af751683498a9f9be59fe8b0d52a64dd155255d85cdb29fea30b1e3f891d46 (sample deposit)

    • Change(s) in In reth here

TODOs:

  • Catchup deposits in 1 of process proposal / finalize block
  • write validation logic in processOperations for first block of Electra2
  • Electra2 fork test
  • Unit tests for deposit requests.
  • Simulation tests for deposit requests.

Remaining Testing

On a live devnet/testnet, ensure that timeouts do not occur when building first Fulu block as the build and verification procedures for this block now include an extra JSON RPC call to the EL. Look for PrepareProposal/ProcessProposal/FinalizeBlock timeouts.

@codecov

codecov Bot commented May 28, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.33333% with 69 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.21%. Comparing base (9ab5f95) to head (c361498).

Files with missing lines Patch % Lines
beacon/deposits/deposits.go 68.80% 25 Missing and 9 partials ⚠️
state-transition/core/state_processor_staking.go 73.33% 8 Missing and 4 partials ⚠️
state-transition/core/state_processor_forks.go 14.28% 4 Missing and 2 partials ⚠️
cli/commands/deposit/db_check.go 0.00% 5 Missing ⚠️
beacon/blockchain/finalize_block.go 66.66% 2 Missing and 1 partial ⚠️
beacon/blockchain/process_proposal.go 40.00% 2 Missing and 1 partial ⚠️
beacon/validator/block_builder.go 62.50% 1 Missing and 2 partials ⚠️
state-transition/core/state_processor.go 57.14% 2 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2794      +/-   ##
==========================================
+ Coverage   60.73%   61.21%   +0.48%     
==========================================
  Files         369      369              
  Lines       18847    18922      +75     
==========================================
+ Hits        11446    11584     +138     
+ Misses       6456     6380      -76     
- Partials      945      958      +13     
Files with missing lines Coverage Δ
beacon/blockchain/service.go 78.43% <100.00%> (-1.57%) ⬇️
beacon/validator/service.go 100.00% <100.00%> (ø)
config/spec/devnet.go 100.00% <100.00%> (ø)
config/spec/mainnet.go 100.00% <ø> (ø)
execution/client/ethclient/engine.go 64.93% <100.00%> (+18.83%) ⬆️
execution/deposit/contract.go 69.64% <100.00%> (+2.33%) ⬆️
node-core/components/validator_service.go 100.00% <100.00%> (ø)
primitives/version/versions.go 100.00% <ø> (ø)
state-transition/core/validation_deposits.go 56.57% <100.00%> (+6.57%) ⬆️
storage/deposit/v1/store.go 74.64% <100.00%> (ø)
... and 8 more

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@calbera calbera changed the title [do not merge] wip 6110 poc [do not merge] wip electra 1 fork for 6110 May 29, 2025
@calbera calbera changed the title [do not merge] wip electra 1 fork for 6110 [do not merge] wip electra1 fork for 6110 May 29, 2025
Comment thread state-transition/core/state_processor_staking.go Outdated
Comment thread state-transition/core/state_processor_forks.go
@rezzmah

rezzmah commented May 29, 2025

Copy link
Copy Markdown
Contributor

Supportive of this approach. Especially since its <200 line diff and gets rid of the CL querying the EL which isn't a great pattern.

Agreed that the Block Gas Limit is a good enough protection from hitting the deposit queue. It's unlikely we'll be x10'ing the gas limit arbitrarily in the coming months as that's not a bottleneck.

As you noted, the EL change is trivial.

There is a point to be noted that we deviate from the spec by removing the deposit queue altogether, which is present in the spec. In the spec, deposit processing is done per epoch, whereas we do it per block. This allows us to avoid any BeaconState changes. IMO the processing overhead of deposits is low enough that it doesn't need to be batched into an epoch (don't have empirical data to support this tho).

Unsure on the solution for the overlap period between old system and new. Perhaps it's just a custom state transition as part of upgradeToElectra1

@calbera

calbera commented Jun 2, 2025

Copy link
Copy Markdown
Contributor Author

There is a point to be noted that we deviate from the spec by removing the deposit queue altogether, which is present in the spec. In the spec, deposit processing is done per epoch, whereas we do it per block. This allows us to avoid any BeaconState changes. IMO the processing overhead of deposits is low enough that it doesn't need to be batched into an epoch (don't have empirical data to support this tho).

It does "deviate from spec" but not any more than we already are "deviated from the spec". Currently also deposits are processed block by block and the state transition operation for each is not too expensive (signatures are not verified on repeat -- most -- deposits). This change only affects the "lag" at which deposits are processed but not the frequency of processing itself.

Comment thread state-transition/core/state_processor_forks.go Outdated
Comment thread beacon/blockchain/service.go
Comment thread state-transition/core/state_processor_forks.go Outdated
Comment thread beacon/deposits/deposits.go
Comment thread beacon/deposits/deposits.go Outdated

@fridrik01 fridrik01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did a quick pass, overall looks good but will need to do another

blockToFetch := blockNum - eth1FollowDistance
deposits, err := depositContract.ReadDeposits(ctx, blockToFetch)
if err != nil {
logger.Error("Failed to read deposits", "block", blockNum, "error", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it safe to drop the error and only log, we had a retry mechanism before in depositCatchupFetcher?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this doesnt change any behavior. That retry mechanism was non-effectual, long overdue cleaning of tech debt

Comment thread execution/deposit/contract.go Outdated
Comment on lines +62 to +63
// If we already fetched deposits for this block, we don't need to do it again.
if depositContract.LastBlockNumber() == lph.GetNumber() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this to guard against calling this several times from different places if isFirstFuluBlock is true?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this guards only to prevent catchup. We will return from this function early if we are not in the first fulu block. If we are in first fulu block we need to run this fetch once amongst prepare/processProposal and finalizeBlock

@fridrik01

Copy link
Copy Markdown
Contributor

@calbera we should test this soon on devnet with load testing deposits

@fridrik01 fridrik01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly just nits

// LastBlockNumber returns the last block number that was successfully
// consumed from the deposit contract.
func (dc *WrappedDepositContract) LastBlockNumber() math.U64 {
return math.U64(dc.lastBlockNumber.Load())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@calbera something I noticed: lastBlockNumber zero-value doesn't distinguish "fetched at block 0" from "never fetched." Should be harmless in practice since IIUC genesis deposits bypass contract events, but wanted to call it out anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah thats true, but for the purpose of deposit contract, its functionally equivalent to say that we "never fetched" deposits if the last block fetched was 0 since 0 block will not have any deposits. We start at 1 onwards.

Comment thread primitives/version/versions.go
Comment thread chain/helpers_test.go

// If on the first block of Fulu, catchup the previous block's deposits. Between
// Prepare/ProcessProposal and FinalizeBlock, this only needs to be done once.
func CatchupFuluDeposits(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unit tests for these would be nice (though they should be covered in simulation/integration tests)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add unit tests for this file since most of the logic already existed in our processproposal / finalizeblock flows, so refactoring it to here should change no behavior, which can be enforced by syncing mainnet, and other e2e tests

calbera added a commit to berachain/bera-reth that referenced this pull request May 4, 2026
Replace the standard EIP-6110 DepositEvent signature with Berachain's
custom Deposit event: Deposit(bytes,bytes,uint64,bytes,uint64) →
0x68af751683498a9f9be59fe8b0d52a64dd155255d85cdb29fea30b1e3f891d46.

The Berachain event differs in two ways: the event name is Deposit (not
DepositEvent) and amount/index are uint64 rather than bytes, requiring
little-endian conversion when constructing deposit request bytestrings.

Enabled in the CL
[here](berachain/beacon-kit#2794)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* EIP-6110 deposit request handling is enabled for the Prague hardfork,
using the configured deposit contract and deposit event topic.
* Added support for a custom deposit event format and exposed deposit
parsing so deposit requests are included in block payloads.

* **Tests**
* Added end-to-end tests validating deposit request creation, payload
contents when active, and absence when no deposit transactions occur.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Cal Bera <calbera@berachain.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: bar-bera <bearbaresco@berachain.com>
@calbera calbera merged commit 1d04436 into main May 5, 2026
37 of 38 checks passed
@calbera calbera deleted the tmp-6110-poc branch May 5, 2026 04:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

EIP-6110: Supply validator deposits on chain

6 participants