Optional slashing protection for remote keys (#4981)

* Optional slashing protection for remote keys

* Merge remote-tracking branch 'origin/unstable' into disable-slashing-protection-web3signer

* Start writing tests

* Merge remote-tracking branch 'origin/unstable' into disable-slashing-protection-web3signer

* Merge remote-tracking branch 'michael/disable-slashing-protection-web3signer' into disable-slashing-protection-web3signer

* Make half-written tests compile

* Make tests work

* Update help text

* Update book CLI text

* Merge remote-tracking branch 'origin/unstable' into disable-slashing-protection-web3signer

* More logging & CLI tests

* CLI tweaks
This commit is contained in:
Michael Sproul 2024-02-06 12:30:31 +11:00 committed by GitHub
parent 795c5778e1
commit 7bec3f9b59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 463 additions and 163 deletions

View File

@ -13,7 +13,7 @@ FLAGS:
--disable-auto-discover --disable-auto-discover
If present, do not attempt to discover new validators in the validators-dir. Validators will need to be If present, do not attempt to discover new validators in the validators-dir. Validators will need to be
manually added to the validator_definitions.yml file. manually added to the validator_definitions.yml file.
--disable-log-timestamp If present, do not include timestamps in logging output. --disable-log-timestamp If present, do not include timestamps in logging output.
--disable-malloc-tuning --disable-malloc-tuning
If present, do not configure the system allocator. Providing this flag will generally increase memory usage, If present, do not configure the system allocator. Providing this flag will generally increase memory usage,
it should only be provided when debugging specific memory allocation issues. it should only be provided when debugging specific memory allocation issues.
@ -21,6 +21,11 @@ FLAGS:
DEPRECATED. Use --broadcast. By default, Lighthouse publishes attestation, sync committee subscriptions and DEPRECATED. Use --broadcast. By default, Lighthouse publishes attestation, sync committee subscriptions and
proposer preparation messages to all beacon nodes provided in the `--beacon-nodes flag`. This option changes proposer preparation messages to all beacon nodes provided in the `--beacon-nodes flag`. This option changes
that behaviour such that these api calls only go out to the first available and synced beacon node that behaviour such that these api calls only go out to the first available and synced beacon node
--disable-slashing-protection-web3signer
Disable Lighthouse's slashing protection for all web3signer keys. This can reduce the I/O burden on the VC
but is only safe if slashing protection is enabled on the remote signer and is implemented correctly. DO NOT
ENABLE THIS FLAG UNLESS YOU ARE CERTAIN THAT SLASHING PROTECTION IS ENABLED ON THE REMOTE SIGNER. YOU WILL
GET SLASHED IF YOU USE THIS FLAG WITHOUT ENABLING WEB3SIGNER'S SLASHING PROTECTION.
--enable-doppelganger-protection --enable-doppelganger-protection
If this flag is set, Lighthouse will delay startup for three epochs and monitor for messages on the network If this flag is set, Lighthouse will delay startup for three epochs and monitor for messages on the network
by any of the validators managed by this client. This will result in three (possibly four) epochs worth of by any of the validators managed by this client. This will result in three (possibly four) epochs worth of
@ -32,8 +37,8 @@ FLAGS:
Enable per validator metrics for > 64 validators. Note: This flag is automatically enabled for <= 64 Enable per validator metrics for > 64 validators. Note: This flag is automatically enabled for <= 64
validators. Enabling this metric for higher validator counts will lead to higher volume of prometheus validators. Enabling this metric for higher validator counts will lead to higher volume of prometheus
metrics being collected. metrics being collected.
-h, --help Prints help information -h, --help Prints help information
--http Enable the RESTful HTTP API server. Disabled by default. --http Enable the RESTful HTTP API server. Disabled by default.
--http-allow-keystore-export --http-allow-keystore-export
If present, allow access to the DELETE /lighthouse/keystores HTTP API method, which allows exporting If present, allow access to the DELETE /lighthouse/keystores HTTP API method, which allows exporting
keystores and passwords to HTTP API consumers who have access to the API token. This method is useful for keystores and passwords to HTTP API consumers who have access to the API token. This method is useful for
@ -47,7 +52,7 @@ FLAGS:
flag unless you're certain that a new slashing protection database is required. Usually, your database will flag unless you're certain that a new slashing protection database is required. Usually, your database will
have been initialized when you imported your validator keys. If you misplace your database and then run with have been initialized when you imported your validator keys. If you misplace your database and then run with
this flag you risk being slashed. this flag you risk being slashed.
--log-color Force outputting colors when emitting logs to the terminal. --log-color Force outputting colors when emitting logs to the terminal.
--logfile-compress --logfile-compress
If present, compress old log files. This can help reduce the space needed to store old logs. If present, compress old log files. This can help reduce the space needed to store old logs.
@ -55,7 +60,7 @@ FLAGS:
If present, log files will be generated as world-readable meaning they can be read by any user on the If present, log files will be generated as world-readable meaning they can be read by any user on the
machine. Note that logs can often contain sensitive information about your validator and so this flag should machine. Note that logs can often contain sensitive information about your validator and so this flag should
be used with caution. For Windows users, the log file permissions will be inherited from the parent folder. be used with caution. For Windows users, the log file permissions will be inherited from the parent folder.
--metrics Enable the Prometheus metrics HTTP server. Disabled by default. --metrics Enable the Prometheus metrics HTTP server. Disabled by default.
--prefer-builder-proposals --prefer-builder-proposals
If this flag is set, Lighthouse will always prefer blocks constructed by builders, regardless of payload If this flag is set, Lighthouse will always prefer blocks constructed by builders, regardless of payload
value. value.
@ -69,7 +74,7 @@ FLAGS:
--use-long-timeouts --use-long-timeouts
If present, the validator client will use longer timeouts for requests made to the beacon node. This flag is If present, the validator client will use longer timeouts for requests made to the beacon node. This flag is
generally not recommended, longer timeouts can cause missed duties when fallbacks are used. generally not recommended, longer timeouts can cause missed duties when fallbacks are used.
-V, --version Prints version information -V, --version Prints version information
OPTIONS: OPTIONS:
--beacon-nodes <NETWORK_ADDRESSES> --beacon-nodes <NETWORK_ADDRESSES>

View File

@ -636,3 +636,20 @@ fn validator_registration_batch_size_zero_value() {
.flag("validator-registration-batch-size", Some("0")) .flag("validator-registration-batch-size", Some("0"))
.run(); .run();
} }
#[test]
fn validator_disable_web3_signer_slashing_protection_default() {
CommandLineTest::new().run().with_config(|config| {
assert!(config.enable_web3signer_slashing_protection);
});
}
#[test]
fn validator_disable_web3_signer_slashing_protection() {
CommandLineTest::new()
.flag("disable-slashing-protection-web3signer", None)
.run()
.with_config(|config| {
assert!(!config.enable_web3signer_slashing_protection);
});
}

