Kiln mev boost (#3062)

## Issue Addressed

MEV boost compatibility

## Proposed Changes

See #2987

## Additional Info

This is blocked on the stabilization of a couple specs, [here](https://github.com/ethereum/beacon-APIs/pull/194) and [here](https://github.com/flashbots/mev-boost/pull/20).

Additional TODO's and outstanding questions

- [ ] MEV boost JWT Auth
- [ ] Will `builder_proposeBlindedBlock` return the revealed payload for the BN to propogate
- [ ] Should we remove `private-tx-proposals` flag and communicate BN <> VC with blinded blocks by default once these endpoints enter the beacon-API's repo? This simplifies merge transition logic. 

Co-authored-by: realbigsean <seananderson33@gmail.com>
Co-authored-by: realbigsean <sean@sigmaprime.io>
This commit is contained in:
realbigsean
2022-03-31 07:52:23 +00:00
parent 83234ee4ce
commit ea783360d3
48 changed files with 1628 additions and 644 deletions

View File

@@ -1,3 +1,4 @@
use crate::beacon_node_fallback::{AllErrored, Error as FallbackError};
use crate::{
beacon_node_fallback::{BeaconNodeFallback, RequireSynced},
graffiti_file::GraffitiFile,
@@ -10,7 +11,30 @@ use slot_clock::SlotClock;
use std::ops::Deref;
use std::sync::Arc;
use tokio::sync::mpsc;
use types::{EthSpec, PublicKeyBytes, Slot};
use types::{
BlindedPayload, BlockType, Epoch, EthSpec, ExecPayload, FullPayload, PublicKeyBytes, Slot,
};
#[derive(Debug)]
pub enum BlockError {
Recoverable(String),
Irrecoverable(String),
}
impl From<AllErrored<BlockError>> for BlockError {
fn from(e: AllErrored<BlockError>) -> Self {
if e.0.iter().any(|(_, error)| {
matches!(
error,
FallbackError::RequestFailed(BlockError::Irrecoverable(_))
)
}) {
BlockError::Irrecoverable(e.to_string())
} else {
BlockError::Recoverable(e.to_string())
}
}
}
/// Builds a `BlockService`.
pub struct BlockServiceBuilder<T, E: EthSpec> {
@@ -20,6 +44,7 @@ pub struct BlockServiceBuilder<T, E: EthSpec> {
context: Option<RuntimeContext<E>>,
graffiti: Option<Graffiti>,
graffiti_file: Option<GraffitiFile>,
private_tx_proposals: bool,
}
impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
@@ -31,6 +56,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
context: None,
graffiti: None,
graffiti_file: None,
private_tx_proposals: false,
}
}
@@ -64,6 +90,11 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
self
}
pub fn private_tx_proposals(mut self, private_tx_proposals: bool) -> Self {
self.private_tx_proposals = private_tx_proposals;
self
}
pub fn build(self) -> Result<BlockService<T, E>, String> {
Ok(BlockService {
inner: Arc::new(Inner {
@@ -81,6 +112,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
.ok_or("Cannot build BlockService without runtime_context")?,
graffiti: self.graffiti,
graffiti_file: self.graffiti_file,
private_tx_proposals: self.private_tx_proposals,
}),
})
}
@@ -94,6 +126,7 @@ pub struct Inner<T, E: EthSpec> {
context: RuntimeContext<E>,
graffiti: Option<Graffiti>,
graffiti_file: Option<GraffitiFile>,
private_tx_proposals: bool,
}
/// Attempts to produce attestations for any block producer(s) at the start of the epoch.
@@ -202,16 +235,46 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
)
}
let private_tx_proposals = self.private_tx_proposals;
let merge_slot = self
.context
.eth2_config
.spec
.bellatrix_fork_epoch
.unwrap_or_else(Epoch::max_value)
.start_slot(E::slots_per_epoch());
for validator_pubkey in proposers {
let service = self.clone();
let log = log.clone();
self.inner.context.executor.spawn(
async move {
if let Err(e) = service.publish_block(slot, validator_pubkey).await {
let publish_result = if private_tx_proposals && slot >= merge_slot {
let mut result = service.clone()
.publish_block::<BlindedPayload<E>>(slot, validator_pubkey)
.await;
match result.as_ref() {
Err(BlockError::Recoverable(e)) => {
error!(log, "Error whilst producing a blinded block, attempting to publish full block"; "error" => ?e);
result = service
.publish_block::<FullPayload<E>>(slot, validator_pubkey)
.await;
},
Err(BlockError::Irrecoverable(e)) => {
error!(log, "Error whilst producing a blinded block, cannot fallback because block was signed"; "error" => ?e);
},
_ => {},
};
result
} else {
service
.publish_block::<FullPayload<E>>(slot, validator_pubkey)
.await
};
if let Err(e) = publish_result {
crit!(
log,
"Error whilst producing block";
"message" => e
"message" => ?e
);
}
},
@@ -223,25 +286,29 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
}
/// Produce a block at the given slot for validator_pubkey
async fn publish_block(
async fn publish_block<Payload: ExecPayload<E>>(
self,
slot: Slot,
validator_pubkey: PublicKeyBytes,
) -> Result<(), String> {
) -> Result<(), BlockError> {
let log = self.context.log();
let _timer =
metrics::start_timer_vec(&metrics::BLOCK_SERVICE_TIMES, &[metrics::BEACON_BLOCK]);
let current_slot = self
.slot_clock
.now()
.ok_or("Unable to determine current slot from clock")?;
let current_slot = self.slot_clock.now().ok_or_else(|| {
BlockError::Recoverable("Unable to determine current slot from clock".to_string())
})?;
let randao_reveal = self
.validator_store
.randao_reveal(validator_pubkey, slot.epoch(E::slots_per_epoch()))
.await
.map_err(|e| format!("Unable to produce randao reveal signature: {:?}", e))?
.map_err(|e| {
BlockError::Recoverable(format!(
"Unable to produce randao reveal signature: {:?}",
e
))
})?
.into();
let graffiti = self
@@ -268,41 +335,86 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BEACON_BLOCK_HTTP_GET],
);
let block = beacon_node
.get_validator_blocks(slot, randao_reveal_ref, graffiti.as_ref())
.await
.map_err(|e| format!("Error from beacon node when producing block: {:?}", e))?
.data;
let block = match Payload::block_type() {
BlockType::Full => {
beacon_node
.get_validator_blocks::<E, Payload>(
slot,
randao_reveal_ref,
graffiti.as_ref(),
)
.await
.map_err(|e| {
BlockError::Recoverable(format!(
"Error from beacon node when producing block: {:?}",
e
))
})?
.data
}
BlockType::Blinded => {
beacon_node
.get_validator_blinded_blocks::<E, Payload>(
slot,
randao_reveal_ref,
graffiti.as_ref(),
)
.await
.map_err(|e| {
BlockError::Recoverable(format!(
"Error from beacon node when producing block: {:?}",
e
))
})?
.data
}
};
drop(get_timer);
if proposer_index != Some(block.proposer_index()) {
return Err(
return Err(BlockError::Recoverable(
"Proposer index does not match block proposer. Beacon chain re-orged"
.to_string(),
);
));
}
let signed_block = self_ref
.validator_store
.sign_block(*validator_pubkey_ref, block, current_slot)
.sign_block::<Payload>(*validator_pubkey_ref, block, current_slot)
.await
.map_err(|e| format!("Unable to sign block: {:?}", e))?;
.map_err(|e| {
BlockError::Recoverable(format!("Unable to sign block: {:?}", e))
})?;
let _post_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BEACON_BLOCK_HTTP_POST],
);
beacon_node
.post_beacon_blocks(&signed_block)
.await
.map_err(|e| {
format!("Error from beacon node when publishing block: {:?}", e)
})?;
Ok::<_, String>(signed_block)
match Payload::block_type() {
BlockType::Full => beacon_node
.post_beacon_blocks(&signed_block)
.await
.map_err(|e| {
BlockError::Irrecoverable(format!(
"Error from beacon node when publishing block: {:?}",
e
))
})?,
BlockType::Blinded => beacon_node
.post_beacon_blinded_blocks(&signed_block)
.await
.map_err(|e| {
BlockError::Irrecoverable(format!(
"Error from beacon node when publishing block: {:?}",
e
))
})?,
}
Ok::<_, BlockError>(signed_block)
})
.await
.map_err(|e| e.to_string())?;
.await?;
info!(
log,

View File

@@ -258,4 +258,12 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
immediately.")
.takes_value(false),
)
.arg(
Arg::with_name("private-tx-proposals")
.long("private-tx-proposals")
.help("If this flag is set, Lighthouse will query the Beacon Node for only block \
headers during proposals and will sign over headers. Useful for outsourcing \
execution payload construction during proposals.")
.takes_value(false),
)
}

