-
Notifications
You must be signed in to change notification settings - Fork 998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
In-protocol deposits flow (no queue approach) #3177
Conversation
Replacement of
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mkalinin Great work!
It looks like a sound solution to me. Need more tests to verify if it works bug-free though. I agreed to merge it into the feature spec collections soon.
# Set deposit receipt start index | ||
if state.deposit_receipts_start_index == NOT_SET_DEPOSIT_RECEIPTS_START_INDEX: | ||
state.deposit_receipts_start_index = deposit_receipt.index |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will there be any old Eth1Data Deposit
after the first DepositReceipt
is processed? That is, will there be a period of two types of deposit data sources in the same block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this period will last until state.eth1_deposit_index
reaches state.deposit_receipts_start_index
and a subsequent deposit receipt is processed or (if no deposit receipt appears) state.eth1_deposit_index
reaches state.eth1_data.deposit_count
Co-authored-by: Hsiao-Wei Wang <hsiaowei.eth@gmail.com>
Really nice design! Noting a todo to update the PR description to reflect the queue-less design. I'm ok giving up (2) and (3) for protocol simplicity. |
#### New `apply_deposit` | ||
|
||
```python | ||
def apply_deposit(state: BeaconState, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might consider giving this new mechanism a name like "synchronous deposits" or something. So that the objects and function names would be further disambiguated from the old machinery -- e.g. apply_synchronous_deposit
, SynchronousDepositReceipt
, etc
that said, maybe you have a better name than "Synchronous deposits"
domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks | ||
signing_root = compute_signing_root(deposit_message, domain) | ||
# Initialize validator if the deposit signature is valid | ||
if bls.Verify(pubkey, signing_root, signature): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to keep this as parallel to the previous fork process_deposit
functions as possible -- so I'd prefer to use the if not
gating condition.
Also, we could make a new helper for this and past phases -- verify_and_add_validator()
that handles all of this logic (from line 247 to 260 here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, I guess we could make apply_deposit
the generic function, define it starting in phase 0, and just utilize it here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, apply_deposit
was introduced here for deduplication purposes, it has the same logic as process_deposit
starting from the point after merkle proof verification and bumping deposit index in the state.
Probably, we can introduce this function in Altair spec and reuse it here. If we did this for the Phase0 then it would have to be overridden in Altair.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which isn't the worst. It's a nice consistency throughout even with the altair override
|
||
# Signify the end of transition to in-protocol deposit logic | ||
if state.eth1_deposit_index >= state.deposit_receipts_start_index: | ||
state.eth1_deposit_index = deposit_receipt.index + 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what is going on here. Isn't the end signified already by state.eth1_deposit_index >= state.deposit_receipts_start_index
and thus we don't need to artificially set eth1_deposit_index
to the current receipt+1?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't do this then Eth1Data poll will keep including already processed deposits in block.
The other way around is to make a change to the validator spec to prevent Eth1Data poll from including deposits if state.eth1_deposit_index >= state.deposit_receipts_start_index
.
# [New in EIP-6110] | ||
# Skip already processed deposits | ||
if state.eth1_deposit_index >= state.deposit_receipts_start_index: | ||
state.eth1_deposit_index += 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused about how this works deposit_receipts_start_index
is static once initialized. So why do we need to do this increment here? can't it just be a return
and forever return
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once state.eth1_deposit_index
reaches the first in-protocol deposit index there is likely a tail of deposits from the same voting period remaining to be processed. Say, no in-protocol deposit is happening for hours from that event. In such a case Eth1Data
poll machinery would be attempting to include the same portion of deposits because the index wouldn't be update.
If we make a change to the validator spec to stop including old fashioned deposits once the first in-protocol index is reached then we can do a simple return. Current spec is made in a way that no changes to the validator logic is required, and all the new changes are introduced into the state transition function where we can ensure to have a good test coverage without additional developer's work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do change the validator spec then we can leave process_deposit
unchanged by enforcing the following condition in the process_operations
:
# Disable former deposit mechanism once all prior deposits are processed
eth1_deposit_index_limit = min(state.eth1_data.deposit_count, state.deposit_receipts_start_index)
if state.eth1_deposit_index < eth1_deposit_index_limit:
assert len(body.deposits) == min(MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index)
else:
assert len(body.deposits) == 0
This actually would look cleaner from the spec perspective
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made it into the spec. Lmk, if it looks fine and I will update validator guide respectively
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UPD: changes to the validator guide are in place now
Co-authored-by: Danny Ryan <dannyjryan@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice edits!
I'm happy to merge the feature and iterate on testing and other things from here in other PRs
# [Modified in EIP-6110] | ||
# Disable former deposit mechanism once all prior deposits are processed | ||
eth1_deposit_index_limit = min(state.eth1_data.deposit_count, state.deposit_receipts_start_index) | ||
if state.eth1_deposit_index < eth1_deposit_index_limit: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice! yes, much cleaner and easier to reason about. 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
|
||
### Deposits | ||
|
||
The expected number of deposits MUST be changed from `min(MAX_DEPOSITS, eth1_data.deposit_count - state.eth1_deposit_index)` to the result of the following function: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note to myself: I just figured out that this PR kind of deprecates MAX_DEPOSITS
preset. MAX_DEPOSITS
doesn't represent the upper bound of deposits anymore.
Co-authored-by: Hsiao-Wei Wang <hsiaowei.eth@gmail.com>
Introduction
On the top of EIP-6110 proposal, this PR introduces a specification of in-protocol deposit processing flow. Advantages of the new deposits machinery are outlined in the EIP, let's list them here for visibility:
Deprecating
Eth1Data
pollThe proposed design allows for straightforward deprecation of
Eth1Data
poll. Once EIP-6110 is activated supplied deposits start to be queued in the beacon state in thepending_deposits
queue. When thestate.eth1_deposit_index
reaches the index of the first queued deposit receipt the new mechanism takes over. Once that happens the network stops relying onEth1Data
poll because the poll does always lag behind the new mechanism due to the follow distance and the voting period.After the new mechanism takes over CL client developers are free to decide when they want to get rid of the old machinery and stop their client from voting on
Eth1Data
. The deprecation may be rolled out in uncoordinated way without any impact on the network.DoS vector
Block's data complexity is analysed in the Data complexity section of the EIP and is not considered as a source of additional DoS vector. Beacon state data complexity is trivial as the new
IndexedDepositData
structure has a smaller size than theValidator
structure.The most consuming computation of deposit processing is signature verification. The complexity is bounded by a maximum number of deposits per block which is around 512 with 30M gas block. So, it is ~512 signature verifications which is roughly about 0.5s of processing without batched verification optimisation. An attacker would need to spend 100 ETH to slow down block processing by a hundred of milliseconds which doesn't look as a sustainable and valuable attack on a long period of time.
An optimistically syncing node may be susceptible to a more severe attack scenario. Such a node can't validate a list of deposits provided in a payload which makes it possible for attacker to include as many deposits as the limitation allows to. Currently, it is 8,192 deposits (1.5MB of data) with rough processing time of 8s. Considering an attacker would need to sign off on this block with its crypto economically viable signature, this attack vector doesn't seem to be valuable as it can't result in a significant slow down of a sync process.
Optimistic sync
An optimistically syncing node have to rely on the honest majority assumption. That is, if adversary is powerful enough to finalize a deposit flow, a syncing node will have to apply these deposits disregarding the validity of deposit receipts with respect to the execution of a given block. Thus, adversary may convince an honest node to accept fake deposits if it can reach finality on invalid chain. The same is applicable to the validity of EL world state today and the proposed deposits machinery doesn't change security model in that regard.
Online nodes can't be tricked into this situation because their EL verifies supplied deposits with respect to the execution.
Pending deposits queue
The proposed design relies on
pending_deposits
queue in the beacon chain state. This queue servers the following goals:Eth1Data
voting and deposit receipts supplied by the new machinery. The gap is inevitable because proposer voting is lagging behind the head by the follow distance plus the size of the voting period.MAX_DEPOSITS * SLOTS_PER_EPOCH
to preserve the status quo.(pubkey, index)
cache re-org aware.Note that this requirement isn't introduced by this proposal and already exists today. Consider the case when there are two competing forks during a period of non-finality and both forks are longer than the follow distance. Any deposit transaction missed or misordered on any of these forks with respect to another fork may result in a different validator indexes assigned to the same pubkeys on each fork.
However, this proposal moves the re-org handling problem from an edge case area to an important thing that either spec or implementations must take care of.
Based on historical data analysis, the expectation is to have less than 1 deposit per block which is less than 32 deposits per epoch. With such a pace the pending deposits queue will contain less than 96 deposits at once in the case when finality is advanced every epoch. Based on that, we assume that the state hashing complexity increase is bounded by roughly a hundred deposits per block in a normal case.
The new machinery doesn't use
is_valid_merkle_branch
function which call implies 32 hashing operations per each deposit. Therefore, the computational complexity is expected to be reduced by the proposed mechanism.Approaches reducing the queue usage
There is a possibility to narrow down the queue usage to (1) only. The queue may be used until the new deposit processing takes over, after that the queue may stay empty and be removed in one of the next CL upgrades.
Achieving that implies giving up on (2) and (3).
Weak subjectivity
One of the inputs into WS period computation is a number of top ups per epoch that stakers may do to prevent their validators from being ejected and ultimately increase their portion of stake with respect to those that are leaking.
Number of top ups is limited by
MAX_DEPOSITS * SLOTS_PER_EPOCH
to preserve the status quo. Removing the queue would mean this boundary moved to512 * SLOTS_PER_EPOCH
with 30M gas block.Modified script shows the following changes in the WS period computations (deviations are highlighted):
16
deposits per slot512
deposits per slotThe deviations don't look too impactful. Tagging @adiasg to see his thoughts on whether we allowed to increase top ups boundary.
Validator pubkey cache
Client implementations will have to maintain their validator's cache in a way that it dependends on a block tree branch. It can be a composite cache consisting of finalized part and a few small pieces tied to a tip of each branch.
Validator becomes active only after relevant deposits are finalized which means that finalized part of the cache should cover most of the hits. While non-finalized parts are supposed to be used only for look ups during deposit processing.