Skip to content

Commit b9fa166

Browse files
committed
Write pubkey cache to disk after releasing lock
1 parent 0435365 commit b9fa166

File tree

2 files changed

+34
-31
lines changed

2 files changed

+34
-31
lines changed

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2638,10 +2638,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
26382638

26392639
// If there are new validators in this block, update our pubkey cache.
26402640
//
2641-
// We perform this _before_ adding the block to fork choice because the pubkey cache is
2642-
// used by attestation processing which will only process an attestation if the block is
2643-
// known to fork choice. This ordering ensure that the pubkey cache is always up-to-date.
2644-
self.validator_pubkey_cache
2641+
// The only keys imported here will be ones for validators deposited in this block, because
2642+
// the cache *must* already have been updated for the parent block when it was imported.
2643+
// Newly deposited validators are not active and their keys are not required by other parts
2644+
// of block processing. The reason we do this here and not after making the block attestable
2645+
// is so we don't have to think about lock ordering with respect to the fork choice lock.
2646+
// There are a bunch of places where we lock both fork choice and the pubkey cache and it
2647+
// would be difficult to check that they all lock fork choice first.
2648+
let mut kv_store_ops = self
2649+
.validator_pubkey_cache
26452650
.try_write_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT)
26462651
.ok_or(Error::ValidatorPubkeyCacheLockTimeout)?
26472652
.import_new_pubkeys(&state)?;
@@ -2773,7 +2778,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
27732778
ops.push(StoreOp::PutState(block.state_root(), &state));
27742779
let txn_lock = self.store.hot_db.begin_rw_transaction();
27752780

