capella forkchoiceUpdated support (#4462)

* capella forkchoiceUpdated support

* match V2 fcU with V2 getPayload
This commit is contained in:
tersec 2023-01-06 21:01:10 +00:00 committed by GitHub
parent 75d0e0d4c8
commit 47cb0f7991
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 147 additions and 116 deletions

View File

@ -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).
@ -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
@ -30,6 +32,7 @@ type
finalizedBlockRoot*: Eth2Digest
timestamp*: uint64
feeRecipient*: Eth1Address
withdrawals*: Opt[seq[Withdrawal]]
ConsensusManager* = object
expectedSlot: Slot
@ -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)
@ -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(
@ -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

View File

@ -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).
@ -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 =
@ -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,
@ -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 =
@ -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,
@ -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:
@ -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:
@ -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:
@ -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.}

View File

@ -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).
@ -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 |

View File

@ -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,

View File

@ -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).
@ -282,7 +282,8 @@ from web3/engine_api import ForkchoiceUpdatedResponse
proc forkchoice_updated(
head_block_hash: Eth2Digest, safe_block_hash: Eth2Digest,
finalized_block_hash: Eth2Digest, timestamp: uint64, random: Eth2Digest,
fee_recipient: ethtypes.Address, execution_engine: Eth1Monitor):
fee_recipient: ethtypes.Address, withdrawals: Opt[seq[Withdrawal]],
execution_engine: Eth1Monitor):
Future[Option[bellatrix.PayloadID]] {.async.} =
logScope:
head_block_hash
@ -295,7 +296,7 @@ proc forkchoice_updated(
awaitWithTimeout(
execution_engine.forkchoiceUpdated(
head_block_hash, safe_block_hash, finalized_block_hash,
timestamp, random.data, fee_recipient),
timestamp, random.data, fee_recipient, withdrawals),
FORKCHOICEUPDATED_TIMEOUT):
error "Engine API fork-choice update timed out"
default(ForkchoiceUpdatedResponse)
@ -396,13 +397,19 @@ proc getExecutionPayload[T](
lastFcU = node.consensusManager.forkchoiceUpdatedInfo
timestamp = withState(proposalState[]):
compute_timestamp_at_slot(forkyState.data, forkyState.data.slot)
withdrawals = withState(proposalState[]):
when stateFork >= BeaconStateFork.Capella:
Opt.some get_expected_withdrawals(forkyState.data)
else:
Opt.none(seq[Withdrawal])
payload_id =
if lastFcU.isSome and
lastFcU.get.headBlockRoot == latestHead and
lastFcU.get.safeBlockRoot == latestSafe and
lastFcU.get.finalizedBlockRoot == latestFinalized and
lastFcU.get.timestamp == timestamp and
lastFcU.get.feeRecipient == feeRecipient:
lastFcU.get.feeRecipient == feeRecipient and
lastFcU.get.withdrawals == withdrawals:
some bellatrix.PayloadID(lastFcU.get.payloadId)
else:
debug "getExecutionPayload: didn't find payloadId, re-querying",
@ -411,11 +418,11 @@ proc getExecutionPayload[T](
feeRecipient,
cachedForkchoiceUpdateInformation = lastFcU
let random = withState(proposalState[]):
get_randao_mix(forkyState.data, get_current_epoch(forkyState.data))
let random = withState(proposalState[]): get_randao_mix(
forkyState.data, get_current_epoch(forkyState.data))
(await forkchoice_updated(
latestHead, latestSafe, latestFinalized, timestamp, random,
feeRecipient, node.consensusManager.eth1Monitor))
feeRecipient, withdrawals, node.consensusManager.eth1Monitor))
payload = try:
awaitWithTimeout(
get_execution_payload[T](payload_id, node.consensusManager.eth1Monitor),