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

@@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize};
use slog::{crit, debug, error, info, trace, Logger};
use slot_clock::SlotClock;
use std::collections::HashMap;
use std::convert::TryInto;
use std::future::Future;
use std::io::Write;
use std::path::PathBuf;
@@ -24,11 +25,15 @@ use tokio::{
sync::{Mutex, MutexGuard, RwLock},
time::{sleep, sleep_until, Instant},
};
use types::{ChainSpec, Epoch, ExecutionBlockHash, ProposerPreparationData, Slot};
pub use engine_api::{
http::HttpJsonRpc, json_structures, PayloadAttributes, PayloadStatusV1Status,
use types::{
BlindedPayload, BlockType, ChainSpec, Epoch, ExecPayload, ExecutionBlockHash,
ProposerPreparationData, SignedBeaconBlock, Slot,
};
use crate::engine_api::Builder;
use crate::engines::Builders;
pub use engine_api::*;
pub use engine_api::{http, http::HttpJsonRpc};
pub use payload_status::PayloadStatus;
mod engine_api;
@@ -59,6 +64,7 @@ const CONFIG_POLL_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Debug)]
pub enum Error {
NoEngines,
NoPayloadBuilder,
ApiError(ApiError),
EngineErrors(Vec<EngineError>),
NotSynced,
@@ -94,7 +100,8 @@ pub struct Proposer {
}
struct Inner {
engines: Engines<HttpJsonRpc>,
engines: Engines,
builders: Builders,
execution_engine_forkchoice_lock: Mutex<()>,
suggested_fee_recipient: Option<Address>,
proposer_preparation_data: Mutex<HashMap<u64, ProposerPreparationDataEntry>>,
@@ -108,6 +115,8 @@ struct Inner {
pub struct Config {
/// Endpoint urls for EL nodes that are running the engine api.
pub execution_endpoints: Vec<SensitiveUrl>,
/// Endpoint urls for services providing the builder api.
pub builder_endpoints: Vec<SensitiveUrl>,
/// JWT secrets for the above endpoints running the engine api.
pub secret_files: Vec<PathBuf>,
/// The default fee recipient to use on the beacon node if none if provided from
@@ -148,6 +157,7 @@ impl ExecutionLayer {
pub fn from_config(config: Config, executor: TaskExecutor, log: Logger) -> Result<Self, Error> {
let Config {
execution_endpoints: urls,
builder_endpoints: builder_urls,
mut secret_files,
suggested_fee_recipient,
jwt_id,
@@ -203,15 +213,24 @@ impl ExecutionLayer {
.collect::<Result<_, _>>()
.map_err(Error::InvalidJWTSecret)?;
let engines: Vec<Engine<_>> = urls
let engines: Vec<Engine<EngineApi>> = urls
.into_iter()
.zip(secrets.into_iter())
.map(|(url, (secret, path))| {
let id = url.to_string();
let auth = Auth::new(secret, jwt_id.clone(), jwt_version.clone());
debug!(log, "Loaded execution endpoint"; "endpoint" => %id, "jwt_path" => ?path);
let api = HttpJsonRpc::new_with_auth(url, auth)?;
Ok(Engine::new(id, api))
let api = HttpJsonRpc::<EngineApi>::new_with_auth(url, auth)?;
Ok(Engine::<EngineApi>::new(id, api))
})
.collect::<Result<_, ApiError>>()?;
let builders: Vec<Engine<BuilderApi>> = builder_urls
.into_iter()
.map(|url| {
let id = url.to_string();
let api = HttpJsonRpc::<BuilderApi>::new(url)?;
Ok(Engine::<BuilderApi>::new(id, api))
})
.collect::<Result<_, ApiError>>()?;
@@ -221,6 +240,10 @@ impl ExecutionLayer {
latest_forkchoice_state: <_>::default(),
log: log.clone(),
},
builders: Builders {
builders,
log: log.clone(),
},
execution_engine_forkchoice_lock: <_>::default(),
suggested_fee_recipient,
proposer_preparation_data: Mutex::new(HashMap::new()),
@@ -237,10 +260,14 @@ impl ExecutionLayer {
}
impl ExecutionLayer {
fn engines(&self) -> &Engines<HttpJsonRpc> {
fn engines(&self) -> &Engines {
&self.inner.engines
}
fn builders(&self) -> &Builders {
&self.inner.builders
}
fn executor(&self) -> &TaskExecutor {
&self.inner.executor
}
@@ -542,14 +569,14 @@ impl ExecutionLayer {
///
/// The result will be returned from the first node that returns successfully. No more nodes
/// will be contacted.
pub async fn get_payload<T: EthSpec>(
pub async fn get_payload<T: EthSpec, Payload: ExecPayload<T>>(
&self,
parent_hash: ExecutionBlockHash,
timestamp: u64,
prev_randao: Hash256,
finalized_block_hash: ExecutionBlockHash,
proposer_index: u64,
) -> Result<ExecutionPayload<T>, Error> {
) -> Result<Payload, Error> {
let _timer = metrics::start_timer_vec(
&metrics::EXECUTION_LAYER_REQUEST_TIMES,
&[metrics::GET_PAYLOAD],
@@ -557,72 +584,121 @@ impl ExecutionLayer {
let suggested_fee_recipient = self.get_suggested_fee_recipient(proposer_index).await;
debug!(
self.log(),
"Issuing engine_getPayload";
"suggested_fee_recipient" => ?suggested_fee_recipient,
"prev_randao" => ?prev_randao,
"timestamp" => timestamp,
"parent_hash" => ?parent_hash,
);
self.engines()
.first_success(|engine| async move {
let payload_id = if let Some(id) = engine
.get_payload_id(parent_hash, timestamp, prev_randao, suggested_fee_recipient)
match Payload::block_type() {
BlockType::Blinded => {
debug!(
self.log(),
"Issuing builder_getPayloadHeader";
"suggested_fee_recipient" => ?suggested_fee_recipient,
"prev_randao" => ?prev_randao,
"timestamp" => timestamp,
"parent_hash" => ?parent_hash,
);
self.builders()
.first_success_without_retry(|engine| async move {
let payload_id = engine
.get_payload_id(
parent_hash,
timestamp,
prev_randao,
suggested_fee_recipient,
)
.await
.ok_or(ApiError::MissingPayloadId {
parent_hash,
timestamp,
prev_randao,
suggested_fee_recipient,
})?;
engine
.api
.get_payload_header_v1::<T>(payload_id)
.await?
.try_into()
.map_err(|_| ApiError::PayloadConversionLogicFlaw)
})
.await
{
// The payload id has been cached for this engine.
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID,
&[metrics::HIT],
);
id
} else {
// The payload id has *not* been cached for this engine. Trigger an artificial
// fork choice update to retrieve a payload ID.
//
// TODO(merge): a better algorithm might try to favour a node that already had a
// cached payload id, since a payload that has had more time to produce is
// likely to be more profitable.
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID,
&[metrics::MISS],
);
let fork_choice_state = ForkChoiceState {
head_block_hash: parent_hash,
safe_block_hash: parent_hash,
finalized_block_hash,
};
let payload_attributes = PayloadAttributes {
timestamp,
prev_randao,
suggested_fee_recipient,
};
engine
.notify_forkchoice_updated(
fork_choice_state,
Some(payload_attributes),
self.log(),
)
.await
.map(|response| response.payload_id)?
.ok_or_else(|| {
error!(
self.log(),
"Exec engine unable to produce payload";
"msg" => "No payload ID, the engine is likely syncing. \
This has the potential to cause a missed block proposal.",
.map_err(Error::EngineErrors)
}
BlockType::Full => {
debug!(
self.log(),
"Issuing engine_getPayload";
"suggested_fee_recipient" => ?suggested_fee_recipient,
"prev_randao" => ?prev_randao,
"timestamp" => timestamp,
"parent_hash" => ?parent_hash,
);
self.engines()
.first_success(|engine| async move {
let payload_id = if let Some(id) = engine
.get_payload_id(
parent_hash,
timestamp,
prev_randao,
suggested_fee_recipient,
)
.await
{
// The payload id has been cached for this engine.
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID,
&[metrics::HIT],
);
id
} else {
// The payload id has *not* been cached for this engine. Trigger an artificial
// fork choice update to retrieve a payload ID.
//
// TODO(merge): a better algorithm might try to favour a node that already had a
// cached payload id, since a payload that has had more time to produce is
// likely to be more profitable.
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_PRE_PREPARED_PAYLOAD_ID,
&[metrics::MISS],
);
let fork_choice_state = ForkChoiceState {
head_block_hash: parent_hash,
safe_block_hash: parent_hash,
finalized_block_hash,
};
let payload_attributes = PayloadAttributes {
timestamp,
prev_randao,
suggested_fee_recipient,
};
ApiError::PayloadIdUnavailable
})?
};
engine
.notify_forkchoice_updated(
fork_choice_state,
Some(payload_attributes),
self.log(),
)
.await
.map(|response| response.payload_id)?
.ok_or_else(|| {
error!(
self.log(),
"Exec engine unable to produce payload";
"msg" => "No payload ID, the engine is likely syncing. \
This has the potential to cause a missed block \
proposal.",
);
engine.api.get_payload_v1(payload_id).await
})
.await
.map_err(Error::EngineErrors)
ApiError::PayloadIdUnavailable
})?
};
engine
.api
.get_payload_v1::<T>(payload_id)
.await
.map(Into::into)
})
.await
.map_err(Error::EngineErrors)
}
}
}
/// Maps to the `engine_newPayload` JSON-RPC call.
@@ -801,10 +877,23 @@ impl ExecutionLayer {
})
.await;
// Only query builders with payload attributes populated.
let builder_broadcast_results = if payload_attributes.is_some() {
self.builders()
.broadcast_without_retry(|engine| async move {
engine
.notify_forkchoice_updated(forkchoice_state, payload_attributes, self.log())
.await
})
.await
} else {
vec![]
};
process_multiple_payload_statuses(
head_block_hash,
broadcast_results
.into_iter()
.chain(builder_broadcast_results.into_iter())
.map(|result| result.map(|response| response.payload_status)),
self.log(),
)
@@ -931,7 +1020,7 @@ impl ExecutionLayer {
/// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md
async fn get_pow_block_hash_at_total_difficulty(
&self,
engine: &Engine<HttpJsonRpc>,
engine: &Engine<EngineApi>,
spec: &ChainSpec,
) -> Result<Option<ExecutionBlockHash>, ApiError> {
let mut block = engine
@@ -1013,7 +1102,6 @@ impl ExecutionLayer {
));
}
}
Ok(None)
})
.await;
@@ -1076,7 +1164,7 @@ impl ExecutionLayer {
/// https://github.com/ethereum/consensus-specs/issues/2636
async fn get_pow_block(
&self,
engine: &Engine<HttpJsonRpc>,
engine: &Engine<EngineApi>,
hash: ExecutionBlockHash,
) -> Result<Option<ExecutionBlock>, ApiError> {
if let Some(cached) = self.execution_blocks().await.get(&hash).copied() {
@@ -1094,6 +1182,23 @@ impl ExecutionLayer {
Ok(None)
}
}
pub async fn propose_blinded_beacon_block<T: EthSpec>(
&self,
block: &SignedBeaconBlock<T, BlindedPayload<T>>,
) -> Result<ExecutionPayload<T>, Error> {
debug!(
self.log(),
"Issuing builder_proposeBlindedBlock";
"root" => ?block.canonical_root(),
);
self.builders()
.first_success_without_retry(|engine| async move {
engine.api.propose_blinded_block_v1(block.clone()).await
})
.await
.map_err(Error::EngineErrors)
}
}
#[cfg(test)]