Builder Specs v0.2.0 (#3134)

## Issue Addressed

https://github.com/sigp/lighthouse/issues/3091

Extends https://github.com/sigp/lighthouse/pull/3062, adding pre-bellatrix block support on blinded endpoints and allowing the normal proposal flow (local payload construction) on blinded endpoints. This resulted in better fallback logic because the VC will not have to switch endpoints on failure in the BN <> Builder API, the BN can just fallback immediately and without repeating block processing that it shouldn't need to. We can also keep VC fallback from the VC<>BN API's blinded endpoint to full endpoint.

## Proposed Changes

- Pre-bellatrix blocks on blinded endpoints
- Add a new `PayloadCache` to the execution layer
- Better fallback-from-builder logic

## Todos

- [x] Remove VC transition logic
- [x] Add logic to only enable builder flow after Merge transition finalization
- [x] Tests
- [x] Fix metrics
- [x] Rustdocs


Co-authored-by: Mac L <mjladson@pm.me>
Co-authored-by: realbigsean <sean@sigmaprime.io>
This commit is contained in:
realbigsean
2022-07-30 00:22:37 +00:00
parent 25f0e261cb
commit 6c2d8b2262
61 changed files with 3522 additions and 687 deletions

View File

@@ -11,9 +11,7 @@ use slot_clock::SlotClock;
use std::ops::Deref;
use std::sync::Arc;
use tokio::sync::mpsc;
use types::{
BlindedPayload, BlockType, Epoch, EthSpec, ExecPayload, FullPayload, PublicKeyBytes, Slot,
};
use types::{BlindedPayload, BlockType, EthSpec, ExecPayload, FullPayload, PublicKeyBytes, Slot};
#[derive(Debug)]
pub enum BlockError {
@@ -44,7 +42,6 @@ pub struct BlockServiceBuilder<T, E: EthSpec> {
context: Option<RuntimeContext<E>>,
graffiti: Option<Graffiti>,
graffiti_file: Option<GraffitiFile>,
private_tx_proposals: bool,
strict_fee_recipient: bool,
}
@@ -57,7 +54,6 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockServiceBuilder<T, E> {
context: None,
graffiti: None,
graffiti_file: None,
private_tx_proposals: false,
strict_fee_recipient: false,
}
}
@@ -92,11 +88,6 @@ 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 strict_fee_recipient(mut self, strict_fee_recipient: bool) -> Self {
self.strict_fee_recipient = strict_fee_recipient;
self
@@ -119,7 +110,6 @@ 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,
strict_fee_recipient: self.strict_fee_recipient,
}),
})
@@ -134,7 +124,6 @@ pub struct Inner<T, E: EthSpec> {
context: RuntimeContext<E>,
graffiti: Option<Graffiti>,
graffiti_file: Option<GraffitiFile>,
private_tx_proposals: bool,
strict_fee_recipient: bool,
}
@@ -244,32 +233,29 @@ 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 builder_proposals = self
.validator_store
.get_builder_proposals(&validator_pubkey);
let service = self.clone();
let log = log.clone();
self.inner.context.executor.spawn(
async move {
let publish_result = if private_tx_proposals && slot >= merge_slot {
let publish_result = if builder_proposals {
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);
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);
error!(log, "Error whilst producing a blinded block, cannot fallback \
because the block was signed"; "error" => ?e);
},
_ => {},
};
@@ -344,12 +330,12 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
let block = self
.beacon_nodes
.first_success(RequireSynced::No, |beacon_node| async move {
let get_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BEACON_BLOCK_HTTP_GET],
);
let block = match Payload::block_type() {
BlockType::Full => {
let _get_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BEACON_BLOCK_HTTP_GET],
);
beacon_node
.get_validator_blocks::<E, Payload>(
slot,
@@ -366,6 +352,10 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
.data
}
BlockType::Blinded => {
let _get_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BLINDED_BEACON_BLOCK_HTTP_GET],
);
beacon_node
.get_validator_blinded_blocks::<E, Payload>(
slot,
@@ -382,7 +372,6 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
.data
}
};
drop(get_timer);
// Ensure the correctness of the execution payload's fee recipient.
if strict_fee_recipient {
@@ -415,43 +404,51 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
// Publish block with first available beacon node.
self.beacon_nodes
.first_success(RequireSynced::No, |beacon_node| async {
let _post_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BEACON_BLOCK_HTTP_POST],
);
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
))
})?,
BlockType::Full => {
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| {
BlockError::Irrecoverable(format!(
"Error from beacon node when publishing block: {:?}",
e
))
})?
}
BlockType::Blinded => {
let _post_timer = metrics::start_timer_vec(
&metrics::BLOCK_SERVICE_TIMES,
&[metrics::BLINDED_BEACON_BLOCK_HTTP_POST],
);
beacon_node
.post_beacon_blinded_blocks(&signed_block)
.await
.map_err(|e| {
BlockError::Irrecoverable(format!(
"Error from beacon node when publishing block: {:?}",
e
))
})?
}
}
info!(
log,
"Successfully published block";
"deposits" => signed_block.message().body().deposits().len(),
"attestations" => signed_block.message().body().attestations().len(),
"graffiti" => ?graffiti.map(|g| g.as_utf8_lossy()),
"slot" => signed_block.slot().as_u64(),
);
Ok::<_, BlockError>(())
})
.await?;
info!(
log,
"Successfully published block";
"block_type" => ?Payload::block_type(),
"deposits" => signed_block.message().body().deposits().len(),
"attestations" => signed_block.message().body().attestations().len(),
"graffiti" => ?graffiti.map(|g| g.as_utf8_lossy()),
"slot" => signed_block.slot().as_u64(),
);
Ok(())
}
}

