Fix assert in slashing protection import (#2881)

## Issue Addressed

There was an overeager assert in the import of slashing protection data here:

fff01b24dd/validator_client/slashing_protection/src/slashing_database.rs (L939)

We were asserting that if the import contained any blocks for a validator, then the database should contain only a single block for that validator due to pruning/consolidation. However, we would only prune if the import contained _relevant blocks_ (that would actually change the maximum slot):

fff01b24dd/validator_client/slashing_protection/src/slashing_database.rs (L629-L633)

This lead to spurious failures (in the form of `ConsistencyError`s) when importing an interchange containing no new blocks for any of the validators. This wasn't hard to trigger, e.g. export and then immediately re-import the same file.

## Proposed Changes

This PR fixes the issue by simplifying the import so that it's more like the import for attestations. I.e. we make the assert true by always pruning when the imported file contains blocks.

In practice this doesn't have any downsides: if we import a new block then the behaviour is as before, except that we drop the `signing_root`. If we import an existing block or an old block then we prune the database to a single block. The only time this would be relevant is during extreme clock drift locally _plus_ import of a non-drifted interchange, which should occur infrequently.

## Additional Info

I've also added `Arbitrary` implementations to the slashing protection types so that we can fuzz them. I have a fuzzer sitting in a separate directory which I may or may not commit in a subsequent PR.

There's a new test in the standard interchange tests v5.2.1 that checks for this issue: https://github.com/eth-clients/slashing-protection-interchange-tests/pull/12
This commit is contained in:
Michael Sproul
2022-01-04 20:46:44 +00:00
parent dfc8968201
commit 0b54ff17f2
8 changed files with 56 additions and 23 deletions

View File

@@ -648,29 +648,17 @@ impl SlashingDatabase {
// Summary of minimum and maximum messages pre-import.
let prev_summary = self.validator_summary(pubkey, txn)?;
// If the interchange contains a new maximum slot block, import it.
// If the interchange contains any blocks, update the database with the new max slot.
let max_block = record.signed_blocks.iter().max_by_key(|b| b.slot);
if let Some(max_block) = max_block {
// Block is relevant if there are no previous blocks, or new block has slot greater than
// previous maximum.
if prev_summary
.max_block_slot
.map_or(true, |max_block_slot| max_block.slot > max_block_slot)
{
self.insert_block_proposal(
txn,
pubkey,
max_block.slot,
max_block
.signing_root
.map(SigningRoot::from)
.unwrap_or_default(),
)?;
// Store new synthetic block with maximum slot and null signing root. Remove all other
// blocks.
let new_max_slot = max_or(prev_summary.max_block_slot, max_block.slot);
let signing_root = SigningRoot::default();
// Prune the database so that it contains *only* the new block.
self.prune_signed_blocks(&record.pubkey, max_block.slot, txn)?;
}
self.clear_signed_blocks(pubkey, txn)?;
self.insert_block_proposal(txn, pubkey, new_max_slot, signing_root)?;
}
// Find the attestations with max source and max target. Unless the input contains slashable
@@ -901,6 +889,23 @@ impl SlashingDatabase {
Ok(())
}
/// Remove all blocks signed by a given `public_key`.
///
/// Dangerous, should only be used immediately before inserting a new block in the same
/// transacation.
fn clear_signed_blocks(
&self,
public_key: &PublicKeyBytes,
txn: &Transaction,
) -> Result<(), NotSafe> {
let validator_id = self.get_validator_id_in_txn(txn, public_key)?;
txn.execute(
"DELETE FROM signed_blocks WHERE validator_id = ?1",
params![validator_id],
)?;
Ok(())
}
/// Prune the signed attestations table for the given validator keys.
pub fn prune_all_signed_attestations<'a>(
&self,