EIP-6110: Supply validator deposits on chain

hey @etan-status, attempting to address some of the concerns you have shared recently:

  • In the attack scenario that you have described 1 ETH is not the cost, but there is an opportunity cost of the funds being locked on CL until they are withdrawn. With 1_000_000 validators it is about 9 days, with 512_000 validators (assuming after consolidation unlocked by MAXEB we have this number) it is 4.5 days. So to effectively burn deposit contract slots one will have to get a decent amount of spare ETH. IMO, it makes such attack non-viable.
  • The order of deposit processing is not enforced, and during transition period more recent deposits will likely be processed before older ones (older than those that are supposed to be included by eth1 data poll). We should reach out to pools to get more feedback on it.
  • MAXEB makes every top-up to an active validator to pass through the activation churn which does eliminate the impact of 6110 on the WS computation.
  • index field is basically required for the transition period.
  • Having deposits can be helpful in a situation when EL is serving block bodies to CL. Otherwise, it would require EL to parse them from receipts.

I have a follow-up questions:

  • Do you suggest to pass deposit_count alongside to each deposit? or to a bunch of deposits?

there is an opportunity cost of the funds being locked on CL until they are withdrawn.

An attacker could just obtain validators with indices spread out in 100k index increments, and do the deposit spam to the one that gets the withdrawal sweep next. That could reduce the lockup delay to <1 day. Or, only spam during the last day before the sweep hits them. Current mainnet could be filled up in ~3 years, it’s a lot but not impossible. We’d also see it and could set up a second deposit contract to take over once the first one is full. Agree that this griefing attack would probably not be too interesting, but I’d just like to see a computation on the actual number here – is it still around a year to fill up, or are we reaching months territory?

  • The order of deposit processing is not enforced
  • index field is basically required for the transition period.

These two are related, right? If it turns out to be a problem for decentralized staking pools that rely on the withdrawal credentials being locked by the deposit with the lowest index, the design may have to change to something that processes deposits in order. In that case, index field may return to being not required, right?

Or, would it still be required as EIP-7685 documents “Within the same type, order is not defined”? For my thoughts on EIP-7685, please also refer to EIP-7251: Increase the MAX_EFFECTIVE_BALANCE - #8 by etan-status and EIP-7685 General purpose execution layer requests - #11 by etan-status – Without EIP-7685, could just replicate the SSZ structures, as in, just put the DepositReceipt as is into both the EL block header and CL ExecutionPayloadHeader and the problems should be gone. We can discuss at next ACDE: Execution Layer Meeting 187 · Issue #1029 · ethereum/pm · GitHub

  • Do you suggest to pass deposit_count alongside to each deposit? or to a bunch of deposits?

It’s a detail and doesn’t matter deeply. For smart contract verifiers, having an index per deposit is probably easier to check. Withdrawal currently have a per-withdrawal index on chain as well. Overall, I’d recommend the approach most consistent with existing design, as in, replicate the consensus data structure as closely as possible.

I am curious where this number is coming from? If we step away from the withdrawals part assuming a perfect situation for an attacker (it has unlimited pool of ETH) then the limitation would be by a max number of deposits per block, which is 1271 with 30M gas limit. This gives us more than 1 year to overflow the merkle tree.

If we assume attacker can’t overcome the number of deposits per block then EIP-6110 moves us from 102 years with the status quo down to 1.28 years to get to the overflow. The computation is as follows: years_to_overflow = (2**32 - 1) / deposits_per_block / blocks_per_year; blocks_per_year = 300 * 24 * 365.25

We would need to somehow distinguish deposits that are processed by the new 6110 logic from those that are supposed to be processed by the poll, as the poll is lagging behind. An alternative to the dynamic approach is to introduce a DEPOSIT_TRANSITION_INDEX = X in both CL and EL clients, and set this parameter in some advance so the set index definitely happens after the HF is activated. Then EL will have to not bubble up deposits that have the index below that parameter and CL can use this parameter to stop processing Eth1Data deposits. But this seems to be more complicated than just passing an index alongside to each deposit.

We actually did the same computation. I used the 15M gas target though, 30M cannot consistently be filled without raising fees to an exorbitant level. so, 2*1.28 years + some leeway to account for imperfections is around 3 years.

The number is actually also the same before / after EIP-6110, as the deposit contract is processed immediately. The only change is that deposits are processed more quickly on the CL, and with that, withdrawals are also processed more quickly.

1 Like

As long as the deposits are processed in order, all’s fine. Maybe a temporary buffer is needed in BeaconState to hold deposits from the new style until the old style loop has caught up. That scratch area could be used generally, and hold all pending deposits regardless of source. Processing would only continue in sequence, as in, no reordering of indices. The queue could also be kept around after the transition if we wish to retain the 16 deposits / block limit (the items would be moved from the queue into the existing deposits mechanism).

Does EL have a requirement to hold on to historical data for a while, similar to CL who has to keep blocks for ~5 months? In that case, one could also transfer the historical catch-up deposits using the new mechanism. But probably more complicated as the EL doesn’t know how far the CL has caught up. The CL could tell EL using a EIP-4788 style merkle proof, I guess, but, rather complex for a temporary mechanism.

BTW: Are we sure that noone is depending on the eth1_data area of the BeaconState being maintained? Theoretically, CL could keep updating it, as it processes deposits.

I see two things worth further investigation and double checking with the community:

  1. Do staking pools or anyone else rely on the strict order of the deposit processing? If yes that we would have to introduce a queue keeping deposits there until the gap is filled by the Eth1Data poll.
  2. Does anyone rely on eth1_data updates in the beacon state?

If we queue deposits then the churn size would be a natural limitation for a number of deposits that can be processed per epoch — so we wouldn’t need any other limit here.

Another aspect to consider is the signature load for the CL. Each deposit is signed separately, there is no potential for aggregating the signature verification. Batching may still be possible.

By going through a queue, there would be no extra load, and deposits would continue to be processed in chunks of 16 per block. Plus, can use it to ensure proper ordering.

Yes, indeed for a large number of deposits per block sig verification could become a bottleneck. But normally the number of deposits per block isn’t significant enough to cause any harmful delays, adversary can pack as much deposits as it can into a block but the cost of this seems to be quite big to induces a long term delays in block processing

Also, CL historically was never affected by the EL gas limit. If there is no ratelimiting queue, that means that this suddenly becomes a security vector. Note that CL / EL are not necessarily run on the same hardware.

Side note if a queue gets introduced: It won’t fragment, so is quite straightforward to introduce.

  1. DepositReceipts get appended to the queue, but not yet processed (they are guaranteed to be in order, sequential, without gaps).
  2. When old style deposits have caught up, start processing the queue (they are guaranteed to all be older than any DepositReceipts).
  3. Process up to 16 deposits from the queue, but only after old style deposits are finished (this prevents the EL gas cap affecting the hardware requirements for a CL client – without a rate limit, at current gas limit, the signature verification load in the CL could increase by >10x at worst case of ~1700 deposits).
class DepositReceipt(Container):
    pubkey: BLSPubkey
    withdrawal_credentials: Bytes32
    amount: Gwei
    signature: BLSSignature
    index: uint64

I think reusing DepositData, similar to how is done in the Deposit could allow slightly more code reuse.

class DepositReceipt(Container):
    data: DepositData
    index: uint64

AFAICS, the reuse will only be on the struct level, at least in the spec logic. DepositReceipt (or DepositRequest) after renaming reflects the corresponding EL structure. Even if we decide to get rid of requests in the EL block, it will still be cleaner to have this struct match the one that is returned by EL.