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:
parent
795c5778e1
commit
7bec3f9b59
@ -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>
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user