View File

@@ -251,8 +251,9 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.takes_value(false),
)
.arg(
Arg::with_name("private-tx-proposals")
.long("private-tx-proposals")
Arg::with_name("builder-proposals")
.long("builder-proposals")
.alias("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.")
@@ -271,4 +272,22 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
for payload construction, where a strict fee recipient check will still be applied.")
.takes_value(false),
)
.arg(
Arg::with_name("builder-registration-timestamp-override")
.long("builder-registration-timestamp-override")
.alias("builder-registration-timestamp-override")
.help("This flag takes a unix timestamp value that will be used to override the \
timestamp used in the builder api registration")
.takes_value(true),
)
.arg(
Arg::with_name("gas-limit")
.long("gas-limit")
.value_name("INTEGER")
.takes_value(true)
.help("The gas limit to be used in all builder proposals for all validators managed \
by this validator client. Note this will not necessarily be used if the gas limit \
set here moves too far from the previous block's gas limit. [default: 30,000,000]")
.requires("builder-proposals"),
)
}

View File

@@ -52,7 +52,12 @@ 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,
/// Enable use of the blinded block endpoints during proposals.
pub builder_proposals: bool,
/// Overrides the timestamp field in builder api ValidatorRegistrationV1
pub builder_registration_timestamp_override: Option<u64>,
/// Fallback gas limit.
pub gas_limit: Option<u64>,
/// 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,7 +96,9 @@ impl Default for Config {
monitoring_api: None,
enable_doppelganger_protection: false,
beacon_nodes_tls_certs: None,
private_tx_proposals: false,
builder_proposals: false,
builder_registration_timestamp_override: None,
gas_limit: None,
strict_fee_recipient: false,
}
}
@@ -300,8 +307,27 @@ impl Config {
config.enable_doppelganger_protection = true;
}
if cli_args.is_present("private-tx-proposals") {
config.private_tx_proposals = true;
if cli_args.is_present("builder-proposals") {
config.builder_proposals = true;
}
config.gas_limit = cli_args
.value_of("gas-limit")
.map(|gas_limit| {
gas_limit
.parse::<u64>()
.map_err(|_| "gas-limit is not a valid u64.")
})
.transpose()?;
if let Some(registration_timestamp_override) =
cli_args.value_of("builder-registration-timestamp-override")
{
config.builder_registration_timestamp_override = Some(
registration_timestamp_override
.parse::<u64>()
.map_err(|_| "builder-registration-timestamp-override is not a valid u64.")?,
);
}
if cli_args.is_present("strict-fee-recipient") {

View File

@@ -140,6 +140,8 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock,
request.enable,
request.graffiti.clone(),
request.suggested_fee_recipient,
request.gas_limit,
request.builder_proposals,
)
.await
.map_err(|e| {
@@ -154,6 +156,8 @@ pub async fn create_validators_mnemonic<P: AsRef<Path>, T: 'static + SlotClock,
description: request.description.clone(),
graffiti: request.graffiti.clone(),
suggested_fee_recipient: request.suggested_fee_recipient,
gas_limit: request.gas_limit,
builder_proposals: request.builder_proposals,
voting_pubkey,
eth1_deposit_tx_data: eth2_serde_utils::hex::encode(&eth1_deposit_data.rlp),
deposit_gwei: request.deposit_gwei,

View File

@@ -205,6 +205,8 @@ fn import_single_keystore<T: SlotClock + 'static, E: EthSpec>(
true,
None,
None,
None,
None,
))
.map_err(|e| format!("failed to initialize validator: {:?}", e))?;

View File

@@ -413,6 +413,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
let voting_password = body.password.clone();
let graffiti = body.graffiti.clone();
let suggested_fee_recipient = body.suggested_fee_recipient;
let gas_limit = body.gas_limit;
let builder_proposals = body.builder_proposals;
let validator_def = {
if let Some(handle) = task_executor.handle() {
@@ -423,6 +425,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
body.enable,
graffiti,
suggested_fee_recipient,
gas_limit,
builder_proposals,
))
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
@@ -469,6 +473,8 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
voting_public_key: web3signer.voting_public_key,
graffiti: web3signer.graffiti,
suggested_fee_recipient: web3signer.suggested_fee_recipient,
gas_limit: web3signer.gas_limit,
builder_proposals: web3signer.builder_proposals,
description: web3signer.description,
signing_definition: SigningDefinition::Web3Signer(
Web3SignerDefinition {
@@ -515,18 +521,32 @@ pub fn serve<T: 'static + SlotClock + Clone, E: EthSpec>(
let initialized_validators_rw_lock = validator_store.initialized_validators();
let mut initialized_validators = initialized_validators_rw_lock.write();
match initialized_validators.is_enabled(&validator_pubkey) {
None => Err(warp_utils::reject::custom_not_found(format!(
match (
initialized_validators.is_enabled(&validator_pubkey),
initialized_validators.validator(&validator_pubkey.compress()),
) {
(None, _) => Err(warp_utils::reject::custom_not_found(format!(
"no validator for {:?}",
validator_pubkey
))),
Some(enabled) if enabled == body.enabled => Ok(()),
Some(_) => {
(Some(is_enabled), Some(initialized_validator))
if Some(is_enabled) == body.enabled
&& initialized_validator.get_gas_limit() == body.gas_limit
&& initialized_validator.get_builder_proposals()
== body.builder_proposals =>
{
Ok(())
}
(Some(_), _) => {
if let Some(handle) = task_executor.handle() {
handle
.block_on(
initialized_validators
.set_validator_status(&validator_pubkey, body.enabled),
initialized_validators.set_validator_definition_fields(
&validator_pubkey,
body.enabled,
body.gas_limit,
body.builder_proposals,
),
)
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(

View File

@@ -123,6 +123,8 @@ fn import_single_remotekey<T: SlotClock + 'static, E: EthSpec>(
voting_public_key: pubkey,
graffiti: None,
suggested_fee_recipient: None,
gas_limit: None,
builder_proposals: None,
description: String::from("Added by remotekey API"),
signing_definition: SigningDefinition::Web3Signer(Web3SignerDefinition {
url,

View File

@@ -83,6 +83,7 @@ impl ApiTester {
let mut config = Config::default();
config.validator_dir = validator_dir.path().into();
config.secrets_dir = secrets_dir.path().into();
config.fee_recipient = Some(TEST_DEFAULT_FEE_RECIPIENT);
let spec = E::default_spec();
@@ -103,7 +104,7 @@ impl ApiTester {
spec,
Some(Arc::new(DoppelgangerService::new(log.clone()))),
slot_clock,
Some(TEST_DEFAULT_FEE_RECIPIENT),
&config,
executor.clone(),
log.clone(),
));
@@ -270,6 +271,8 @@ impl ApiTester {
description: format!("boi #{}", i),
graffiti: None,
suggested_fee_recipient: None,
gas_limit: None,
builder_proposals: None,
deposit_gwei: E::default_spec().max_effective_balance,
})
.collect::<Vec<_>>();
@@ -401,6 +404,8 @@ impl ApiTester {
keystore,
graffiti: None,
suggested_fee_recipient: None,
gas_limit: None,
builder_proposals: None,
};
self.client
@@ -419,6 +424,8 @@ impl ApiTester {
keystore,
graffiti: None,
suggested_fee_recipient: None,
gas_limit: None,
builder_proposals: None,
};
let response = self
@@ -455,6 +462,8 @@ impl ApiTester {
description: format!("{}", i),
graffiti: None,
suggested_fee_recipient: None,
gas_limit: None,
builder_proposals: None,
voting_public_key: kp.pk,
url: format!("http://signer_{}.com/", i),
root_certificate_path: None,
@@ -484,7 +493,7 @@ impl ApiTester {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
self.client
.patch_lighthouse_validators(&validator.voting_pubkey, enabled)
.patch_lighthouse_validators(&validator.voting_pubkey, Some(enabled), None, None)
.await
.unwrap();
@@ -521,6 +530,56 @@ impl ApiTester {
self
}
pub async fn set_gas_limit(self, index: usize, gas_limit: u64) -> Self {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
self.client
.patch_lighthouse_validators(&validator.voting_pubkey, None, Some(gas_limit), None)
.await
.unwrap();
self
}
pub async fn assert_gas_limit(self, index: usize, gas_limit: u64) -> Self {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
assert_eq!(
self.validator_store.get_gas_limit(&validator.voting_pubkey),
gas_limit
);
self
}
pub async fn set_builder_proposals(self, index: usize, builder_proposals: bool) -> Self {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
self.client
.patch_lighthouse_validators(
&validator.voting_pubkey,
None,
None,
Some(builder_proposals),
)
.await
.unwrap();
self
}
pub async fn assert_builder_proposals(self, index: usize, builder_proposals: bool) -> Self {
let validator = &self.client.get_lighthouse_validators().await.unwrap().data[index];
assert_eq!(
self.validator_store
.get_builder_proposals(&validator.voting_pubkey),
builder_proposals
);
self
}
}
struct HdValidatorScenario {
@@ -583,6 +642,8 @@ fn routes_with_invalid_auth() {
description: <_>::default(),
graffiti: <_>::default(),
suggested_fee_recipient: <_>::default(),
gas_limit: <_>::default(),
builder_proposals: <_>::default(),
deposit_gwei: <_>::default(),
}])
.await
@@ -612,13 +673,15 @@ fn routes_with_invalid_auth() {
keystore,
graffiti: <_>::default(),
suggested_fee_recipient: <_>::default(),
gas_limit: <_>::default(),
builder_proposals: <_>::default(),
})
.await
})
.await
.test_with_invalid_auth(|client| async move {
client
.patch_lighthouse_validators(&PublicKeyBytes::empty(), false)
.patch_lighthouse_validators(&PublicKeyBytes::empty(), Some(false), None, None)
.await
})
.await
@@ -735,6 +798,74 @@ fn validator_enabling() {
});
}
#[test]
fn validator_gas_limit() {
let runtime = build_runtime();
let weak_runtime = Arc::downgrade(&runtime);
runtime.block_on(async {
ApiTester::new(weak_runtime)
.await
.create_hd_validators(HdValidatorScenario {
count: 2,
specify_mnemonic: false,
key_derivation_path_offset: 0,
disabled: vec![],
})
.await
.assert_enabled_validators_count(2)
.assert_validators_count(2)
.set_gas_limit(0, 500)
.await
.assert_gas_limit(0, 500)
.await
// Update gas limit while validator is disabled.
.set_validator_enabled(0, false)
.await
.assert_enabled_validators_count(1)
.assert_validators_count(2)
.set_gas_limit(0, 1000)
.await
.set_validator_enabled(0, true)
.await
.assert_enabled_validators_count(2)
.assert_gas_limit(0, 1000)
.await
});
}
#[test]
fn validator_builder_proposals() {
let runtime = build_runtime();
let weak_runtime = Arc::downgrade(&runtime);
runtime.block_on(async {
ApiTester::new(weak_runtime)
.await
.create_hd_validators(HdValidatorScenario {
count: 2,
specify_mnemonic: false,
key_derivation_path_offset: 0,
disabled: vec![],
})
.await
.assert_enabled_validators_count(2)
.assert_validators_count(2)
.set_builder_proposals(0, true)
.await
// Test setting builder proposals while the validator is disabled
.set_validator_enabled(0, false)
.await
.assert_enabled_validators_count(1)
.assert_validators_count(2)
.set_builder_proposals(0, false)
.await
.set_validator_enabled(0, true)
.await
.assert_enabled_validators_count(2)
.assert_builder_proposals(0, false)
.await
});
}
#[test]
fn keystore_validator_creation() {
let runtime = build_runtime();

View File

@@ -39,6 +39,8 @@ fn web3signer_validator_with_pubkey(pubkey: PublicKey) -> Web3SignerValidatorReq
description: "".into(),
graffiti: None,
suggested_fee_recipient: None,
gas_limit: None,
builder_proposals: None,
voting_public_key: pubkey,
url: web3_signer_url(),
root_certificate_path: None,
@@ -465,7 +467,7 @@ fn import_and_delete_conflicting_web3_signer_keystores() {
for pubkey in &pubkeys {
tester
.client
.patch_lighthouse_validators(pubkey, false)
.patch_lighthouse_validators(pubkey, Some(false), None, None)
.await
.unwrap();
}

View File

@@ -11,7 +11,9 @@ pub const UNREGISTERED: &str = "unregistered";
pub const FULL_UPDATE: &str = "full_update";
pub const BEACON_BLOCK: &str = "beacon_block";
pub const BEACON_BLOCK_HTTP_GET: &str = "beacon_block_http_get";
pub const BLINDED_BEACON_BLOCK_HTTP_GET: &str = "blinded_beacon_block_http_get";
pub const BEACON_BLOCK_HTTP_POST: &str = "beacon_block_http_post";
pub const BLINDED_BEACON_BLOCK_HTTP_POST: &str = "blinded_beacon_block_http_post";
pub const ATTESTATIONS: &str = "attestations";
pub const ATTESTATIONS_HTTP_GET: &str = "attestations_http_get";
pub const ATTESTATIONS_HTTP_POST: &str = "attestations_http_post";

View File

@@ -110,6 +110,8 @@ pub struct InitializedValidator {
signing_method: Arc<SigningMethod>,
graffiti: Option<Graffiti>,
suggested_fee_recipient: Option<Address>,
gas_limit: Option<u64>,
builder_proposals: Option<bool>,
/// The validators index in `state.validators`, to be updated by an external service.
index: Option<u64>,
}
@@ -129,6 +131,22 @@ impl InitializedValidator {
SigningMethod::Web3Signer { .. } => None,
}
}
pub fn get_suggested_fee_recipient(&self) -> Option<Address> {
self.suggested_fee_recipient
}
pub fn get_gas_limit(&self) -> Option<u64> {
self.gas_limit
}
pub fn get_builder_proposals(&self) -> Option<bool> {
self.builder_proposals
}
pub fn get_index(&self) -> Option<u64> {
self.index
}
}
fn open_keystore(path: &Path) -> Result<Keystore, Error> {
@@ -292,6 +310,8 @@ impl InitializedValidator {
signing_method: Arc::new(signing_method),
graffiti: def.graffiti.map(Into::into),
suggested_fee_recipient: def.suggested_fee_recipient,
gas_limit: def.gas_limit,
builder_proposals: def.builder_proposals,
index: None,
})
}
@@ -622,7 +642,28 @@ impl InitializedValidators {
.and_then(|v| v.suggested_fee_recipient)
}
/// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled` values.
/// Returns the `gas_limit` for a given public key specified in the
/// `ValidatorDefinitions`.
pub fn gas_limit(&self, public_key: &PublicKeyBytes) -> Option<u64> {
self.validators.get(public_key).and_then(|v| v.gas_limit)
}
/// Returns the `builder_proposals` for a given public key specified in the
/// `ValidatorDefinitions`.
pub fn builder_proposals(&self, public_key: &PublicKeyBytes) -> Option<bool> {
self.validators
.get(public_key)
.and_then(|v| v.builder_proposals)
}
/// Returns an `Option` of a reference to an `InitializedValidator` for a given public key specified in the
/// `ValidatorDefinitions`.
pub fn validator(&self, public_key: &PublicKeyBytes) -> Option<&InitializedValidator> {
self.validators.get(public_key)
}
/// Sets the `InitializedValidator` and `ValidatorDefinition` `enabled`, `gas_limit`, and `builder_proposals`
/// values.
///
/// ## Notes
///
@@ -630,11 +671,17 @@ impl InitializedValidators {
/// disk. A newly enabled validator will be added to `self.validators`, whilst a newly disabled
/// validator will be removed from `self.validators`.
///
/// If a `gas_limit` is included in the call to this function, it will also be updated and saved
/// to disk. If `gas_limit` is `None` the `gas_limit` *will not* be unset in `ValidatorDefinition`
/// or `InitializedValidator`. The same logic applies to `builder_proposals`.
///
/// Saves the `ValidatorDefinitions` to file, even if no definitions were changed.
pub async fn set_validator_status(
pub async fn set_validator_definition_fields(
&mut self,
voting_public_key: &PublicKey,
enabled: bool,
enabled: Option<bool>,
gas_limit: Option<u64>,
builder_proposals: Option<bool>,
) -> Result<(), Error> {
if let Some(def) = self
.definitions
@@ -642,11 +689,33 @@ impl InitializedValidators {
.iter_mut()
.find(|def| def.voting_public_key == *voting_public_key)
{
def.enabled = enabled;
// Don't overwrite fields if they are not set in this request.
if let Some(enabled) = enabled {
def.enabled = enabled;
}
if let Some(gas_limit) = gas_limit {
def.gas_limit = Some(gas_limit);
}
if let Some(builder_proposals) = builder_proposals {
def.builder_proposals = Some(builder_proposals);
}
}
self.update_validators().await?;
if let Some(val) = self
.validators
.get_mut(&PublicKeyBytes::from(voting_public_key))
{
// Don't overwrite fields if they are not set in this request.
if let Some(gas_limit) = gas_limit {
val.gas_limit = Some(gas_limit);
}
if let Some(builder_proposals) = builder_proposals {
val.builder_proposals = Some(builder_proposals);
}
}
self.definitions
.save(&self.validators_dir)
.map_err(Error::UnableToSaveDefinitions)?;

View File

@@ -362,7 +362,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
context.eth2_config.spec.clone(),
doppelganger_service.clone(),
slot_clock.clone(),
config.fee_recipient,
&config,
context.executor.clone(),
log.clone(),
));
@@ -413,7 +413,6 @@ 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)
.strict_fee_recipient(config.strict_fee_recipient)
.build()?;
@@ -430,6 +429,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
.validator_store(validator_store.clone())
.beacon_nodes(beacon_nodes.clone())
.runtime_context(context.service_context("preparation".into()))
.builder_registration_timestamp_override(config.builder_registration_timestamp_override)
.build()?;
let sync_committee_service = SyncCommitteeService::new(
@@ -487,10 +487,7 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
self.preparation_service
.clone()
.start_update_service(
self.config.private_tx_proposals,
&self.context.eth2_config.spec,
)
.start_update_service(&self.context.eth2_config.spec)
.map_err(|e| format!("Unable to start preparation service: {}", e))?;
if let Some(doppelganger_service) = self.doppelganger_service.clone() {

View File

@@ -22,12 +22,16 @@ const PROPOSER_PREPARATION_LOOKAHEAD_EPOCHS: u64 = 2;
/// Number of epochs to wait before re-submitting validator registration.
const EPOCHS_PER_VALIDATOR_REGISTRATION_SUBMISSION: u64 = 1;
/// The number of validator registrations to include per request to the beacon node.
const VALIDATOR_REGISTRATION_BATCH_SIZE: usize = 500;
/// Builds an `PreparationService`.
pub struct PreparationServiceBuilder<T: SlotClock + 'static, E: EthSpec> {
validator_store: Option<Arc<ValidatorStore<T, E>>>,
slot_clock: Option<T>,
beacon_nodes: Option<Arc<BeaconNodeFallback<T, E>>>,
context: Option<RuntimeContext<E>>,
builder_registration_timestamp_override: Option<u64>,
}
impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> {
@@ -37,6 +41,7 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> {
slot_clock: None,
beacon_nodes: None,
context: None,
builder_registration_timestamp_override: None,
}
}
@@ -60,6 +65,14 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> {
self
}
pub fn builder_registration_timestamp_override(
mut self,
builder_registration_timestamp_override: Option<u64>,
) -> Self {
self.builder_registration_timestamp_override = builder_registration_timestamp_override;
self
}
pub fn build(self) -> Result<PreparationService<T, E>, String> {
Ok(PreparationService {
inner: Arc::new(Inner {
@@ -75,6 +88,8 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationServiceBuilder<T, E> {
context: self
.context
.ok_or("Cannot build PreparationService without runtime_context")?,
builder_registration_timestamp_override: self
.builder_registration_timestamp_override,
validator_registration_cache: RwLock::new(HashMap::new()),
}),
})
@@ -87,6 +102,7 @@ pub struct Inner<T, E: EthSpec> {
slot_clock: T,
beacon_nodes: Arc<BeaconNodeFallback<T, E>>,
context: RuntimeContext<E>,
builder_registration_timestamp_override: Option<u64>,
// Used to track unpublished validator registration changes.
validator_registration_cache:
RwLock<HashMap<ValidatorRegistrationKey, SignedValidatorRegistrationData>>,
@@ -137,14 +153,8 @@ impl<T, E: EthSpec> Deref for PreparationService<T, E> {
}
impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> {
pub fn start_update_service(
self,
start_registration_service: bool,
spec: &ChainSpec,
) -> Result<(), String> {
if start_registration_service {
self.clone().start_validator_registration_service(spec)?;
}
pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> {
self.clone().start_validator_registration_service(spec)?;
self.start_proposer_prepare_service(spec)
}
@@ -208,7 +218,7 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> {
let validator_registration_fut = async move {
loop {
// Poll the endpoint immediately to ensure fee recipients are received.
if let Err(e) = self.register_validators(&spec).await {
if let Err(e) = self.register_validators().await {
error!(log,"Error during validator registration";"error" => ?e);
}
@@ -251,35 +261,48 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> {
}
fn collect_preparation_data(&self, spec: &ChainSpec) -> Vec<ProposerPreparationData> {
self.collect_data(spec, |_, validator_index, fee_recipient| {
ProposerPreparationData {
validator_index,
fee_recipient,
}
})
}
fn collect_validator_registration_keys(
&self,
spec: &ChainSpec,
) -> Vec<ValidatorRegistrationKey> {
self.collect_data(spec, |pubkey, _, fee_recipient| {
ValidatorRegistrationKey {
fee_recipient,
//TODO(sean) this is geth's default, we should make this configurable and maybe have the default be dynamic.
// Discussion here: https://github.com/ethereum/builder-specs/issues/17
gas_limit: 30_000_000,
pubkey,
}
})
}
fn collect_data<G, U>(&self, spec: &ChainSpec, map_fn: G) -> Vec<U>
where
G: Fn(PublicKeyBytes, u64, Address) -> U,
{
let log = self.context.log();
self.collect_proposal_data(|pubkey, proposal_data| {
if let Some(fee_recipient) = proposal_data.fee_recipient {
Some(ProposerPreparationData {
// Ignore fee recipients for keys without indices, they are inactive.
validator_index: proposal_data.validator_index?,
fee_recipient,
})
} else {
if spec.bellatrix_fork_epoch.is_some() {
error!(
log,
"Validator is missing fee recipient";
"msg" => "update validator_definitions.yml",
"pubkey" => ?pubkey
);
}
None
}
})
}
fn collect_validator_registration_keys(&self) -> Vec<ValidatorRegistrationKey> {
self.collect_proposal_data(|pubkey, proposal_data| {
// We don't log for missing fee recipients here because this will be logged more
// frequently in `collect_preparation_data`.
proposal_data.fee_recipient.and_then(|fee_recipient| {
proposal_data
.builder_proposals
.then(|| ValidatorRegistrationKey {
fee_recipient,
gas_limit: proposal_data.gas_limit,
pubkey,
})
})
})
}
fn collect_proposal_data<G, U>(&self, map_fn: G) -> Vec<U>
where
G: Fn(PublicKeyBytes, ProposalData) -> Option<U>,
{
let all_pubkeys: Vec<_> = self
.validator_store
.voting_pubkeys(DoppelgangerStatus::ignored);
@@ -287,23 +310,8 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> {
all_pubkeys
.into_iter()
.filter_map(|pubkey| {
// Ignore fee recipients for keys without indices, they are inactive.
let validator_index = self.validator_store.validator_index(&pubkey)?;
let fee_recipient = self.validator_store.get_fee_recipient(&pubkey);
if let Some(fee_recipient) = fee_recipient {
Some(map_fn(pubkey, validator_index, fee_recipient))
} else {
if spec.bellatrix_fork_epoch.is_some() {
error!(
log,
"Validator is missing fee recipient";
"msg" => "update validator_definitions.yml",
"pubkey" => ?pubkey
);
}
None
}
let proposal_data = self.validator_store.proposal_data(&pubkey)?;
map_fn(pubkey, proposal_data)
})
.collect()
}
@@ -341,8 +349,8 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> {
}
/// Register validators with builders, used in the blinded block proposal flow.
async fn register_validators(&self, spec: &ChainSpec) -> Result<(), String> {
let registration_keys = self.collect_validator_registration_keys(spec);
async fn register_validators(&self) -> Result<(), String> {
let registration_keys = self.collect_validator_registration_keys();
let mut changed_keys = vec![];
@@ -388,10 +396,15 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> {
let signed_data = if let Some(signed_data) = cached_registration_opt {
signed_data
} else {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("{e:?}"))?
.as_secs();
let timestamp =
if let Some(timestamp) = self.builder_registration_timestamp_override {
timestamp
} else {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| format!("{e:?}"))?
.as_secs()
};
let ValidatorRegistrationKey {
fee_recipient,
@@ -426,29 +439,35 @@ impl<T: SlotClock + 'static, E: EthSpec> PreparationService<T, E> {
}
if !signed.is_empty() {
let signed_ref = signed.as_slice();
match self
.beacon_nodes
.first_success(RequireSynced::Yes, |beacon_node| async move {
beacon_node
.post_validator_register_validator(signed_ref)
.await
})
.await
{
Ok(()) => debug!(
log,
"Published validator registration";
"count" => registration_data_len,
),
Err(e) => error!(
log,
"Unable to publish validator registration";
"error" => %e,
),
for batch in signed.chunks(VALIDATOR_REGISTRATION_BATCH_SIZE) {
match self
.beacon_nodes
.first_success(RequireSynced::Yes, |beacon_node| async move {
beacon_node.post_validator_register_validator(batch).await
})
.await
{
Ok(()) => info!(
log,
"Published validator registrations to the builder network";
"count" => registration_data_len,
),
Err(e) => error!(
log,
"Unable to publish validator registrations to the builder network";
"error" => %e,
),
}
}
}
Ok(())
}
}
/// A helper struct, used for passing data from the validator store to services.
pub struct ProposalData {
pub(crate) validator_index: Option<u64>,
pub(crate) fee_recipient: Option<Address>,
pub(crate) gas_limit: u64,
pub(crate) builder_proposals: bool,
}

View File

@@ -3,6 +3,7 @@ use crate::{
http_metrics::metrics,
initialized_validators::InitializedValidators,
signing_method::{Error as SigningError, SignableMessage, SigningContext, SigningMethod},
Config,
};
use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString};
use parking_lot::{Mutex, RwLock};
@@ -27,6 +28,7 @@ use types::{
use validator_dir::ValidatorDir;
pub use crate::doppelganger_service::DoppelgangerStatus;
use crate::preparation_service::ProposalData;
#[derive(Debug, PartialEq)]
pub enum Error {
@@ -52,6 +54,11 @@ impl From<SigningError> for Error {
/// This acts as a maximum safe-guard against clock drift.
const SLASHING_PROTECTION_HISTORY_EPOCHS: u64 = 512;
/// Currently used as the default gas limit in execution clients.
///
/// https://github.com/ethereum/builder-specs/issues/17
const DEFAULT_GAS_LIMIT: u64 = 30_000_000;
struct LocalValidator {
validator_dir: ValidatorDir,
voting_keypair: Keypair,
@@ -87,6 +94,8 @@ pub struct ValidatorStore<T, E: EthSpec> {
doppelganger_service: Option<Arc<DoppelgangerService>>,
slot_clock: T,
fee_recipient_process: Option<Address>,
gas_limit: Option<u64>,
builder_proposals: bool,
task_executor: TaskExecutor,
_phantom: PhantomData<E>,
}
@@ -102,7 +111,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
spec: ChainSpec,
doppelganger_service: Option<Arc<DoppelgangerService>>,
slot_clock: T,
fee_recipient_process: Option<Address>,
config: &Config,
task_executor: TaskExecutor,
log: Logger,
) -> Self {
@@ -115,7 +124,9 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
log,
doppelganger_service,
slot_clock,
fee_recipient_process,
fee_recipient_process: config.fee_recipient,
gas_limit: config.gas_limit,
builder_proposals: config.builder_proposals,
task_executor,
_phantom: PhantomData,
}
@@ -146,6 +157,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
/// Insert a new validator to `self`, where the validator is represented by an EIP-2335
/// keystore on the filesystem.
#[allow(clippy::too_many_arguments)]
pub async fn add_validator_keystore<P: AsRef<Path>>(
&self,
voting_keystore_path: P,
@@ -153,12 +165,16 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
enable: bool,
graffiti: Option<GraffitiString>,
suggested_fee_recipient: Option<Address>,
gas_limit: Option<u64>,
builder_proposals: Option<bool>,
) -> Result<ValidatorDefinition, String> {
let mut validator_def = ValidatorDefinition::new_keystore_with_password(
voting_keystore_path,
Some(password),
graffiti.map(Into::into),
suggested_fee_recipient,
gas_limit,
builder_proposals,
)
.map_err(|e| format!("failed to create validator definitions: {:?}", e))?;
@@ -200,6 +216,23 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
Ok(validator_def)
}
/// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`.
/// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`,
/// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`.
pub fn proposal_data(&self, pubkey: &PublicKeyBytes) -> Option<ProposalData> {
self.validators
.read()
.validator(pubkey)
.map(|validator| ProposalData {
validator_index: validator.get_index(),
fee_recipient: self
.get_fee_recipient_defaulting(validator.get_suggested_fee_recipient()),
gas_limit: self.get_gas_limit_defaulting(validator.get_gas_limit()),
builder_proposals: self
.get_builder_proposals_defaulting(validator.get_builder_proposals()),
})
}
/// Attempts to resolve the pubkey to a validator index.
///
/// It may return `None` if the `pubkey` is:
@@ -366,9 +399,12 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
pub fn get_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option<Address> {
// If there is a `suggested_fee_recipient` in the validator definitions yaml
// file, use that value.
self.suggested_fee_recipient(validator_pubkey)
// If there's nothing in the file, try the process-level default value.
.or(self.fee_recipient_process)
self.get_fee_recipient_defaulting(self.suggested_fee_recipient(validator_pubkey))
}
pub fn get_fee_recipient_defaulting(&self, fee_recipient: Option<Address>) -> Option<Address> {
// If there's nothing in the file, try the process-level default value.
fee_recipient.or(self.fee_recipient_process)
}
/// Returns the suggested_fee_recipient from `validator_definitions.yml` if any.
@@ -379,6 +415,45 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
.suggested_fee_recipient(validator_pubkey)
}
/// Returns the gas limit for the given public key. The priority order for fetching
/// the gas limit is:
///
/// 1. validator_definitions.yml
/// 2. process level gas limit
/// 3. `DEFAULT_GAS_LIMIT`
pub fn get_gas_limit(&self, validator_pubkey: &PublicKeyBytes) -> u64 {
self.get_gas_limit_defaulting(self.validators.read().gas_limit(validator_pubkey))
}
fn get_gas_limit_defaulting(&self, gas_limit: Option<u64>) -> u64 {
// If there is a `gas_limit` in the validator definitions yaml
// file, use that value.
gas_limit
// If there's nothing in the file, try the process-level default value.
.or(self.gas_limit)
// If there's no process-level default, use the `DEFAULT_GAS_LIMIT`.
.unwrap_or(DEFAULT_GAS_LIMIT)
}
/// Returns a `bool` for the given public key that denotes whther this validator should use the
/// builder API. The priority order for fetching this value is:
///
/// 1. validator_definitions.yml
/// 2. process level flag
pub fn get_builder_proposals(&self, validator_pubkey: &PublicKeyBytes) -> bool {
// If there is a `suggested_fee_recipient` in the validator definitions yaml
// file, use that value.
self.get_builder_proposals_defaulting(
self.validators.read().builder_proposals(validator_pubkey),
)
}
fn get_builder_proposals_defaulting(&self, builder_proposals: Option<bool>) -> bool {
builder_proposals
// If there's nothing in the file, try the process-level default value.
.unwrap_or(self.builder_proposals)
}
pub async fn sign_block<Payload: ExecPayload<E>>(
&self,
validator_pubkey: PublicKeyBytes,