View File

@@ -55,6 +55,7 @@ pub struct Config {
/// If true, enable functionality that monitors the network for attestations or proposals from
/// any of the validators managed by this client before starting up.
pub enable_doppelganger_protection: bool,
pub private_tx_proposals: bool,
/// A list of custom certificates that the validator client will additionally use when
/// connecting to a beacon node over SSL/TLS.
pub beacon_nodes_tls_certs: Option<Vec<PathBuf>>,
@@ -91,6 +92,7 @@ impl Default for Config {
monitoring_api: None,
enable_doppelganger_protection: false,
beacon_nodes_tls_certs: None,
private_tx_proposals: false,
}
}
}
@@ -306,6 +308,10 @@ impl Config {
config.enable_doppelganger_protection = true;
}
if cli_args.is_present("private-tx-proposals") {
config.private_tx_proposals = true;
}
Ok(config)
}
}

View File

@@ -400,6 +400,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
.runtime_context(context.service_context("block".into()))
.graffiti(config.graffiti)
.graffiti_file(config.graffiti_file.clone())
.private_tx_proposals(config.private_tx_proposals)
.build()?;
let attestation_service = AttestationServiceBuilder::new()

View File

@@ -33,9 +33,9 @@ pub enum Error {
}
/// Enumerates all messages that can be signed by a validator.
pub enum SignableMessage<'a, T: EthSpec> {
pub enum SignableMessage<'a, T: EthSpec, Payload: ExecPayload<T> = FullPayload<T>> {
RandaoReveal(Epoch),
BeaconBlock(&'a BeaconBlock<T>),
BeaconBlock(&'a BeaconBlock<T, Payload>),
AttestationData(&'a AttestationData),
SignedAggregateAndProof(&'a AggregateAndProof<T>),
SelectionProof(Slot),
@@ -47,7 +47,7 @@ pub enum SignableMessage<'a, T: EthSpec> {
SignedContributionAndProof(&'a ContributionAndProof<T>),
}
impl<'a, T: EthSpec> SignableMessage<'a, T> {
impl<'a, T: EthSpec, Payload: ExecPayload<T>> SignableMessage<'a, T, Payload> {
/// Returns the `SignedRoot` for the contained message.
///
/// The actual `SignedRoot` trait is not used since it also requires a `TreeHash` impl, which is
@@ -113,9 +113,9 @@ impl SigningContext {
impl SigningMethod {
/// Return the signature of `signable_message`, with respect to the `signing_context`.
pub async fn get_signature<T: EthSpec>(
pub async fn get_signature<T: EthSpec, Payload: ExecPayload<T>>(
&self,
signable_message: SignableMessage<'_, T>,
signable_message: SignableMessage<'_, T, Payload>,
signing_context: SigningContext,
spec: &ChainSpec,
executor: &TaskExecutor,

View File

@@ -34,7 +34,7 @@ pub struct ForkInfo {
#[derive(Debug, PartialEq, Serialize)]
#[serde(bound = "T: EthSpec", rename_all = "snake_case")]
pub enum Web3SignerObject<'a, T: EthSpec> {
pub enum Web3SignerObject<'a, T: EthSpec, Payload: ExecPayload<T>> {
AggregationSlot {
slot: Slot,
},
@@ -42,7 +42,7 @@ pub enum Web3SignerObject<'a, T: EthSpec> {
Attestation(&'a AttestationData),
BeaconBlock {
version: ForkName,
block: &'a BeaconBlock<T>,
block: &'a BeaconBlock<T, Payload>,
},
#[allow(dead_code)]
Deposit {
@@ -66,8 +66,8 @@ pub enum Web3SignerObject<'a, T: EthSpec> {
ContributionAndProof(&'a ContributionAndProof<T>),
}
impl<'a, T: EthSpec> Web3SignerObject<'a, T> {
pub fn beacon_block(block: &'a BeaconBlock<T>) -> Result<Self, Error> {
impl<'a, T: EthSpec, Payload: ExecPayload<T>> Web3SignerObject<'a, T, Payload> {
pub fn beacon_block(block: &'a BeaconBlock<T, Payload>) -> Result<Self, Error> {
let version = match block {
BeaconBlock::Base(_) => ForkName::Phase0,
BeaconBlock::Altair(_) => ForkName::Altair,
@@ -99,7 +99,7 @@ impl<'a, T: EthSpec> Web3SignerObject<'a, T> {
#[derive(Debug, PartialEq, Serialize)]
#[serde(bound = "T: EthSpec")]
pub struct SigningRequest<'a, T: EthSpec> {
pub struct SigningRequest<'a, T: EthSpec, Payload: ExecPayload<T>> {
#[serde(rename = "type")]
pub message_type: MessageType,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -107,7 +107,7 @@ pub struct SigningRequest<'a, T: EthSpec> {
#[serde(rename = "signingRoot")]
pub signing_root: Hash256,
#[serde(flatten)]
pub object: Web3SignerObject<'a, T>,
pub object: Web3SignerObject<'a, T, Payload>,
}
#[derive(Debug, PartialEq, Deserialize)]

View File

@@ -18,10 +18,11 @@ use std::sync::Arc;
use task_executor::TaskExecutor;
use types::{
attestation::Error as AttestationError, graffiti::GraffitiString, Address, AggregateAndProof,
Attestation, BeaconBlock, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork,
Graffiti, Hash256, Keypair, PublicKeyBytes, SelectionProof, Signature, SignedAggregateAndProof,
SignedBeaconBlock, SignedContributionAndProof, Slot, SyncAggregatorSelectionData,
SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId,
Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch,
EthSpec, ExecPayload, Fork, Graffiti, Hash256, Keypair, PublicKeyBytes, SelectionProof,
Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, Slot,
SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage,
SyncSelectionProof, SyncSubnetId,
};
use validator_dir::ValidatorDir;
@@ -338,7 +339,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
let signing_context = self.signing_context(Domain::Randao, signing_epoch);
let signature = signing_method
.get_signature::<E>(
.get_signature::<E, BlindedPayload<E>>(
SignableMessage::RandaoReveal(signing_epoch),
signing_context,
&self.spec,
@@ -359,12 +360,12 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
.suggested_fee_recipient(validator_pubkey)
}
pub async fn sign_block(
pub async fn sign_block<Payload: ExecPayload<E>>(
&self,
validator_pubkey: PublicKeyBytes,
block: BeaconBlock<E>,
block: BeaconBlock<E, Payload>,
current_slot: Slot,
) -> Result<SignedBeaconBlock<E>, Error> {
) -> Result<SignedBeaconBlock<E, Payload>, Error> {
// Make sure the block slot is not higher than the current slot to avoid potential attacks.
if block.slot() > current_slot {
warn!(
@@ -397,7 +398,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?;
let signature = signing_method
.get_signature(
.get_signature::<E, Payload>(
SignableMessage::BeaconBlock(&block),
signing_context,
&self.spec,
@@ -466,7 +467,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
Ok(Safe::Valid) => {
let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?;
let signature = signing_method
.get_signature::<E>(
.get_signature::<E, BlindedPayload<E>>(
SignableMessage::AttestationData(&attestation.data),
signing_context,
&self.spec,
@@ -543,7 +544,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?;
let signature = signing_method
.get_signature(
.get_signature::<E, BlindedPayload<E>>(
SignableMessage::SignedAggregateAndProof(&message),
signing_context,
&self.spec,
@@ -576,7 +577,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?;
let signature = signing_method
.get_signature::<E>(
.get_signature::<E, BlindedPayload<E>>(
SignableMessage::SelectionProof(slot),
signing_context,
&self.spec,
@@ -615,7 +616,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
};
let signature = signing_method
.get_signature::<E>(
.get_signature::<E, BlindedPayload<E>>(
SignableMessage::SyncSelectionProof(&message),
signing_context,
&self.spec,
@@ -641,7 +642,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?;
let signature = signing_method
.get_signature::<E>(
.get_signature::<E, BlindedPayload<E>>(
SignableMessage::SyncCommitteeSignature {
beacon_block_root,
slot,
@@ -686,7 +687,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
};
let signature = signing_method
.get_signature(
.get_signature::<E, BlindedPayload<E>>(
SignableMessage::SignedContributionAndProof(&message),
signing_context,
&self.spec,