Skip to content

Commit

Permalink
capella forkchoiceUpdated support (#4462)
Browse files Browse the repository at this point in the history
* capella forkchoiceUpdated support

* match V2 fcU with V2 getPayload
  • Loading branch information
tersec authored Jan 6, 2023
1 parent 75d0e0d commit 47cb0f7
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 116 deletions.
21 changes: 14 additions & 7 deletions beacon_chain/consensus_object_pools/consensus_manager.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# beacon_chain
# Copyright (c) 2018-2022 Status Research & Development GmbH
# Copyright (c) 2018-2023 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
Expand All @@ -16,6 +16,8 @@ import
../consensus_object_pools/[blockchain_dag, block_quarantine, attestation_pool],
../eth1/eth1_monitor

from ../spec/beaconstate import get_expected_withdrawals
from ../spec/datatypes/capella import Withdrawal
from ../spec/eth2_apis/dynamic_fee_recipients import
DynamicFeeRecipientsStore, getDynamicFeeRecipient
from ../validators/keystore_management import
Expand All @@ -30,6 +32,7 @@ type
finalizedBlockRoot*: Eth2Digest
timestamp*: uint64
feeRecipient*: Eth1Address
withdrawals*: Opt[seq[Withdrawal]]

ConsensusManager* = object
expectedSlot: Slot
Expand Down Expand Up @@ -360,6 +363,11 @@ proc runProposalForkchoiceUpdated*(
get_randao_mix(forkyState.data, get_current_epoch(forkyState.data)).data
feeRecipient = self[].getFeeRecipient(
nextProposer, Opt.some(validatorIndex), nextWallSlot.epoch)
withdrawals = withState(self.dag.headState):
when stateFork >= BeaconStateFork.Capella:
Opt.some get_expected_withdrawals(forkyState.data)
else:
Opt.none(seq[Withdrawal])
beaconHead = self.attestationPool[].getBeaconHead(self.dag.head)
headBlockRoot = self.dag.loadExecutionBlockRoot(beaconHead.blck)

Expand All @@ -369,11 +377,9 @@ proc runProposalForkchoiceUpdated*(
try:
let fcResult = awaitWithTimeout(
forkchoiceUpdated(
self.eth1Monitor,
headBlockRoot,
beaconHead.safeExecutionPayloadHash,
beaconHead.finalizedExecutionPayloadHash,
timestamp, randomData, feeRecipient),
self.eth1Monitor, headBlockRoot, beaconHead.safeExecutionPayloadHash,
beaconHead.finalizedExecutionPayloadHash, timestamp, randomData,
feeRecipient, withdrawals),
FORKCHOICEUPDATED_TIMEOUT):
debug "runProposalForkchoiceUpdated: forkchoiceUpdated timed out"
ForkchoiceUpdatedResponse(
Expand All @@ -389,7 +395,8 @@ proc runProposalForkchoiceUpdated*(
safeBlockRoot: beaconHead.safeExecutionPayloadHash,
finalizedBlockRoot: beaconHead.finalizedExecutionPayloadHash,
timestamp: timestamp,
feeRecipient: feeRecipient)
feeRecipient: feeRecipient,
withdrawals: withdrawals)
except CatchableError as err:
error "Engine API fork-choice update failed", err = err.msg

Expand Down
85 changes: 50 additions & 35 deletions beacon_chain/eth1/eth1_monitor.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# beacon_chain
# Copyright (c) 2018-2022 Status Research & Development GmbH
# Copyright (c) 2018-2023 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
Expand Down Expand Up @@ -323,6 +323,24 @@ template asBlockHash*(x: Eth2Digest): BlockHash =

const weiInGwei = 1_000_000_000.u256

from ../spec/datatypes/capella import ExecutionPayload, Withdrawal

func asConsensusWithdrawal(w: WithdrawalV1): capella.Withdrawal =
capella.Withdrawal(
index: w.index.uint64,
validator_index: w.validatorIndex.uint64,
address: ExecutionAddress(data: w.address.distinctBase),

# TODO spec doesn't mention non-even-multiples, also overflow
amount: (w.amount.u256 div weiInGwei).truncate(uint64))

func asEngineWithdrawal(w: capella.Withdrawal): WithdrawalV1 =
WithdrawalV1(
index: Quantity(w.index),
validatorIndex: Quantity(w.validator_index),
address: Address(w.address.data),
amount: w.amount.u256 * weiInGwei)

func asConsensusExecutionPayload*(rpcExecutionPayload: ExecutionPayloadV1):
bellatrix.ExecutionPayload =
template getTransaction(tt: TypedTransaction): bellatrix.Transaction =
Expand All @@ -348,18 +366,10 @@ func asConsensusExecutionPayload*(rpcExecutionPayload: ExecutionPayloadV1):
transactions: List[bellatrix.Transaction, MAX_TRANSACTIONS_PER_PAYLOAD].init(
mapIt(rpcExecutionPayload.transactions, it.getTransaction)))

from ../spec/datatypes/capella import ExecutionPayload, Withdrawal

func asConsensusExecutionPayload*(rpcExecutionPayload: ExecutionPayloadV2):
capella.ExecutionPayload =
template getTransaction(tt: TypedTransaction): bellatrix.Transaction =
bellatrix.Transaction.init(tt.distinctBase)
template getConsensusWithdrawal(w: WithdrawalV1): capella.Withdrawal =
capella.Withdrawal(
index: w.index.uint64,
validator_index: w.validatorIndex.uint64,
address: ExecutionAddress(data: w.address.distinctBase),
amount: (w.amount.u256 div weiInGwei).truncate(uint64)) # TODO spec doesn't mention non-even-multiples, also overflow

capella.ExecutionPayload(
parent_hash: rpcExecutionPayload.parentHash.asEth2Digest,
Expand All @@ -381,7 +391,7 @@ func asConsensusExecutionPayload*(rpcExecutionPayload: ExecutionPayloadV2):
transactions: List[bellatrix.Transaction, MAX_TRANSACTIONS_PER_PAYLOAD].init(
mapIt(rpcExecutionPayload.transactions, it.getTransaction)),
withdrawals: List[capella.Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD].init(
mapIt(rpcExecutionPayload.withdrawals, it.getConsensusWithdrawal)))
mapIt(rpcExecutionPayload.withdrawals, it.asConsensusWithdrawal)))

