-
Notifications
You must be signed in to change notification settings - Fork 40
feat: electra withdrawals #1431
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
Changes from all commits
ed74379
71b3674
9aa4202
f09b71b
df45ee4
8fb4d63
6e34108
620e700
d904593
5445583
5cbc5af
753badb
29bf08a
17bf479
42b0bfb
6df1b36
9e13ad5
abc2585
5b7e5dc
4eddb4b
44a00ec
9bc3a0b
ed8be8a
c084913
6ec0216
7da3d5a
70d40aa
e4e8607
0e53f57
12ed2d3
a73c061
6ad6e4d
3eef3e7
88c7308
14a0539
ebc73e0
f8c443a
8f72d6b
b4a9d15
280669a
c2db045
7bc38b9
6d74e53
40b0a5c
82afc64
6521f68
e437245
8417e96
39dd889
2db24de
1b8024e
253b824
6ace4d9
2166fb4
2c1001f
0f8b360
5b9721c
38c5a4e
a7997c5
20515be
0726736
65b1f57
9f720d5
27ef594
4dcfb90
e246258
15453e1
93c1002
5b4ece8
3e392ea
ff7ab9f
7cab380
aaa1dde
ae7be10
7f55534
b426cc7
7d711d5
c4008f2
ffbd715
a9a9d2f
3346f10
86d539f
b85fcbd
5f2929f
8dab043
ba16426
2fef959
0b06993
2a1bbfc
ba77dcb
48965d5
5415c8c
f9250cb
1bb0e6c
c049a33
2b6c4e1
bdd2e21
a764dc9
2eb2ce3
5bdef50
e163179
75bf0b4
dd42e10
d1eacf7
87a465e
46717b4
27aeeb0
0b8ed91
f3d5531
8b6f6a8
81b04d9
a6c143c
7125416
6ba37fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -294,11 +294,18 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do | |
%BeaconState{validators: validators} = state, | ||
%ExecutionPayload{withdrawals: withdrawals} | ||
) do | ||
expected_withdrawals = get_expected_withdrawals(state) | ||
{expected_withdrawals, processed_partial_withdrawals_count} = get_expected_withdrawals(state) | ||
|
||
with :ok <- check_withdrawals(withdrawals, expected_withdrawals) do | ||
state | ||
|> Map.update!(:balances, &decrease_balances(&1, withdrawals)) | ||
|> then( | ||
&%BeaconState{ | ||
&1 | ||
| pending_partial_withdrawals: | ||
Enum.drop(&1.pending_partial_withdrawals, processed_partial_withdrawals_count) | ||
} | ||
) | ||
|> update_next_withdrawal_index(withdrawals) | ||
|> update_next_withdrawal_validator_index(withdrawals, Aja.Vector.size(validators)) | ||
|> then(&{:ok, &1}) | ||
|
@@ -355,49 +362,147 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do | |
end) | ||
end | ||
|
||
@spec get_expected_withdrawals(BeaconState.t()) :: list(Withdrawal.t()) | ||
@spec get_expected_withdrawals(BeaconState.t()) :: | ||
{list(Withdrawal.t()), non_neg_integer()} | ||
def get_expected_withdrawals(%BeaconState{} = state) do | ||
# Compute the next batch of withdrawals which should be included in a block. | ||
epoch = Accessors.get_current_epoch(state) | ||
|
||
max_validators_per_withdrawals_sweep = ChainSpec.get("MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP") | ||
max_withdrawals_per_payload = ChainSpec.get("MAX_WITHDRAWALS_PER_PAYLOAD") | ||
max_effective_balance = ChainSpec.get("MAX_EFFECTIVE_BALANCE") | ||
# Consume pending partial withdrawals | ||
{processed_partial_withdrawals_count, withdrawal_index, pending_partial_withdrawals} = | ||
state.pending_partial_withdrawals | ||
|> Enum.reduce_while({0, state.next_withdrawal_index, []}, fn withdrawal, | ||
{processed_partial_withdrawals_count, | ||
withdrawal_index, | ||
withdrawals} -> | ||
process_partial_withdrawal( | ||
state, | ||
withdrawal, | ||
processed_partial_withdrawals_count, | ||
withdrawal_index, | ||
withdrawals | ||
) | ||
end) | ||
|
||
bound = state.validators |> Aja.Vector.size() |> min(max_validators_per_withdrawals_sweep) | ||
# Sweep for remaining. | ||
non_partial_withdrawals = | ||
Stream.zip([state.validators, state.balances]) | ||
|> Stream.with_index() | ||
|> Stream.cycle() | ||
|> Stream.drop(state.next_withdrawal_validator_index) | ||
|> Stream.take(bound) | ||
|> Stream.map(fn {{validator, balance}, index} -> | ||
partially_withdrawn_balance = | ||
Enum.sum( | ||
for withdrawal <- pending_partial_withdrawals, | ||
withdrawal.validator_index == index, | ||
do: withdrawal.amount | ||
) | ||
|
||
balance = balance - partially_withdrawn_balance | ||
|
||
Stream.zip([state.validators, state.balances]) | ||
|> Stream.with_index() | ||
|> Stream.cycle() | ||
|> Stream.drop(state.next_withdrawal_validator_index) | ||
|> Stream.take(bound) | ||
|> Stream.map(fn {{validator, balance}, index} -> | ||
cond do | ||
Validator.fully_withdrawable_validator?(validator, balance, epoch) -> | ||
{validator, balance, index} | ||
|
||
Validator.partially_withdrawable_validator?(validator, balance) -> | ||
{validator, balance - max_effective_balance, index} | ||
|
||
true -> | ||
nil | ||
end | ||
end) | ||
|> Stream.reject(&is_nil/1) | ||
|> Stream.with_index() | ||
|> Stream.map(fn {{validator, balance, validator_index}, index} -> | ||
%Validator{withdrawal_credentials: withdrawal_credentials} = validator | ||
cond do | ||
Validator.fully_withdrawable_validator?(validator, balance, epoch) -> | ||
{validator, balance, index} | ||
|
||
<<_::binary-size(12), execution_address::binary>> = withdrawal_credentials | ||
Validator.partially_withdrawable_validator?(validator, balance) -> | ||
{validator, balance - Validator.get_max_effective_balance(validator), index} | ||
|
||
%Withdrawal{ | ||
index: index + state.next_withdrawal_index, | ||
validator_index: validator_index, | ||
address: execution_address, | ||
amount: balance | ||
true -> | ||
nil | ||
end | ||
end) | ||
|> Stream.reject(&is_nil/1) | ||
|> Stream.with_index() | ||
|> Stream.map(fn {{validator, balance, validator_index}, index} -> | ||
%Validator{withdrawal_credentials: withdrawal_credentials} = validator | ||
|
||
<<_::binary-size(12), execution_address::binary>> = withdrawal_credentials | ||
|
||
%Withdrawal{ | ||
index: index + withdrawal_index, | ||
validator_index: validator_index, | ||
address: execution_address, | ||
amount: balance | ||
} | ||
end) | ||
|
||
complete_withdrawals = | ||
(pending_partial_withdrawals ++ Enum.to_list(non_partial_withdrawals)) | ||
|> Enum.take(max_withdrawals_per_payload) | ||
|
||
{complete_withdrawals, processed_partial_withdrawals_count} | ||
end | ||
|
||
defp process_partial_withdrawal( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can improve both of this functions (process_partial_withdrawal and do_process_partial_withdrawal), but i guess it's better to have them merged now to the electra-support branch and pass the spec-test. I'm going to add these 2 to the list of functions we need to revisit for legibility. |
||
state, | ||
withdrawal, | ||
processed_partial_withdrawals_count, | ||
withdrawal_index, | ||
withdrawals | ||
) do | ||
epoch = Accessors.get_current_epoch(state) | ||
|
||
max_pending_partials_per_withdrawals_sweep = | ||
ChainSpec.get("MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP") | ||
|
||
# We expect partial withdrawals to be ordered by withdrawable epoch | ||
if withdrawal.withdrawable_epoch > epoch || | ||
processed_partial_withdrawals_count == max_pending_partials_per_withdrawals_sweep do | ||
{:halt, {processed_partial_withdrawals_count, withdrawal_index, withdrawals}} | ||
Comment on lines
+453
to
+455
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is mostly a question I have, they are ordered right? specially for the Btw I checked that this was the correct condition to break the loop in the spec but i was intrigued about the order. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good question, I don't explicitly order them but I've yet to see a case in which they aren't not in the spec tests and not while syncing Sepolia. I can add that or maybe leave a comment so that if we run into this issue later in testing we can fix it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe there is no way to get them unordered but yes, I think we should left at least a comment for the future (but probably where we actually add the pending widthrawal instead of here) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Left comment both there and when we are adding the partial withdrawals to the state here |
||
else | ||
do_process_partial_withdrawal( | ||
state, | ||
withdrawal, | ||
processed_partial_withdrawals_count, | ||
withdrawal_index, | ||
withdrawals | ||
) | ||
end | ||
end | ||
|
||
defp do_process_partial_withdrawal( | ||
state, | ||
withdrawal, | ||
processed_partial_withdrawals_count, | ||
withdrawal_index, | ||
withdrawals | ||
) do | ||
far_future_epoch = Constants.far_future_epoch() | ||
min_activation_balance = ChainSpec.get("MIN_ACTIVATION_BALANCE") | ||
validator = Aja.Vector.at(state.validators, withdrawal.validator_index) | ||
has_sufficient_effective_balance = validator.effective_balance >= min_activation_balance | ||
|
||
has_excess_balance = | ||
Aja.Vector.at(state.balances, withdrawal.validator_index) > min_activation_balance | ||
|
||
if validator.exit_epoch == far_future_epoch && has_sufficient_effective_balance && | ||
has_excess_balance do | ||
withdrawable_balance = | ||
min( | ||
Aja.Vector.at(state.balances, withdrawal.validator_index) - | ||
min_activation_balance, | ||
withdrawal.amount | ||
) | ||
|
||
<<_::binary-size(12), address::binary>> = validator.withdrawal_credentials | ||
|
||
withdrawal = %Withdrawal{ | ||
index: withdrawal_index, | ||
validator_index: withdrawal.validator_index, | ||
address: address, | ||
amount: withdrawable_balance | ||
} | ||
end) | ||
|> Enum.take(max_withdrawals_per_payload) | ||
|
||
{:cont, | ||
{processed_partial_withdrawals_count + 1, withdrawal_index + 1, | ||
withdrawals ++ [withdrawal]}} | ||
else | ||
{:cont, {processed_partial_withdrawals_count + 1, withdrawal_index, withdrawals}} | ||
end | ||
end | ||
|
||
@spec process_proposer_slashing(BeaconState.t(), Types.ProposerSlashing.t()) :: | ||
|
@@ -1030,7 +1135,7 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do | |
|
||
is_correct_source_address = | ||
case validator.withdrawal_credentials do | ||
<<_::binary-size(12), rest>> -> rest == address | ||
<<_::binary-size(12), validator_address::binary>> -> validator_address == address | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
_ -> false | ||
end | ||
|
||
|
@@ -1102,7 +1207,8 @@ defmodule LambdaEthereumConsensus.StateTransition.Operations do | |
{:ok, | ||
%BeaconState{ | ||
state | ||
| pending_partial_withdrawals: | ||
| # We should make sure that partial withdrawals are ordered by withdrawable epoch | ||
pending_partial_withdrawals: | ||
state.pending_partial_withdrawals ++ [pending_partial_withdrawal] | ||
}} | ||
else | ||
|
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.
Most of this function was preserved, but probably worth to take a look at it in the future regarding to clarity (and even performance after measuring it)