View File

@ -45,7 +45,7 @@ mod tests {
initialized_validators::{ initialized_validators::{
load_pem_certificate, load_pkcs12_identity, InitializedValidators, load_pem_certificate, load_pkcs12_identity, InitializedValidators,
}, },
validator_store::ValidatorStore, validator_store::{Error as ValidatorStoreError, ValidatorStore},
SlashingDatabase, SLASHING_PROTECTION_FILENAME, SlashingDatabase, SLASHING_PROTECTION_FILENAME,
}; };
@ -157,6 +157,18 @@ mod tests {
} }
} }
#[derive(Debug, Clone, Copy)]
struct SlashingProtectionConfig {
/// Whether to enable slashing protection for web3signer keys locally within Lighthouse.
local: bool,
}
impl Default for SlashingProtectionConfig {
fn default() -> Self {
SlashingProtectionConfig { local: true }
}
}
impl Web3SignerRig { impl Web3SignerRig {
pub async fn new(network: &str, listen_address: &str, listen_port: u16) -> Self { pub async fn new(network: &str, listen_address: &str, listen_port: u16) -> Self {
GET_WEB3SIGNER_BIN GET_WEB3SIGNER_BIN
@ -231,6 +243,8 @@ mod tests {
)) ))
.arg("eth2") .arg("eth2")
.arg(format!("--network={}", network)) .arg(format!("--network={}", network))
// Can't *easily* test `--slashing-protection-enabled=true` because web3signer
// requires a Postgres instance.
.arg("--slashing-protection-enabled=false") .arg("--slashing-protection-enabled=false")
.stdout(stdio()) .stdout(stdio())
.stderr(stdio()) .stderr(stdio())
@ -294,10 +308,16 @@ mod tests {
_validator_dir: TempDir, _validator_dir: TempDir,
runtime: Arc<tokio::runtime::Runtime>, runtime: Arc<tokio::runtime::Runtime>,
_runtime_shutdown: exit_future::Signal, _runtime_shutdown: exit_future::Signal,
using_web3signer: bool,
} }
impl ValidatorStoreRig { impl ValidatorStoreRig {
pub async fn new(validator_definitions: Vec<ValidatorDefinition>, spec: ChainSpec) -> Self { pub async fn new(
validator_definitions: Vec<ValidatorDefinition>,
slashing_protection_config: SlashingProtectionConfig,
using_web3signer: bool,
spec: ChainSpec,
) -> Self {
let log = environment::null_logger().unwrap(); let log = environment::null_logger().unwrap();
let validator_dir = TempDir::new().unwrap(); let validator_dir = TempDir::new().unwrap();
@ -333,6 +353,10 @@ mod tests {
let slot_clock = let slot_clock =
TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1));
let config = validator_client::Config {
enable_web3signer_slashing_protection: slashing_protection_config.local,
..Default::default()
};
let validator_store = ValidatorStore::<_, E>::new( let validator_store = ValidatorStore::<_, E>::new(
initialized_validators, initialized_validators,
@ -351,6 +375,7 @@ mod tests {
_validator_dir: validator_dir, _validator_dir: validator_dir,
runtime, runtime,
_runtime_shutdown: runtime_shutdown, _runtime_shutdown: runtime_shutdown,
using_web3signer,
} }
} }
@ -379,7 +404,12 @@ mod tests {
} }
impl TestingRig { impl TestingRig {
pub async fn new(network: &str, spec: ChainSpec, listen_port: u16) -> Self { pub async fn new(
network: &str,
slashing_protection_config: SlashingProtectionConfig,
spec: ChainSpec,
listen_port: u16,
) -> Self {
let signer_rig = let signer_rig =
Web3SignerRig::new(network, WEB3SIGNER_LISTEN_ADDRESS, listen_port).await; Web3SignerRig::new(network, WEB3SIGNER_LISTEN_ADDRESS, listen_port).await;
let validator_pubkey = signer_rig.keypair.pk.clone(); let validator_pubkey = signer_rig.keypair.pk.clone();
@ -401,7 +431,13 @@ mod tests {
voting_keystore_password: Some(KEYSTORE_PASSWORD.to_string().into()), voting_keystore_password: Some(KEYSTORE_PASSWORD.to_string().into()),
}, },
}; };
ValidatorStoreRig::new(vec![validator_definition], spec.clone()).await ValidatorStoreRig::new(
vec![validator_definition],
slashing_protection_config,
false,
spec.clone(),
)
.await
}; };
let remote_signer_validator_store = { let remote_signer_validator_store = {
@ -423,7 +459,13 @@ mod tests {
client_identity_password: Some(client_identity_password()), client_identity_password: Some(client_identity_password()),
}), }),
}; };
ValidatorStoreRig::new(vec![validator_definition], spec).await ValidatorStoreRig::new(
vec![validator_definition],
slashing_protection_config,
true,
spec,
)
.await
}; };
Self { Self {
@ -466,6 +508,36 @@ mod tests {
assert!(prev_signature.is_some(), "sanity check"); assert!(prev_signature.is_some(), "sanity check");
self self
} }
/// Assert that a slashable message fails to be signed locally and is either signed or not
/// by the web3signer rig depending on the value of `web3signer_should_sign`.
pub async fn assert_slashable_message_should_sign<F, R>(
self,
case_name: &str,
generate_sig: F,
web3signer_should_sign: bool,
) -> Self
where
F: Fn(PublicKeyBytes, Arc<ValidatorStore<TestingSlotClock, E>>) -> R,
R: Future<Output = Result<(), ValidatorStoreError>>,
{
for validator_rig in &self.validator_rigs {
let result =
generate_sig(self.validator_pubkey, validator_rig.validator_store.clone())
.await;
if !validator_rig.using_web3signer || !web3signer_should_sign {
let err = result.unwrap_err();
assert!(
matches!(err, ValidatorStoreError::Slashable(_)),
"should not sign slashable {case_name}"
);
} else {
assert_eq!(result, Ok(()), "should sign slashable {case_name}");
}
}
self
}
} }
/// Get a generic, arbitrary attestation for signing. /// Get a generic, arbitrary attestation for signing.
@ -504,64 +576,69 @@ mod tests {
let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap(); let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap();
let spec = &network_config.chain_spec::<E>().unwrap(); let spec = &network_config.chain_spec::<E>().unwrap();
TestingRig::new(network, spec.clone(), listen_port) TestingRig::new(
.await network,
.assert_signatures_match("randao_reveal", |pubkey, validator_store| async move { SlashingProtectionConfig::default(),
spec.clone(),
listen_port,
)
.await
.assert_signatures_match("randao_reveal", |pubkey, validator_store| async move {
validator_store
.randao_reveal(pubkey, Epoch::new(0))
.await
.unwrap()
})
.await
.assert_signatures_match("beacon_block_base", |pubkey, validator_store| async move {
let block = BeaconBlock::Base(BeaconBlockBase::empty(spec));
let block_slot = block.slot();
validator_store
.sign_block(pubkey, block, block_slot)
.await
.unwrap()
})
.await
.assert_signatures_match("attestation", |pubkey, validator_store| async move {
let mut attestation = get_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, Epoch::new(0))
.await
.unwrap();
attestation
})
.await
.assert_signatures_match("signed_aggregate", |pubkey, validator_store| async move {
let attestation = get_attestation();
validator_store
.produce_signed_aggregate_and_proof(
pubkey,
0,
attestation,
SelectionProof::from(Signature::empty()),
)
.await
.unwrap()
})
.await
.assert_signatures_match("selection_proof", |pubkey, validator_store| async move {
validator_store
.produce_selection_proof(pubkey, Slot::new(0))
.await
.unwrap()
})
.await
.assert_signatures_match(
"validator_registration",
|pubkey, validator_store| async move {
let val_reg_data = get_validator_registration(pubkey);
validator_store validator_store
.randao_reveal(pubkey, Epoch::new(0)) .sign_validator_registration_data(val_reg_data)
.await .await
.unwrap() .unwrap()
}) },
.await )
.assert_signatures_match("beacon_block_base", |pubkey, validator_store| async move { .await;
let block = BeaconBlock::Base(BeaconBlockBase::empty(spec));
let block_slot = block.slot();
validator_store
.sign_block(pubkey, block, block_slot)
.await
.unwrap()
})
.await
.assert_signatures_match("attestation", |pubkey, validator_store| async move {
let mut attestation = get_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, Epoch::new(0))
.await
.unwrap();
attestation
})
.await
.assert_signatures_match("signed_aggregate", |pubkey, validator_store| async move {
let attestation = get_attestation();
validator_store
.produce_signed_aggregate_and_proof(
pubkey,
0,
attestation,
SelectionProof::from(Signature::empty()),
)
.await
.unwrap()
})
.await
.assert_signatures_match("selection_proof", |pubkey, validator_store| async move {
validator_store
.produce_selection_proof(pubkey, Slot::new(0))
.await
.unwrap()
})
.await
.assert_signatures_match(
"validator_registration",
|pubkey, validator_store| async move {
let val_reg_data = get_validator_registration(pubkey);
validator_store
.sign_validator_registration_data(val_reg_data)
.await
.unwrap()
},
)
.await;
} }
/// Test all the Altair types. /// Test all the Altair types.
@ -573,82 +650,78 @@ mod tests {
.unwrap() .unwrap()
.start_slot(E::slots_per_epoch()); .start_slot(E::slots_per_epoch());
TestingRig::new(network, spec.clone(), listen_port) TestingRig::new(
.await network,
.assert_signatures_match( SlashingProtectionConfig::default(),
"beacon_block_altair", spec.clone(),
|pubkey, validator_store| async move { listen_port,
let mut altair_block = BeaconBlockAltair::empty(spec); )
altair_block.slot = altair_fork_slot; .await
validator_store .assert_signatures_match(
.sign_block(pubkey, BeaconBlock::Altair(altair_block), altair_fork_slot) "beacon_block_altair",
.await |pubkey, validator_store| async move {
.unwrap() let mut altair_block = BeaconBlockAltair::empty(spec);
}, altair_block.slot = altair_fork_slot;
) validator_store
.await .sign_block(pubkey, BeaconBlock::Altair(altair_block), altair_fork_slot)
.assert_signatures_match( .await
"sync_selection_proof", .unwrap()
|pubkey, validator_store| async move { },
validator_store )
.produce_sync_selection_proof( .await
&pubkey, .assert_signatures_match(
altair_fork_slot, "sync_selection_proof",
SyncSubnetId::from(0), |pubkey, validator_store| async move {
) validator_store
.await .produce_sync_selection_proof(&pubkey, altair_fork_slot, SyncSubnetId::from(0))
.unwrap() .await
}, .unwrap()
) },
.await )
.assert_signatures_match( .await
"sync_committee_signature", .assert_signatures_match(
|pubkey, validator_store| async move { "sync_committee_signature",
validator_store |pubkey, validator_store| async move {
.produce_sync_committee_signature( validator_store
altair_fork_slot, .produce_sync_committee_signature(altair_fork_slot, Hash256::zero(), 0, &pubkey)
Hash256::zero(), .await
0, .unwrap()
&pubkey, },
) )
.await .await
.unwrap() .assert_signatures_match(
}, "signed_contribution_and_proof",
) |pubkey, validator_store| async move {
.await let contribution = SyncCommitteeContribution {
.assert_signatures_match( slot: altair_fork_slot,
"signed_contribution_and_proof", beacon_block_root: <_>::default(),
|pubkey, validator_store| async move { subcommittee_index: <_>::default(),
let contribution = SyncCommitteeContribution { aggregation_bits: <_>::default(),
slot: altair_fork_slot, signature: AggregateSignature::empty(),
beacon_block_root: <_>::default(), };
subcommittee_index: <_>::default(), validator_store
aggregation_bits: <_>::default(), .produce_signed_contribution_and_proof(
signature: AggregateSignature::empty(), 0,
}; pubkey,
validator_store contribution,
.produce_signed_contribution_and_proof( SyncSelectionProof::from(Signature::empty()),
0, )
pubkey, .await
contribution, .unwrap()
SyncSelectionProof::from(Signature::empty()), },
) )
.await .await
.unwrap() .assert_signatures_match(
}, "validator_registration",
) |pubkey, validator_store| async move {
.await let val_reg_data = get_validator_registration(pubkey);
.assert_signatures_match( validator_store
"validator_registration", .sign_validator_registration_data(val_reg_data)
|pubkey, validator_store| async move { .await
let val_reg_data = get_validator_registration(pubkey); .unwrap()
validator_store },
.sign_validator_registration_data(val_reg_data) )
.await .await;
.unwrap()
},
)
.await;
} }
/// Test all the Merge types. /// Test all the Merge types.
@ -660,17 +733,154 @@ mod tests {
.unwrap() .unwrap()
.start_slot(E::slots_per_epoch()); .start_slot(E::slots_per_epoch());
TestingRig::new(network, spec.clone(), listen_port) TestingRig::new(
.await network,
.assert_signatures_match("beacon_block_merge", |pubkey, validator_store| async move { SlashingProtectionConfig::default(),
let mut merge_block = BeaconBlockMerge::empty(spec); spec.clone(),
merge_block.slot = merge_fork_slot; listen_port,
)
.await
.assert_signatures_match("beacon_block_merge", |pubkey, validator_store| async move {
let mut merge_block = BeaconBlockMerge::empty(spec);
merge_block.slot = merge_fork_slot;
validator_store
.sign_block(pubkey, BeaconBlock::Merge(merge_block), merge_fork_slot)
.await
.unwrap()
})
.await;
}
async fn test_lighthouse_slashing_protection(
slashing_protection_config: SlashingProtectionConfig,
listen_port: u16,
) {
// Run these tests on mainnet.
let network = "mainnet";
let network_config = Eth2NetworkConfig::constant(network).unwrap().unwrap();
let spec = &network_config.chain_spec::<E>().unwrap();
let merge_fork_slot = spec
.bellatrix_fork_epoch
.unwrap()
.start_slot(E::slots_per_epoch());
// The slashable message should only be signed by the web3signer validator if slashing
// protection is disabled in Lighthouse.
let slashable_message_should_sign = !slashing_protection_config.local;
let first_attestation = || {
let mut attestation = get_attestation();
attestation.data.source.epoch = Epoch::new(1);
attestation.data.target.epoch = Epoch::new(4);
attestation
};
let double_vote_attestation = || {
let mut attestation = first_attestation();
attestation.data.beacon_block_root = Hash256::from_low_u64_be(1);
attestation
};
let surrounding_attestation = || {
let mut attestation = first_attestation();
attestation.data.source.epoch = Epoch::new(0);
attestation.data.target.epoch = Epoch::new(5);
attestation
};
let surrounded_attestation = || {
let mut attestation = first_attestation();
attestation.data.source.epoch = Epoch::new(2);
attestation.data.target.epoch = Epoch::new(3);
attestation
};
let first_block = || {
let mut merge_block = BeaconBlockMerge::empty(spec);
merge_block.slot = merge_fork_slot;
BeaconBlock::Merge(merge_block)
};
let double_vote_block = || {
let mut block = first_block();
*block.state_root_mut() = Hash256::repeat_byte(0xff);
block
};
let current_epoch = Epoch::new(5);
TestingRig::new(
network,
slashing_protection_config,
spec.clone(),
listen_port,
)
.await
.assert_signatures_match("first_attestation", |pubkey, validator_store| async move {
let mut attestation = first_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, current_epoch)
.await
.unwrap();
attestation
})
.await
.assert_slashable_message_should_sign(
"double_vote_attestation",
move |pubkey, validator_store| async move {
let mut attestation = double_vote_attestation();
validator_store validator_store
.sign_block(pubkey, BeaconBlock::Merge(merge_block), merge_fork_slot) .sign_attestation(pubkey, 0, &mut attestation, current_epoch)
.await .await
.unwrap() },
}) slashable_message_should_sign,
.await; )
.await
.assert_slashable_message_should_sign(
"surrounding_attestation",
move |pubkey, validator_store| async move {
let mut attestation = surrounding_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, current_epoch)
.await
},
slashable_message_should_sign,
)
.await
.assert_slashable_message_should_sign(
"surrounded_attestation",
move |pubkey, validator_store| async move {
let mut attestation = surrounded_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, current_epoch)
.await
},
slashable_message_should_sign,
)
.await
.assert_signatures_match("first_block", |pubkey, validator_store| async move {
let block = first_block();
let slot = block.slot();
validator_store
.sign_block(pubkey, block, slot)
.await
.unwrap()
})
.await
.assert_slashable_message_should_sign(
"double_vote_block",
move |pubkey, validator_store| async move {
let block = double_vote_block();
let slot = block.slot();
validator_store
.sign_block(pubkey, block, slot)
.await
.map(|_| ())
},
slashable_message_should_sign,
)
.await;
} }
#[tokio::test] #[tokio::test]
@ -707,4 +917,14 @@ mod tests {
async fn sepolia_merge_types() { async fn sepolia_merge_types() {
test_merge_types("sepolia", 4252).await test_merge_types("sepolia", 4252).await
} }
#[tokio::test]
async fn slashing_protection_disabled_locally() {
test_lighthouse_slashing_protection(SlashingProtectionConfig { local: false }, 4253).await
}
#[tokio::test]
async fn slashing_protection_enabled_locally() {
test_lighthouse_slashing_protection(SlashingProtectionConfig { local: true }, 4254).await
}
} }