func asEngineExecutionPayload*(executionPayload: bellatrix.ExecutionPayload):
ExecutionPayloadV1 =
Expand Down Expand Up @@ -410,12 +420,6 @@ func asEngineExecutionPayload*(executionPayload: capella.ExecutionPayload):
ExecutionPayloadV2 =
template getTypedTransaction(tt: bellatrix.Transaction): TypedTransaction =
TypedTransaction(tt.distinctBase)
template getEngineWithdrawal(w: capella.Withdrawal): WithdrawalV1 =
WithdrawalV1(
index: Quantity(w.index),
validatorIndex: Quantity(w.validator_index),
address: Address(w.address.data),
amount: w.amount.u256 * weiInGwei)

engine_api.ExecutionPayloadV2(
parentHash: executionPayload.parent_hash.asBlockHash,
Expand All @@ -434,7 +438,7 @@ func asEngineExecutionPayload*(executionPayload: capella.ExecutionPayload):
baseFeePerGas: executionPayload.base_fee_per_gas,
blockHash: executionPayload.block_hash.asBlockHash,
transactions: mapIt(executionPayload.transactions, it.getTypedTransaction),
withdrawals: mapIt(executionPayload.withdrawals, it.getEngineWithdrawal))
withdrawals: mapIt(executionPayload.withdrawals, it.asEngineWithdrawal))

func shortLog*(b: Eth1Block): string =
try:
Expand Down Expand Up @@ -589,9 +593,9 @@ proc newPayload*(p: Eth1Monitor, payload: engine_api.ExecutionPayloadV2):

p.dataProvider.web3.provider.engine_newPayloadV2(payload)

proc forkchoiceUpdated*(p: Eth1Monitor,
headBlock, safeBlock, finalizedBlock: Eth2Digest):
Future[engine_api.ForkchoiceUpdatedResponse] =
proc forkchoiceUpdated*(
p: Eth1Monitor, headBlock, safeBlock, finalizedBlock: Eth2Digest):
Future[engine_api.ForkchoiceUpdatedResponse] =
# Eth1 monitor can recycle connections without (external) warning; at least,
# don't crash.
if p.isNil or p.dataProvider.isNil:
Expand All @@ -608,12 +612,12 @@ proc forkchoiceUpdated*(p: Eth1Monitor,
finalizedBlockHash: finalizedBlock.asBlockHash),
none(engine_api.PayloadAttributesV1))

