MEV block proposal (#3883)

* MEV validator registration

* add nearby canary to detect new beacon chain forks

* remove special MEV graffiti

* web3signer support

* fix trace logging

* Nim 1.2 needs raises Defect

* use template rather than proc in REST JSON parsing

* use --payload-builder-enable and --payload-builder-url

* explicitly default MEV to disabled

* explicitly empty default value for payload builder URL

* revert attestation pool to unstable version
This commit is contained in:
tersec 2022-08-01 06:41:47 +00:00 committed by GitHub
parent 17bf42316e
commit d62d13a23c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 635 additions and 45 deletions

View File

@ -66,6 +66,7 @@ type
lightClientPool*: ref LightClientPool
exitPool*: ref ExitPool
eth1Monitor*: Eth1Monitor
payloadBuilderRestClient*: RestClientRef
restServer*: RestServerRef
keymanagerServer*: RestServerRef
keymanagerToken*: Option[string]

View File

@ -541,6 +541,18 @@ type
desc: "Suggested fee recipient"
name: "suggested-fee-recipient" .}: Option[Address]
payloadBuilderEnable* {.
hidden
desc: "Enable external payload builder"
defaultValue: false
name: "payload-builder-enable" .}: bool
payloadBuilderUrl* {.
hidden
desc: "Payload builder URL"
defaultValue: ""
name: "payload-builder-url" .}: string
of BNStartUpCmd.createTestnet:
testnetDepositsFile* {.
desc: "A LaunchPad deposits file for the genesis state validators"

View File

@ -770,6 +770,17 @@ proc init*(T: type BeaconNode,
max(Moment.init(bellatrixEpochTime, Second),
Moment.now)
let payloadBuilderRestClient =
if config.payloadBuilderEnable:
RestClientRef.new(
config.payloadBuilderUrl,
httpFlags = {HttpClientFlag.NewConnectionAlways}).valueOr:
warn "Payload builder REST client setup failed",
payloadBuilderUrl = config.payloadBuilderUrl
nil
else:
nil
let node = BeaconNode(
nickname: nickname,
graffitiBytes: if config.graffiti.isSome: config.graffiti.get
@ -780,6 +791,7 @@ proc init*(T: type BeaconNode,
config: config,
attachedValidators: validatorPool,
eth1Monitor: eth1Monitor,
payloadBuilderRestClient: payloadBuilderRestClient,
restServer: restServer,
keymanagerServer: keymanagerServer,
keymanagerToken: keymanagerToken,
@ -801,7 +813,7 @@ proc init*(T: type BeaconNode,
node
func strictVerification(node: BeaconNode, slot: Slot) =
func verifyFinalization(node: BeaconNode, slot: Slot) =
# Epoch must be >= 4 to check finalization
const SETTLING_TIME_OFFSET = 1'u64
let epoch = slot.epoch()
@ -1376,7 +1388,7 @@ proc onSlotStart(node: BeaconNode, wallTime: BeaconTime,
wallSlot.epoch.toGaugeValue - finalizedEpoch.toGaugeValue)
if node.config.strictVerification:
strictVerification(node, wallSlot)
verifyFinalization(node, wallSlot)
node.consensusManager[].updateHead(wallSlot)

View File

@ -320,6 +320,21 @@ proc installApiHandlers*(node: SigningNode) =
validator.data.privateKey)
signature = cooked.toValidatorSig().toHex()
signatureResponse(Http200, signature)
of Web3SignerRequestKind.ValidatorRegistration:
let
forkInfo = request.forkInfo.get()
cooked = get_builder_signature(
forkInfo.fork, ValidatorRegistrationV1(
fee_recipient:
ExecutionAddress(data: distinctBase(Eth1Address.fromHex(
request.validatorRegistration.feeRecipient))),
gas_limit: request.validatorRegistration.gasLimit,
timestamp: request.validatorRegistration.timestamp,
pubkey: request.validatorRegistration.pubkey,
),
validator.data.privateKey)
signature = cooked.toValidatorSig().toHex()
signatureResponse(Http200, signature)
proc validate(key: string, value: string): int =
case key

View File

@ -819,14 +819,10 @@ template unrecognizedFieldWarning =
fieldName, typeName = typetraits.name(typeof value)
## ForkedBeaconBlock
proc readValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock](
reader: var JsonReader[RestJson],
value: var BlockType) {.raises: [IOError, SerializationError, Defect].} =
var
version: Option[BeaconBlockFork]
data: Option[JsonString]
for fieldName in readObjectFields(reader):
template prepareForkedBlockReading(
reader: var JsonReader[RestJson], value: untyped,
version: var Option[BeaconBlockFork], data: var Option[JsonString]) =
for fieldName {.inject.} in readObjectFields(reader):
case fieldName
of "version":
if version.isSome():
@ -855,6 +851,15 @@ proc readValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock](
if data.isNone():
reader.raiseUnexpectedValue("Field data is missing")
proc readValue*[BlockType: ForkedBeaconBlock](
reader: var JsonReader[RestJson],
value: var BlockType) {.raises: [IOError, SerializationError, Defect].} =
var
version: Option[BeaconBlockFork]
data: Option[JsonString]
prepareForkedBlockReading(reader, value, version, data)
case version.get():
of BeaconBlockFork.Phase0:
let res =
@ -893,6 +898,58 @@ proc readValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock](
reader.raiseUnexpectedValue("Incorrect bellatrix block format")
value = ForkedBeaconBlock.init(res.get()).BlockType
proc readValue*[BlockType: Web3SignerForkedBeaconBlock](
reader: var JsonReader[RestJson],
value: var BlockType) {.raises: [IOError, SerializationError, Defect].} =
var
version: Option[BeaconBlockFork]
data: Option[JsonString]
prepareForkedBlockReading(reader, value, version, data)
case version.get():
of BeaconBlockFork.Phase0:
let res =
try:
some(RestJson.decode(string(data.get()),
phase0.BeaconBlock,
requireAllFields = true,
allowUnknownFields = true))
except SerializationError:
none[phase0.BeaconBlock]()
if res.isNone():
reader.raiseUnexpectedValue("Incorrect phase0 block format")
value = Web3SignerForkedBeaconBlock(
kind: BeaconBlockFork.Phase0,
phase0Data: res.get())
of BeaconBlockFork.Altair:
let res =
try:
some(RestJson.decode(string(data.get()),
altair.BeaconBlock,
requireAllFields = true,
allowUnknownFields = true))
except SerializationError:
none[altair.BeaconBlock]()
if res.isNone():
reader.raiseUnexpectedValue("Incorrect altair block format")
value = Web3SignerForkedBeaconBlock(
kind: BeaconBlockFork.Altair,
altairData: res.get())
of BeaconBlockFork.Bellatrix:
let res =
try:
some(RestJson.decode(string(data.get()),
BeaconBlockHeader,
requireAllFields = true,
allowUnknownFields = true))
except SerializationError:
none[BeaconBlockHeader]()
if res.isNone():
reader.raiseUnexpectedValue("Incorrect bellatrix block format")
value = Web3SignerForkedBeaconBlock(
kind: BeaconBlockFork.Bellatrix,
bellatrixData: res.get())
proc writeValue*[BlockType: Web3SignerForkedBeaconBlock|ForkedBeaconBlock](
writer: var JsonWriter[RestJson],
@ -1441,6 +1498,9 @@ proc writeValue*(writer: var JsonWriter[RestJson],
writer.writeField("fork_info", value.forkInfo.get())
if isSome(value.signingRoot):
writer.writeField("signingRoot", value.signingRoot)
# https://github.com/ConsenSys/web3signer/blob/41834a927088f1bde7a097e17d19e954d0058e54/core/src/main/resources/openapi-specs/eth2/signing/schemas.yaml#L421-L425 (branch v22.7.0)
# It's the "beacon_block" field even when it's not a block, but a header
writer.writeField("beacon_block", value.beaconBlock)
of Web3SignerRequestKind.Deposit:
writer.writeField("type", "DEPOSIT")
@ -1489,6 +1549,15 @@ proc writeValue*(writer: var JsonWriter[RestJson],
writer.writeField("signingRoot", value.signingRoot)
writer.writeField("contribution_and_proof",
value.syncCommitteeContributionAndProof)
of Web3SignerRequestKind.ValidatorRegistration:
# https://consensys.github.io/web3signer/web3signer-eth2.html#operation/ETH2_SIGN
doAssert(value.forkInfo.isSome(),
"forkInfo should be set for this type of request")
writer.writeField("type", "VALIDATOR_REGISTRATION")
writer.writeField("fork_info", value.forkInfo.get())
if isSome(value.signingRoot):
writer.writeField("signingRoot", value.signingRoot)
writer.writeField("validator_registration", value.validatorRegistration)
writer.endRecord()
proc readValue*(reader: var JsonReader[RestJson],
@ -1532,6 +1601,8 @@ proc readValue*(reader: var JsonReader[RestJson],
Web3SignerRequestKind.SyncCommitteeSelectionProof
of "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF":
Web3SignerRequestKind.SyncCommitteeContributionAndProof
of "VALIDATOR_REGISTRATION":
Web3SignerRequestKind.ValidatorRegistration
else:
reader.raiseUnexpectedValue("Unexpected `type` value")
)
@ -1625,6 +1696,8 @@ proc readValue*(reader: var JsonReader[RestJson],
forkInfo: forkInfo, signingRoot: signingRoot, blck: data
)
of Web3SignerRequestKind.BlockV2:
# https://github.com/ConsenSys/web3signer/blob/41834a927088f1bde7a097e17d19e954d0058e54/core/src/main/resources/openapi-specs/eth2/signing/schemas.yaml#L421-L425 (branch v22.7.0)
# It's the "beacon_block" field even when it's not a block, but a header
if dataName != "beacon_block":
reader.raiseUnexpectedValue("Field `beacon_block` is missing")
if forkInfo.isNone():
@ -1740,6 +1813,25 @@ proc readValue*(reader: var JsonReader[RestJson],
forkInfo: forkInfo, signingRoot: signingRoot,
syncCommitteeContributionAndProof: data
)
of Web3SignerRequestKind.ValidatorRegistration:
if dataName != "validator_registration":
reader.raiseUnexpectedValue(
"Field `validator_registration` is missing")
if forkInfo.isNone():
reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data =
block:
let res =
decodeJsonString(Web3SignerValidatorRegistration, data.get())
if res.isErr():
reader.raiseUnexpectedValue(
"Incorrect field `validator_registration` format")
res.get()
Web3SignerRequest(
kind: Web3SignerRequestKind.ValidatorRegistration,
forkInfo: forkInfo, signingRoot: signingRoot,
validatorRegistration: data
)
## RemoteKeystoreStatus
proc writeValue*(writer: var JsonWriter[RestJson],

View File

@ -486,10 +486,20 @@ type
serializedFieldName: "beacon_block_root".}: Eth2Digest
slot*: Slot
# https://consensys.github.io/web3signer/web3signer-eth2.html#operation/ETH2_SIGN
Web3SignerValidatorRegistration* = object
feeRecipient* {.
serializedFieldName: "fee_recipient".}: string
gasLimit* {.
serializedFieldName: "gas_limit".}: uint64
timestamp*: uint64
pubkey*: ValidatorPubKey
Web3SignerRequestKind* {.pure.} = enum
AggregationSlot, AggregateAndProof, Attestation, Block, BlockV2,
Deposit, RandaoReveal, VoluntaryExit, SyncCommitteeMessage,
SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof
SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof,
ValidatorRegistration
Web3SignerRequest* = object
signingRoot*: Option[Eth2Digest]
@ -528,6 +538,10 @@ type
of Web3SignerRequestKind.SyncCommitteeContributionAndProof:
syncCommitteeContributionAndProof* {.
serializedFieldName: "contribution_and_proof".}: ContributionAndProof
of Web3SignerRequestKind.ValidatorRegistration:
validatorRegistration* {.
serializedFieldName: "validator_registration".}:
Web3SignerValidatorRegistration
GetBlockResponse* = DataEnclosedObject[phase0.SignedBeaconBlock]
GetStateResponse* = DataEnclosedObject[phase0.BeaconState]
@ -774,6 +788,26 @@ func init*(t: typedesc[Web3SignerRequest], fork: Fork,
syncCommitteeContributionAndProof: data
)
from stew/byteutils import to0xHex
func init*(t: typedesc[Web3SignerRequest], fork: Fork,
genesis_validators_root: Eth2Digest,
data: ValidatorRegistrationV1,
signingRoot: Option[Eth2Digest] = none[Eth2Digest]()
): Web3SignerRequest =
Web3SignerRequest(
kind: Web3SignerRequestKind.ValidatorRegistration,
forkInfo: some(Web3SignerForkInfo(
fork: fork, genesis_validators_root: genesis_validators_root
)),
signingRoot: signingRoot,
validatorRegistration: Web3SignerValidatorRegistration(
feeRecipient: data.fee_recipient.data.to0xHex,
gasLimit: data.gas_limit,
timestamp: data.timestamp,
pubkey: data.pubkey)
)
func init*(t: typedesc[RestSyncCommitteeMessage],
slot: Slot,
beacon_block_root: Eth2Digest,

View File

@ -15,7 +15,8 @@ import
chronicles,
../extras,
"."/[block_id, eth2_merkleization, eth2_ssz_serialization, presets],
./datatypes/[phase0, altair, bellatrix]
./datatypes/[phase0, altair, bellatrix],
./mev/bellatrix_mev
export
extras, block_id, phase0, altair, bellatrix, eth2_merkleization,
@ -110,7 +111,11 @@ type
of BeaconBlockFork.Altair: altairData*: altair.BeaconBlock
of BeaconBlockFork.Bellatrix: bellatrixData*: bellatrix.BeaconBlock
Web3SignerForkedBeaconBlock* {.borrow: `.`} = distinct ForkedBeaconBlock
Web3SignerForkedBeaconBlock* = object
case kind*: BeaconBlockFork
of BeaconBlockFork.Phase0: phase0Data*: phase0.BeaconBlock
of BeaconBlockFork.Altair: altairData*: altair.BeaconBlock
of BeaconBlockFork.Bellatrix: bellatrixData*: BeaconBlockHeader
ForkedTrustedBeaconBlock* = object
case kind*: BeaconBlockFork
@ -452,11 +457,10 @@ template withBlck*(
func proposer_index*(x: ForkedBeaconBlock): uint64 =
withBlck(x): blck.proposer_index
func hash_tree_root*(x: ForkedBeaconBlock): Eth2Digest =
func hash_tree_root*(x: ForkedBeaconBlock | Web3SignerForkedBeaconBlock):
Eth2Digest =
withBlck(x): hash_tree_root(blck)
func hash_tree_root*(x: Web3SignerForkedBeaconBlock): Eth2Digest {.borrow.}
template getForkedBlockField*(
x: ForkedSignedBeaconBlock |
ForkedMsgTrustedSignedBeaconBlock |
@ -522,7 +526,7 @@ template withStateAndBlck*(
body
func toBeaconBlockHeader*(
blck: SomeForkyBeaconBlock): BeaconBlockHeader =
blck: SomeForkyBeaconBlock | BlindedBeaconBlock): BeaconBlockHeader =
## Reduce a given `BeaconBlock` to its `BeaconBlockHeader`.
BeaconBlockHeader(
slot: blck.slot,

View File

@ -26,7 +26,7 @@ type
signature*: ValidatorSig
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/builder.md#builderbid
BuilderBid = object
BuilderBid* = object
header*: ExecutionPayloadHeader
value*: UInt256
pubkey*: ValidatorPubKey
@ -67,5 +67,5 @@ const
DOMAIN_APPLICATION_BUILDER* = DomainType([byte 0x00, 0x00, 0x00, 0x01])
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#constants
EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION* = 1.Epoch
EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION* = 1
BUILDER_PROPOSAL_DELAY_TOLERANCE* = 1.seconds

View File

@ -39,5 +39,4 @@ proc submitBlindedBlock*(body: SignedBlindedBeaconBlock
proc checkBuilderStatus*(): RestPlainResponse {.
rest, endpoint: "/eth/v1/builder/status",
meth: MethodGet.}
## https://github.com/ethereum/builder-specs/blob/v0.1.0/apis/builder/status.yaml
## https://github.com/ethereum/builder-specs/blob/v0.2.0/apis/builder/status.yaml

View File

@ -320,7 +320,6 @@ proc get_contribution_and_proof_signature*(
blsSign(privkey, signing_root.data)
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/altair/validator.md#aggregation-selection
func is_sync_committee_aggregator*(signature: ValidatorSig): bool =
let
@ -336,3 +335,25 @@ proc verify_contribution_and_proof_signature*(
fork, genesis_validators_root, msg)
blsVerify(pubkey, signing_root.data, signature)
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/builder.md#signing
func compute_builder_signing_root*(
fork: Fork, msg: BuilderBid | ValidatorRegistrationV1): Eth2Digest =
# Uses genesis fork version regardless
doAssert fork.current_version == fork.previous_version
let domain = get_domain(
fork, DOMAIN_APPLICATION_BUILDER, GENESIS_EPOCH, ZERO_HASH)
compute_signing_root(msg, domain)
proc get_builder_signature*(
fork: Fork, msg: ValidatorRegistrationV1, privkey: ValidatorPrivKey):
CookedSig =
let signing_root = compute_builder_signing_root(fork, msg)
blsSign(privkey, signing_root.data)
proc verify_builder_signature*(
fork: Fork, msg: BuilderBid,
pubkey: ValidatorPubKey | CookedPubKey, signature: SomeSig): bool =
let signing_root = compute_builder_signing_root(fork, msg)
blsVerify(pubkey, signing_root.data, signature)

View File

@ -518,7 +518,10 @@ proc makeBeaconBlock*(
# TODO:
# `verificationFlags` is needed only in tests and can be
# removed if we don't use invalid signatures there
verificationFlags: UpdateFlags = {}): Result[ForkedBeaconBlock, cstring] =
verificationFlags: UpdateFlags = {},
transactions_root: Opt[Eth2Digest] = Opt.none Eth2Digest,
execution_payload_root: Opt[Eth2Digest] = Opt.none Eth2Digest):
Result[ForkedBeaconBlock, cstring] =
## Create a block for the given state. The latest block applied to it will
## be used for the parent_root value, and the slot will be take from
## state.slot meaning process_slots must be called up to the slot for which
@ -540,6 +543,30 @@ proc makeBeaconBlock*(
rollback(state)
return err(res.error())
# Override for MEV
if transactions_root.isSome and execution_payload_root.isSome:
withState(state):
static: doAssert high(BeaconStateFork) == BeaconStateFork.Bellatrix
when stateFork == BeaconStateFork.Bellatrix:
state.data.latest_execution_payload_header.transactions_root =
transactions_root.get
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/beacon-chain.md#beaconblockbody
# Effectively hash_tree_root(ExecutionPayload) with the beacon block
# body, with the execution payload replaced by the execution payload
# header. htr(payload) == htr(payload header), so substitute.
state.data.latest_block_header.body_root = hash_tree_root(
[hash_tree_root(randao_reveal),
hash_tree_root(eth1_data),
hash_tree_root(graffiti),
hash_tree_root(exits.proposer_slashings),
hash_tree_root(exits.attester_slashings),
hash_tree_root(List[Attestation, Limit MAX_ATTESTATIONS](attestations)),
hash_tree_root(List[Deposit, Limit MAX_DEPOSITS](deposits)),
hash_tree_root(exits.voluntary_exits),
hash_tree_root(sync_aggregate),
execution_payload_root.get])
state.`kind Data`.root = hash_tree_root(state.`kind Data`.data)
blck.`kind Data`.state_root = state.`kind Data`.root

View File

@ -863,7 +863,7 @@ func process_registry_updates*(
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/beacon-chain.md#slashings
func get_adjusted_total_slashing_balance*(
state: ForkyBeaconState, total_balance: Gwei): Gwei =
let multiplier =
const multiplier =
# tradeoff here about interleaving phase0/altair, but for these
# single-constant changes...
when state is phase0.BeaconState:

View File

@ -44,11 +44,10 @@ import
../sszdump, ../sync/sync_manager,
../gossip_processing/block_processor,
".."/[conf, beacon_clock, beacon_node],
"."/[slashing_protection, validator_pool, keystore_management]
"."/[slashing_protection, validator_pool, keystore_management],
".."/spec/mev/rest_bellatrix_mev_calls
from eth/async_utils import awaitWithTimeout
from web3/engine_api import ForkchoiceUpdatedResponse
from web3/engine_api_types import PayloadExecutionStatus
# Metrics for tracking attestation and beacon block loss
const delayBuckets = [-Inf, -4.0, -2.0, -1.0, -0.5, -0.1, -0.05,
@ -308,6 +307,8 @@ proc getBlockProposalEth1Data*(node: BeaconNode,
state, finalizedEpochRef.eth1_data,
finalizedEpochRef.eth1_deposit_index)
from web3/engine_api import ForkchoiceUpdatedResponse
# TODO: This copies the entire BeaconState on each call
proc forkchoice_updated(state: bellatrix.BeaconState,
head_block_hash: Eth2Digest,
@ -343,6 +344,8 @@ proc get_execution_payload(
asConsensusExecutionPayload(
await execution_engine.getPayload(payload_id.get))
from web3/engine_api_types import PayloadExecutionStatus
proc getExecutionPayload(
node: BeaconNode, proposalState: auto,
epoch: Epoch,
@ -413,12 +416,14 @@ proc getExecutionPayload(
msg = err.msg
return empty_execution_payload
proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode,
randao_reveal: ValidatorSig,
validator_index: ValidatorIndex,
graffiti: GraffitiBytes,
head: BlockRef, slot: Slot
): Future[ForkedBlockResult] {.async.} =
proc makeBeaconBlockForHeadAndSlot*(
node: BeaconNode, randao_reveal: ValidatorSig,
validator_index: ValidatorIndex, graffiti: GraffitiBytes, head: BlockRef,
slot: Slot,
execution_payload: Opt[ExecutionPayload] = Opt.none(ExecutionPayload),
transactions_root: Opt[Eth2Digest] = Opt.none(Eth2Digest),
execution_payload_root: Opt[Eth2Digest] = Opt.none(Eth2Digest)):
Future[ForkedBlockResult] {.async.} =
# Advance state to the slot that we're proposing for
let
proposalState = assignClone(node.dag.headState)
@ -461,12 +466,15 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode,
SyncAggregate.init()
else:
node.syncCommitteeMsgPool[].produceSyncAggregate(head.root),
if slot.epoch < node.dag.cfg.BELLATRIX_FORK_EPOCH or
not (
is_merge_transition_complete(proposalState.bellatrixData.data) or
((not node.eth1Monitor.isNil) and
node.eth1Monitor.terminalBlockHash.isSome)):
default(bellatrix.ExecutionPayload)
if executionPayload.isSome:
executionPayload.get
elif slot.epoch < node.dag.cfg.BELLATRIX_FORK_EPOCH or
not (
is_merge_transition_complete(proposalState.bellatrixData.data) or
((not node.eth1Monitor.isNil) and
node.eth1Monitor.terminalBlockHash.isSome)):
# https://github.com/nim-lang/Nim/issues/19802
(static(default(bellatrix.ExecutionPayload)))
else:
let pubkey = node.dag.validatorKey(validator_index)
(await getExecutionPayload(
@ -475,7 +483,17 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode,
# TODO https://github.com/nim-lang/Nim/issues/19802
if pubkey.isSome: pubkey.get.toPubKey else: default(ValidatorPubKey))),
noRollback, # Temporary state - no need for rollback
cache)
cache,
transactions_root =
if transactions_root.isSome:
Opt.some transactions_root.get
else:
Opt.none(Eth2Digest),
execution_payload_root =
if execution_payload_root.isSome:
Opt.some execution_payload_root.get
else:
Opt.none Eth2Digest)
if res.isErr():
# This is almost certainly a bug, but it's complex enough that there's a
# small risk it might happen even when most proposals succeed - thus we
@ -489,6 +507,226 @@ proc makeBeaconBlockForHeadAndSlot*(node: BeaconNode,
head = shortLog(head),
slot
proc getBlindedExecutionPayload(
node: BeaconNode, slot: Slot, executionBlockRoot: Eth2Digest,
pubkey: ValidatorPubKey):
Future[Result[ExecutionPayloadHeader, cstring]] {.async.} =
if node.payloadBuilderRestClient.isNil:
return err "getBlindedBeaconBlock: nil REST client"
let blindedHeader = await node.payloadBuilderRestClient.getHeader(
slot, executionBlockRoot, pubkey)
const httpOk = 200
if blindedHeader.status != httpOk:
return err "getBlindedExecutionPayload: non-200 HTTP response"
else:
if not verify_builder_signature(
node.dag.cfg.genesisFork, blindedHeader.data.data.message,
blindedHeader.data.data.message.pubkey,
blindedHeader.data.data.signature):
return err "getBlindedExecutionPayload: signature verification failed"
return ok blindedHeader.data.data.message.header
import std/macros
func getFieldNames(x: typedesc[auto]): seq[string] {.compileTime.} =
var res: seq[string]
for name, _ in fieldPairs(default(x)):
res.add name
res
macro copyFields(
dst: untyped, src: untyped, fieldNames: static[seq[string]]): untyped =
result = newStmtList()
for name in fieldNames:
if name notin [
# These fields are the ones which vary between the blinded and
# unblinded objects, and can't simply be copied.
"transactions_root", "execution_payload",
"execution_payload_header", "body"]:
result.add newAssignment(
newDotExpr(dst, ident(name)), newDotExpr(src, ident(name)))
proc getBlindedBeaconBlock(
node: BeaconNode, slot: Slot, head: BlockRef, validator: AttachedValidator,
validator_index: ValidatorIndex, forkedBlock: ForkedBeaconBlock,
executionPayloadHeader: ExecutionPayloadHeader):
Future[Result[SignedBlindedBeaconBlock, string]] {.async.} =
const
blckFields = getFieldNames(typeof(forkedBlock.bellatrixData))
blckBodyFields = getFieldNames(typeof(forkedBlock.bellatrixData.body))
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal
var blindedBlock: SignedBlindedBeaconBlock
copyFields(blindedBlock.message, forkedBlock.bellatrixData, blckFields)
copyFields(
blindedBlock.message.body, forkedBlock.bellatrixData.body, blckBodyFields)
blindedBlock.message.body.execution_payload_header = executionPayloadHeader
# Check with slashing protection before submitBlindedBlock
let
fork = node.dag.forkAtEpoch(slot.epoch)
genesis_validators_root = node.dag.genesis_validators_root
blockRoot = hash_tree_root(blindedBlock.message)
signing_root = compute_block_signing_root(
fork, genesis_validators_root, slot, blockRoot)
notSlashable = node.attachedValidators
.slashingProtection
.registerBlock(validator_index, validator.pubkey, slot, signing_root)
if notSlashable.isErr:
warn "Slashing protection activated for MEV block",
validator = validator.pubkey,
slot = slot,
existingProposal = notSlashable.error
return err("MEV proposal would be slashable: " & $notSlashable.error)
blindedBlock.signature =
block:
let res = await validator.getBlockSignature(
fork, genesis_validators_root, slot, blockRoot, blindedBlock.message)
if res.isErr():
return err("Unable to sign block: " & res.error())
res.get()
return ok blindedBlock
proc proposeBlockMEV(
node: BeaconNode, head: BlockRef, validator: AttachedValidator, slot: Slot,
randao: ValidatorSig, validator_index: ValidatorIndex):
Future[Opt[BlockRef]] {.async.} =
let
executionBlockRoot = node.dag.loadExecutionBlockRoot(head)
executionPayloadHeader = awaitWithTimeout(
node.getBlindedExecutionPayload(
slot, executionBlockRoot, validator.pubkey),
BUILDER_PROPOSAL_DELAY_TOLERANCE):
Result[ExecutionPayloadHeader, cstring].err(
"getBlindedExecutionPayload timed out")
if executionPayloadHeader.isErr:
debug "proposeBlockMEV: getBlindedExecutionPayload failed",
error = executionPayloadHeader.error
# Haven't committed to the MEV block, so allow EL fallback.
return Opt.none BlockRef
# When creating this block, need to ensure it uses the MEV-provided execution
# payload, both to avoid repeated calls to network services and to ensure the
# consistency of this block (e.g., its state root being correct). Since block
# processing does not work directly using blinded blocks, fix up transactions
# root after running the state transition function on an otherwise equivalent
# non-blinded block without transactions.
var shimExecutionPayload: ExecutionPayload
copyFields(
shimExecutionPayload, executionPayloadHeader.get,
getFieldNames(ExecutionPayloadHeader))
let newBlock = await makeBeaconBlockForHeadAndSlot(
node, randao, validator_index, node.graffitiBytes, head, slot,
execution_payload = Opt.some shimExecutionPayload,
transactions_root = Opt.some executionPayloadHeader.get.transactions_root,
execution_payload_root =
Opt.some hash_tree_root(executionPayloadHeader.get))
if newBlock.isErr():
# Haven't committed to the MEV block, so allow EL fallback.
return Opt.none BlockRef # already logged elsewhere!
let forkedBlck = newBlock.get()
# This is only substantively asynchronous with a remote key signer
let blindedBlock = awaitWithTimeout(
node.getBlindedBeaconBlock(
slot, head, validator, validator_index, forkedBlck,
executionPayloadHeader.get),
500.milliseconds):
Result[SignedBlindedBeaconBlock, string].err "getBlindedBlock timed out"
if blindedBlock.isOk:
# By time submitBlindedBlock is called, must already have done slashing
# protection check
let unblindedPayload =
try:
await node.payloadBuilderRestClient.submitBlindedBlock(
blindedBlock.get)
# From here on, including error paths, disallow local EL production by
# returning Opt.some, regardless of whether on head or newBlock.
except RestDecodingError as exc:
info "proposeBlockMEV: REST recoding error",
slot, head = shortLog(head), validator_index, blindedBlock,
error = exc.msg
return Opt.some head
except CatchableError as exc:
info "proposeBlockMEV: exception in submitBlindedBlock",
slot, head = shortLog(head), validator_index, blindedBlock,
error = exc.msg
return Opt.some head
const httpOk = 200
if unblindedPayload.status == httpOk:
if hash_tree_root(
blindedBlock.get.message.body.execution_payload_header) !=
hash_tree_root(unblindedPayload.data.data):
debug "proposeBlockMEV: unblinded payload doesn't match blinded payload",
blindedPayload =
blindedBlock.get.message.body.execution_payload_header
else:
# Signature provided is consistent with unblinded execution payload,
# so construct full beacon block
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#block-proposal
var signedBlock = bellatrix.SignedBeaconBlock(
signature: blindedBlock.get.signature)
copyFields(
signedBlock.message, blindedBlock.get.message,
getFieldNames(typeof(signedBlock.message)))
copyFields(
signedBlock.message.body, blindedBlock.get.message.body,
getFieldNames(typeof(signedBlock.message.body)))
signedBlock.message.body.execution_payload = unblindedPayload.data.data
signedBlock.root = hash_tree_root(signedBlock.message)
doAssert signedBlock.root == hash_tree_root(blindedBlock.get.message)
debug "proposeBlockMEV: proposing unblinded block",
blck = shortLog(signedBlock)
let newBlockRef =
(await node.router.routeSignedBeaconBlock(signedBlock)).valueOr:
# submitBlindedBlock has run, so don't allow fallback to run
return Opt.some head # Errors logged in router
if newBlockRef.isNone():
return Opt.some head # Validation errors logged in router
notice "Block proposed (MEV)",
blockRoot = shortLog(signedBlock.root), blck = shortLog(signedBlock),
signature = shortLog(signedBlock.signature), validator = shortLog(validator)
beacon_blocks_proposed.inc()
return Opt.some newBlockRef.get()
else:
debug "proposeBlockMEV: submitBlindedBlock failed",
slot, head = shortLog(head), validator_index, blindedBlock,
payloadStatus = unblindedPayload.status
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#proposer-slashing
# This means if a validator publishes a signature for a
# `BlindedBeaconBlock` (via a dissemination of a
# `SignedBlindedBeaconBlock`) then the validator **MUST** not use the
# local build process as a fallback, even in the event of some failure
# with the external buildernetwork.
return Opt.some head
else:
info "proposeBlockMEV: getBlindedBeaconBlock failed",
slot, head = shortLog(head), validator_index, blindedBlock,
error = blindedBlock.error
return Opt.none BlockRef
proc proposeBlock(node: BeaconNode,
validator: AttachedValidator,
validator_index: ValidatorIndex,
@ -516,8 +754,23 @@ proc proposeBlock(node: BeaconNode,
return head
res.get()
newBlock = await makeBeaconBlockForHeadAndSlot(
node, randao, validator_index, node.graffitiBytes, head, slot)
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#responsibilites-during-the-merge-transition
# "Honest validators will not utilize the external builder network until
# after the transition from the proof-of-work chain to the proof-of-stake
# beacon chain has been finalized by the proof-of-stake validators."
if node.config.payloadBuilderEnable and
not node.dag.loadExecutionBlockRoot(node.dag.finalizedHead.blck).isZero:
let newBlockMEV = await node.proposeBlockMEV(
head, validator, slot, randao, validator_index)
if newBlockMEV.isSome:
# This might be equivalent to the `head` passed in, but it signals that
# `submitBlindedBlock` ran, so don't do anything else. Otherwise, it is
# fine to try again with the local EL.
return newBlockMEV.get
let newBlock = await makeBeaconBlockForHeadAndSlot(
node, randao, validator_index, node.graffitiBytes, head, slot)
if newBlock.isErr():
return head # already logged elsewhere!
@ -918,6 +1171,80 @@ proc updateValidatorMetrics*(node: BeaconNode) =
node.attachedValidatorBalanceTotal = total
attached_validator_balance_total.set(total.toGaugeValue)
from std/times import epochTime
proc getValidatorRegistration(
node: BeaconNode, validator: AttachedValidator):
Future[Result[SignedValidatorRegistrationV1, string]] {.async.} =
# Stand-in, reasonable default
const gasLimit = 30000000
let feeRecipient =
node.config.getSuggestedFeeRecipient(validator.pubkey).valueOr:
node.config.defaultFeeRecipient
var validatorRegistration = SignedValidatorRegistrationV1(
message: ValidatorRegistrationV1(
fee_recipient: ExecutionAddress(data: distinctBase(feeRecipient)),
gas_limit: gasLimit,
timestamp: epochTime().uint64,
pubkey: validator.pubkey))
let signature = await validator.getBuilderSignature(
node.dag.cfg.genesisFork, validatorRegistration.message)
debug "getValidatorRegistration: registering",
validatorRegistration
if signature.isErr:
return err signature.error
validatorRegistration.signature = signature.get
return ok validatorRegistration
proc registerValidators(node: BeaconNode) {.async.} =
try:
if (not node.config.payloadBuilderEnable) or
node.currentSlot.epoch < node.dag.cfg.BELLATRIX_FORK_EPOCH:
return
elif node.config.payloadBuilderEnable and
node.payloadBuilderRestClient.isNil:
warn "registerValidators: node.config.payloadBuilderEnable and node.payloadBuilderRestClient.isNil"
return
const HttpOk = 200
let restBuilderStatus = await node.payloadBuilderRestClient.checkBuilderStatus
if restBuilderStatus.status != HttpOk:
warn "registerValidators: specified builder not available",
builderUrl = node.config.payloadBuilderUrl,
builderStatus = restBuilderStatus
return
# TODO split this across slots of epoch to support larger numbers of
# validators
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#validator-registration
var validatorRegistrations: seq[SignedValidatorRegistrationV1]
for validator in node.attachedValidators[].validators.values:
let validatorRegistration =
await node.getValidatorRegistration(validator)
if validatorRegistration.isErr:
debug "registerValidators: validatorRegistration failed",
validatorRegistration
continue
validatorRegistrations.add validatorRegistration.get
let registerValidatorResult =
await node.payloadBuilderRestClient.registerValidator(
validatorRegistrations)
if HttpOk != registerValidatorResult.status:
warn "registerValidators: Couldn't register validator with MEV builder",
registerValidatorResult
except CatchableError as exc:
warn "registerValidators: exception",
error = exc.msg
proc handleValidatorDuties*(node: BeaconNode, lastSlot, slot: Slot) {.async.} =
## Perform validator duties - create blocks, vote and aggregate existing votes
if node.attachedValidators[].count == 0:
@ -980,6 +1307,13 @@ proc handleValidatorDuties*(node: BeaconNode, lastSlot, slot: Slot) {.async.} =
curSlot += 1
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/validator.md#registration-dissemination
# This specification suggests validators re-submit to builder software every
# `EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION` epochs.
if slot.is_epoch and
slot.epoch mod EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION == 0:
asyncSpawn node.registerValidators()
let
newHead = await handleProposal(node, head, slot)
didSubmitBlock = (newHead != head)

View File

@ -210,7 +210,8 @@ proc signData(v: AttachedValidator,
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/phase0/validator.md#signature
proc getBlockSignature*(v: AttachedValidator, fork: Fork,
genesis_validators_root: Eth2Digest, slot: Slot,
block_root: Eth2Digest, blck: ForkedBeaconBlock
block_root: Eth2Digest,
blck: ForkedBeaconBlock | BlindedBeaconBlock
): Future[SignatureResult] {.async.} =
return
case v.kind
@ -220,9 +221,33 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork,
fork, genesis_validators_root, slot, block_root,
v.data.privateKey).toValidatorSig())
of ValidatorKind.Remote:
let request = Web3SignerRequest.init(
fork, genesis_validators_root, blck.Web3SignerForkedBeaconBlock)
await v.signData(request)
when blck is BlindedBeaconBlock:
let request = Web3SignerRequest.init(
fork, genesis_validators_root,
Web3SignerForkedBeaconBlock(
kind: BeaconBlockFork.Bellatrix,
bellatrixData: blck.toBeaconBlockHeader))
await v.signData(request)
else:
let
web3SignerBlock =
case blck.kind
of BeaconBlockFork.Phase0:
Web3SignerForkedBeaconBlock(
kind: BeaconBlockFork.Phase0,
phase0Data: blck.phase0Data)
of BeaconBlockFork.Altair:
Web3SignerForkedBeaconBlock(
kind: BeaconBlockFork.Altair,
altairData: blck.altairData)
of BeaconBlockFork.Bellatrix:
Web3SignerForkedBeaconBlock(
kind: BeaconBlockFork.Bellatrix,
bellatrixData: blck.bellatrixData.toBeaconBlockHeader)
request = Web3SignerRequest.init(
fork, genesis_validators_root, web3SignerBlock)
await v.signData(request)
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/phase0/validator.md#aggregate-signature
proc getAttestationSignature*(v: AttachedValidator, fork: Fork,
@ -363,3 +388,17 @@ proc getSlotSignature*(v: AttachedValidator, fork: Fork,
v.slotSignature = some((slot, signature.get))
return signature
# https://github.com/ethereum/builder-specs/blob/v0.2.0/specs/builder.md#signing
proc getBuilderSignature*(v: AttachedValidator, fork: Fork,
validatorRegistration: ValidatorRegistrationV1):
Future[SignatureResult] {.async.} =
return
case v.kind
of ValidatorKind.Local:
SignatureResult.ok(get_builder_signature(
fork, validatorRegistration, v.data.privateKey).toValidatorSig())
of ValidatorKind.Remote:
let request = Web3SignerRequest.init(
fork, ZERO_HASH, validatorRegistration)
await v.signData(request)