View File

@ -367,6 +367,17 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
constructed by builders, regardless of payload value.") constructed by builders, regardless of payload value.")
.takes_value(false), .takes_value(false),
) )
.arg(
Arg::with_name("disable-slashing-protection-web3signer")
.long("disable-slashing-protection-web3signer")
.help("Disable Lighthouse's slashing protection for all web3signer keys. This can \
reduce the I/O burden on the VC but is only safe if slashing protection \
is enabled on the remote signer and is implemented correctly. DO NOT ENABLE \
THIS FLAG UNLESS YOU ARE CERTAIN THAT SLASHING PROTECTION IS ENABLED ON \
THE REMOTE SIGNER. YOU WILL GET SLASHED IF YOU USE THIS FLAG WITHOUT \
ENABLING WEB3SIGNER'S SLASHING PROTECTION.")
.takes_value(false)
)
/* /*
* Experimental/development options. * Experimental/development options.
*/ */

View File

@ -76,6 +76,8 @@ pub struct Config {
pub enable_latency_measurement_service: bool, pub enable_latency_measurement_service: bool,
/// Defines the number of validators per `validator/register_validator` request sent to the BN. /// Defines the number of validators per `validator/register_validator` request sent to the BN.
pub validator_registration_batch_size: usize, pub validator_registration_batch_size: usize,
/// Enable slashing protection even while using web3signer keys.
pub enable_web3signer_slashing_protection: bool,
/// Enables block production via the block v3 endpoint. This configuration option can be removed post deneb. /// Enables block production via the block v3 endpoint. This configuration option can be removed post deneb.
pub produce_block_v3: bool, pub produce_block_v3: bool,
/// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value. /// Specifies the boost factor, a percentage multiplier to apply to the builder's payload value.
@ -124,6 +126,7 @@ impl Default for Config {
broadcast_topics: vec![ApiTopic::Subscriptions], broadcast_topics: vec![ApiTopic::Subscriptions],
enable_latency_measurement_service: true, enable_latency_measurement_service: true,
validator_registration_batch_size: 500, validator_registration_batch_size: 500,
enable_web3signer_slashing_protection: true,
produce_block_v3: false, produce_block_v3: false,
builder_boost_factor: None, builder_boost_factor: None,
prefer_builder_proposals: false, prefer_builder_proposals: false,
@ -407,6 +410,19 @@ impl Config {
return Err("validator-registration-batch-size cannot be 0".to_string()); return Err("validator-registration-batch-size cannot be 0".to_string());
} }
config.enable_web3signer_slashing_protection =
if cli_args.is_present("disable-slashing-protection-web3signer") {
warn!(
log,
"Slashing protection for remote keys disabled";
"info" => "ensure slashing protection on web3signer is enabled or you WILL \
get slashed"
);
false
} else {
true
};
Ok(config) Ok(config)
} }
} }