proc forkchoiceUpdated*(p: Eth1Monitor,
headBlock, safeBlock, finalizedBlock: Eth2Digest,
timestamp: uint64,
randomData: array[32, byte],
suggestedFeeRecipient: Eth1Address):
Future[engine_api.ForkchoiceUpdatedResponse] =
proc forkchoiceUpdated*(
p: Eth1Monitor, headBlock, safeBlock, finalizedBlock: Eth2Digest,
timestamp: uint64, randomData: array[32, byte],
suggestedFeeRecipient: Eth1Address,
withdrawals: Opt[seq[capella.Withdrawal]]):
Future[engine_api.ForkchoiceUpdatedResponse] =
# Eth1 monitor can recycle connections without (external) warning; at least,
# don't crash.
if p.isNil or p.dataProvider.isNil:
Expand All @@ -623,15 +627,26 @@ proc forkchoiceUpdated*(p: Eth1Monitor,
payloadStatus: PayloadStatusV1(status: PayloadExecutionStatus.syncing)))
return fcuR

p.dataProvider.web3.provider.engine_forkchoiceUpdatedV1(
ForkchoiceStateV1(
headBlockHash: headBlock.asBlockHash,
safeBlockHash: safeBlock.asBlockHash,
finalizedBlockHash: finalizedBlock.asBlockHash),
some(engine_api.PayloadAttributesV1(
timestamp: Quantity timestamp,
prevRandao: FixedBytes[32] randomData,
suggestedFeeRecipient: suggestedFeeRecipient)))
let forkchoiceState = ForkchoiceStateV1(
headBlockHash: headBlock.asBlockHash,
safeBlockHash: safeBlock.asBlockHash,
finalizedBlockHash: finalizedBlock.asBlockHash)

if withdrawals.isNone:
p.dataProvider.web3.provider.engine_forkchoiceUpdatedV1(
forkchoiceState,
some(engine_api.PayloadAttributesV1(
timestamp: Quantity timestamp,
prevRandao: FixedBytes[32] randomData,
suggestedFeeRecipient: suggestedFeeRecipient)))
else:
p.dataProvider.web3.provider.engine_forkchoiceUpdatedV2(
forkchoiceState,
some(engine_api.PayloadAttributesV2(
timestamp: Quantity timestamp,
prevRandao: FixedBytes[32] randomData,
suggestedFeeRecipient: suggestedFeeRecipient,
withdrawals: mapIt(withdrawals.get, it.asEngineWithdrawal))))