2776-
if let Err(e) = self.store.do_atomically(ops) {
2781+
kv_store_ops.extend(self.store.convert_to_kv_batch(ops)?);
2782+
2783+
if let Err(e) = self.store.hot_db.do_atomically(kv_store_ops) {
27772784
error!(
27782785
self.log,
27792786
"Database write failed!";

beacon_node/beacon_chain/src/validator_pubkey_cache.rs

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ use crate::{BeaconChainTypes, BeaconStore};
33
use ssz::{Decode, Encode};
44
use std::collections::HashMap;
55
use std::convert::TryInto;
6-
use store::{DBColumn, Error as StoreError, StoreItem};
6+
use std::marker::PhantomData;
7+
use store::{DBColumn, Error as StoreError, KeyValueStore, KeyValueStoreOp, StoreItem};
78
use types::{BeaconState, Hash256, PublicKey, PublicKeyBytes};
89

910
/// Provides a mapping of `validator_index -> validator_publickey`.
@@ -14,21 +15,17 @@ use types::{BeaconState, Hash256, PublicKey, PublicKeyBytes};
1415
/// 2. To reduce the amount of public key _decompression_ required. A `BeaconState` stores public
1516
/// keys in compressed form and they are needed in decompressed form for signature verification.
1617
/// Decompression is expensive when many keys are involved.
17-
///
18-
/// The cache has a `backing` that it uses to maintain a persistent, on-disk
19-
/// copy of itself. This allows it to be restored between process invocations.
2018
pub struct ValidatorPubkeyCache<T: BeaconChainTypes> {
2119
pubkeys: Vec<PublicKey>,
2220
indices: HashMap<PublicKeyBytes, usize>,
2321
pubkey_bytes: Vec<PublicKeyBytes>,
24-
store: BeaconStore<T>,
22+
_phantom: PhantomData<T>,
2523
}
2624

2725
impl<T: BeaconChainTypes> ValidatorPubkeyCache<T> {
2826
/// Create a new public key cache using the keys in `state.validators`.
2927
///
30-
/// Also creates a new persistence file, returning an error if there is already a file at
31-
/// `persistence_path`.
28+
/// The new cache will be updated with the keys from `state` and immediately written to disk.
3229
pub fn new(
3330
state: &BeaconState<T::EthSpec>,
3431
store: BeaconStore<T>,
@@ -37,10 +34,11 @@ impl<T: BeaconChainTypes> ValidatorPubkeyCache<T> {
3734
pubkeys: vec![],
3835
indices: HashMap::new(),
3936
pubkey_bytes: vec![],
40-
store,
37+
_phantom: PhantomData,
4138
};
4239

43-
cache.import_new_pubkeys(state)?;
40+
let store_ops = cache.import_new_pubkeys(state)?;
41+
store.hot_db.do_atomically(store_ops)?;
4442

4543
Ok(cache)
4644
}
@@ -69,55 +67,52 @@ impl<T: BeaconChainTypes> ValidatorPubkeyCache<T> {
6967
pubkeys,
7068
indices,
7169
pubkey_bytes,
72-
store,
70+
_phantom: PhantomData,
7371
})
7472
}
7573

7674
/// Scan the given `state` and add any new validator public keys.
7775
///
7876
/// Does not delete any keys from `self` if they don't appear in `state`.
77+
///
78+
/// NOTE: The caller *must* commit the returned I/O batch as part of the block import process.
7979
pub fn import_new_pubkeys(
8080
&mut self,
8181
state: &BeaconState<T::EthSpec>,
82-
) -> Result<(), BeaconChainError> {
82+
) -> Result<Vec<KeyValueStoreOp>, BeaconChainError> {
8383
if state.validators().len() > self.pubkeys.len() {
8484
self.import(
8585
state.validators()[self.pubkeys.len()..]
8686
.iter()
8787
.map(|v| v.pubkey),
8888
)
8989
} else {
90-
Ok(())
90+
Ok(vec![])
9191
}
9292
}
9393

9494
/// Adds zero or more validators to `self`.
95-
fn import<I>(&mut self, validator_keys: I) -> Result<(), BeaconChainError>
95+
fn import<I>(&mut self, validator_keys: I) -> Result<Vec<KeyValueStoreOp>, BeaconChainError>
9696
where
9797
I: Iterator<Item = PublicKeyBytes> + ExactSizeIterator,
9898
{
9999
self.pubkey_bytes.reserve(validator_keys.len());
100100
self.pubkeys.reserve(validator_keys.len());
101101
self.indices.reserve(validator_keys.len());
102102

103+
let mut store_ops = Vec::with_capacity(validator_keys.len());
103104
for pubkey in validator_keys {
104105
let i = self.pubkeys.len();
105106

106107
if self.indices.contains_key(&pubkey) {
107108
return Err(BeaconChainError::DuplicateValidatorPublicKey);
108109
}
109110

110-
// The item is written to disk _before_ it is written into
111-
// the local struct.
112-
//
113-
// This means that a pubkey cache read from disk will always be equivalent to or
114-
// _later than_ the cache that was running in the previous instance of Lighthouse.
115-
//
116-
// The motivation behind this ordering is that we do not want to have states that
117-
// reference a pubkey that is not in our cache. However, it's fine to have pubkeys
118-
// that are never referenced in a state.
119-
self.store
120-
.put_item(&DatabasePubkey::key_for_index(i), &DatabasePubkey(pubkey))?;
111+
// Stage the new validator key for writing to disk.
112+
// It will be committed atomically when the block that introduced it is written to disk.
113+
// Notably it is NOT written while the write lock on the cache is held.
114+
// See: https://github.com/sigp/lighthouse/issues/2327
115+
store_ops.push(DatabasePubkey(pubkey).as_kv_store_op(DatabasePubkey::key_for_index(i)));
121116

122117
self.pubkeys.push(
123118
(&pubkey)
@@ -129,7 +124,7 @@ impl<T: BeaconChainTypes> ValidatorPubkeyCache<T> {
129124
self.indices.insert(pubkey, i);
130125
}
131126

132-
Ok(())
127+
Ok(store_ops)
133128
}
134129

135130
/// Get the public key for a validator with index `i`.
@@ -296,9 +291,10 @@ mod test {
296291

297292
// Add some more keypairs.
298293
let (state, keypairs) = get_state(12);
299-
cache
294+
let ops = cache
300295
.import_new_pubkeys(&state)
301296
.expect("should import pubkeys");
297+
store.hot_db.do_atomically(ops).unwrap();
302298
check_cache_get(&cache, &keypairs[..]);
303299
drop(cache);
304300

0 commit comments

Comments
 (0)