View File

@ -117,6 +117,20 @@ impl SigningContext {
} }
impl SigningMethod { impl SigningMethod {
/// Return whether this signing method requires local slashing protection.
pub fn requires_local_slashing_protection(
&self,
enable_web3signer_slashing_protection: bool,
) -> bool {
match self {
// Slashing protection is ALWAYS required for local keys. DO NOT TURN THIS OFF.
SigningMethod::LocalKeystore { .. } => true,
// Slashing protection is only required for remote signer keys when the configuration
// dictates that it is desired.
SigningMethod::Web3Signer { .. } => enable_web3signer_slashing_protection,
}
}
/// Return the signature of `signable_message`, with respect to the `signing_context`. /// Return the signature of `signable_message`, with respect to the `signing_context`.
pub async fn get_signature<T: EthSpec, Payload: AbstractExecPayload<T>>( pub async fn get_signature<T: EthSpec, Payload: AbstractExecPayload<T>>(
&self, &self,

View File

@ -97,6 +97,7 @@ pub struct ValidatorStore<T, E: EthSpec> {
fee_recipient_process: Option<Address>, fee_recipient_process: Option<Address>,
gas_limit: Option<u64>, gas_limit: Option<u64>,
builder_proposals: bool, builder_proposals: bool,
enable_web3signer_slashing_protection: bool,
produce_block_v3: bool, produce_block_v3: bool,
prefer_builder_proposals: bool, prefer_builder_proposals: bool,
builder_boost_factor: Option<u64>, builder_boost_factor: Option<u64>,
@ -131,6 +132,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
fee_recipient_process: config.fee_recipient, fee_recipient_process: config.fee_recipient,
gas_limit: config.gas_limit, gas_limit: config.gas_limit,
builder_proposals: config.builder_proposals, builder_proposals: config.builder_proposals,
enable_web3signer_slashing_protection: config.enable_web3signer_slashing_protection,
produce_block_v3: config.produce_block_v3, produce_block_v3: config.produce_block_v3,
prefer_builder_proposals: config.prefer_builder_proposals, prefer_builder_proposals: config.prefer_builder_proposals,
builder_boost_factor: config.builder_boost_factor, builder_boost_factor: config.builder_boost_factor,
@ -604,19 +606,26 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
let signing_context = self.signing_context(Domain::BeaconProposer, signing_epoch); let signing_context = self.signing_context(Domain::BeaconProposer, signing_epoch);
let domain_hash = signing_context.domain_hash(&self.spec); let domain_hash = signing_context.domain_hash(&self.spec);
let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?;
// Check for slashing conditions. // Check for slashing conditions.
let slashing_status = self.slashing_protection.check_and_insert_block_proposal( let slashing_status = if signing_method
&validator_pubkey, .requires_local_slashing_protection(self.enable_web3signer_slashing_protection)
&block.block_header(), {
domain_hash, self.slashing_protection.check_and_insert_block_proposal(
); &validator_pubkey,
&block.block_header(),
domain_hash,
)
} else {
Ok(Safe::Valid)
};
match slashing_status { match slashing_status {
// We can safely sign this block without slashing. // We can safely sign this block without slashing.
Ok(Safe::Valid) => { Ok(Safe::Valid) => {
metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SUCCESS]); metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SUCCESS]);
let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?;
let signature = signing_method let signature = signing_method
.get_signature::<E, Payload>( .get_signature::<E, Payload>(
SignableMessage::BeaconBlock(&block), SignableMessage::BeaconBlock(&block),
@ -672,20 +681,28 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
}); });
} }
// Get the signing method and check doppelganger protection.
let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?;
// Checking for slashing conditions. // Checking for slashing conditions.
let signing_epoch = attestation.data.target.epoch; let signing_epoch = attestation.data.target.epoch;
let signing_context = self.signing_context(Domain::BeaconAttester, signing_epoch); let signing_context = self.signing_context(Domain::BeaconAttester, signing_epoch);
let domain_hash = signing_context.domain_hash(&self.spec); let domain_hash = signing_context.domain_hash(&self.spec);
let slashing_status = self.slashing_protection.check_and_insert_attestation( let slashing_status = if signing_method
&validator_pubkey, .requires_local_slashing_protection(self.enable_web3signer_slashing_protection)
&attestation.data, {
domain_hash, self.slashing_protection.check_and_insert_attestation(
); &validator_pubkey,
&attestation.data,
domain_hash,
)
} else {
Ok(Safe::Valid)
};
match slashing_status { match slashing_status {
// We can safely sign this attestation. // We can safely sign this attestation.
Ok(Safe::Valid) => { Ok(Safe::Valid) => {
let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?;
let signature = signing_method let signature = signing_method
.get_signature::<E, BlindedPayload<E>>( .get_signature::<E, BlindedPayload<E>>(
SignableMessage::AttestationData(&attestation.data), SignableMessage::AttestationData(&attestation.data),