# TODO can't be defined within exchangeTransitionConfiguration
proc `==`(x, y: Quantity): bool {.borrow, noSideEffect.}
Expand Down
70 changes: 69 additions & 1 deletion beacon_chain/spec/beaconstate.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# beacon_chain
# Copyright (c) 2018-2022 Status Research & Development GmbH
# Copyright (c) 2018-2023 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
Expand Down Expand Up @@ -818,6 +818,74 @@ func get_next_sync_committee_keys(
i += 1'u64
res

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.2/specs/capella/beacon-chain.md#has_eth1_withdrawal_credential
func has_eth1_withdrawal_credential(validator: Validator): bool =
## Check if ``validator`` has an 0x01 prefixed "eth1" withdrawal credential.
validator.withdrawal_credentials.data[0] == ETH1_ADDRESS_WITHDRAWAL_PREFIX

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.2/specs/capella/beacon-chain.md#is_fully_withdrawable_validator
func is_fully_withdrawable_validator(
validator: Validator, balance: Gwei, epoch: Epoch): bool =
## Check if ``validator`` is fully withdrawable.
has_eth1_withdrawal_credential(validator) and
validator.withdrawable_epoch <= epoch and balance > 0

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.2/specs/capella/beacon-chain.md#is_partially_withdrawable_validator
func is_partially_withdrawable_validator(
validator: Validator, balance: Gwei): bool =
## Check if ``validator`` is partially withdrawable.
let
has_max_effective_balance =
validator.effective_balance == MAX_EFFECTIVE_BALANCE
has_excess_balance = balance > MAX_EFFECTIVE_BALANCE
has_eth1_withdrawal_credential(validator) and
has_max_effective_balance and has_excess_balance

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.2/specs/capella/beacon-chain.md#new-get_expected_withdrawals
func get_expected_withdrawals*(state: capella.BeaconState): seq[Withdrawal] =
let
epoch = get_current_epoch(state)
num_validators = lenu64(state.validators)
var
withdrawal_index = state.next_withdrawal_index
validator_index = state.next_withdrawal_validator_index
withdrawals: seq[Withdrawal] = @[]
bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP)
for _ in 0 ..< bound:
let
validator = state.validators[validator_index]
balance = state.balances[validator_index]
if is_fully_withdrawable_validator(validator, balance, epoch):
var w = Withdrawal(
index: withdrawal_index,
validator_index: validator_index,
amount: balance)
w.address.data[0..19] = validator.withdrawal_credentials.data[12..^1]
withdrawals.add w
withdrawal_index = WithdrawalIndex(withdrawal_index + 1)
elif is_partially_withdrawable_validator(validator, balance):
var w = Withdrawal(
index: withdrawal_index,
validator_index: validator_index,
amount: balance - MAX_EFFECTIVE_BALANCE)
w.address.data[0..19] = validator.withdrawal_credentials.data[12..^1]
withdrawals.add w
withdrawal_index = WithdrawalIndex(withdrawal_index + 1)
if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD:
break
validator_index = (validator_index + 1) mod num_validators
withdrawals

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.2/specs/eip4844/beacon-chain.md#disabling-withdrawals
func get_expected_withdrawals*(state: eip4844.BeaconState): seq[Withdrawal] =
# During testing we avoid Capella-specific updates to the state transition.
#
# ...
#
# The `get_expected_withdrawals` function is also modified to return an empty
# withdrawals list.
@[]

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.2/specs/altair/beacon-chain.md#get_next_sync_committee
func get_next_sync_committee*(
state: altair.BeaconState | bellatrix.BeaconState | capella.BeaconState |
Expand Down
66 changes: 0 additions & 66 deletions beacon_chain/spec/state_transition_block.nim
Original file line number Diff line number Diff line change
Expand Up @@ -673,72 +673,6 @@ proc process_execution_payload*(

ok()

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.1/specs/capella/beacon-chain.md#has_eth1_withdrawal_credential
func has_eth1_withdrawal_credential(validator: Validator): bool =
## Check if ``validator`` has an 0x01 prefixed "eth1" withdrawal credential.
validator.withdrawal_credentials.data[0] == ETH1_ADDRESS_WITHDRAWAL_PREFIX

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.1/specs/capella/beacon-chain.md#is_fully_withdrawable_validator
func is_fully_withdrawable_validator(
validator: Validator, balance: Gwei, epoch: Epoch): bool =
## Check if ``validator`` is fully withdrawable.
has_eth1_withdrawal_credential(validator) and
validator.withdrawable_epoch <= epoch and balance > 0

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.1/specs/capella/beacon-chain.md#is_partially_withdrawable_validator
func is_partially_withdrawable_validator(
validator: Validator, balance: Gwei): bool =
## Check if ``validator`` is partially withdrawable.
let
has_max_effective_balance =
validator.effective_balance == MAX_EFFECTIVE_BALANCE
has_excess_balance = balance > MAX_EFFECTIVE_BALANCE
has_eth1_withdrawal_credential(validator) and
has_max_effective_balance and has_excess_balance

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.2/specs/capella/beacon-chain.md#new-get_expected_withdrawals
func get_expected_withdrawals(state: capella.BeaconState): seq[Withdrawal] =
let epoch = get_current_epoch(state)
var
withdrawal_index = state.next_withdrawal_index
validator_index = state.next_withdrawal_validator_index
withdrawals: seq[Withdrawal] = @[]
bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP)
for _ in 0 ..< bound:
let
validator = state.validators[validator_index]
balance = state.balances[validator_index]
if is_fully_withdrawable_validator(validator, balance, epoch):
var w = Withdrawal(
index: withdrawal_index,
validator_index: validator_index,
amount: balance)
w.address.data[0..19] = validator.withdrawal_credentials.data[12..^1]
withdrawals.add w
withdrawal_index = WithdrawalIndex(withdrawal_index + 1)
elif is_partially_withdrawable_validator(validator, balance):
var w = Withdrawal(
index: withdrawal_index,
validator_index: validator_index,
amount: balance - MAX_EFFECTIVE_BALANCE)
w.address.data[0..19] = validator.withdrawal_credentials.data[12..^1]
withdrawals.add w
withdrawal_index = WithdrawalIndex(withdrawal_index + 1)
if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD:
break
validator_index = (validator_index + 1) mod lenu64(state.validators)
withdrawals

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.1/specs/eip4844/beacon-chain.md#disabling-withdrawals
func get_expected_withdrawals(state: eip4844.BeaconState): seq[Withdrawal] =
# During testing we avoid Capella-specific updates to the state transition.
#
# ...
#
# The `get_expected_withdrawals` function is also modified to return an empty
# withdrawals list.
@[]

# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.1/specs/capella/beacon-chain.md#new-process_withdrawals
func process_withdrawals*(
state: var capella.BeaconState,
Expand Down
Loading

0 comments on commit 47cb0f7

Please sign in to comment.