mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-03 00:31:50 +00:00
Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com>
755 lines
24 KiB
Rust
755 lines
24 KiB
Rust
//! Provides the `ObservedBlobSidecars` struct which allows for rejecting `BlobSidecar`s
|
|
//! that we have already seen over the gossip network.
|
|
//! Only `BlobSidecar`s that have completed proposer signature verification can be added
|
|
//! to this cache to reduce DoS risks.
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::marker::PhantomData;
|
|
use std::sync::Arc;
|
|
use types::{BlobSidecar, ChainSpec, DataColumnSidecar, EthSpec, Hash256, SignedBeaconBlock, Slot};
|
|
|
|
type ValidatorIndex = u64;
|
|
type BeaconBlockRoot = Hash256;
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum Error {
|
|
/// The slot of the provided `ObservableDataSidecar` is prior to finalization and should not have been provided
|
|
/// to this function. This is an internal error.
|
|
FinalizedDataSidecar {
|
|
slot: Slot,
|
|
finalized_slot: Slot,
|
|
},
|
|
/// The data sidecar contains an invalid index, the data sidecar is invalid.
|
|
/// Note: The invalid data should have been caught and flagged as an error much before reaching
|
|
/// here.
|
|
InvalidDataIndex(u64),
|
|
|
|
// An unexpected data sidecar variant was received
|
|
UnexpectedVariant,
|
|
}
|
|
|
|
pub trait ObservableDataSidecar {
|
|
fn slot(&self) -> Slot;
|
|
fn index(&self) -> u64;
|
|
fn proposer_index(&self) -> Option<ValidatorIndex>;
|
|
fn beacon_block_root(&self) -> BeaconBlockRoot;
|
|
fn max_num_of_items(spec: &ChainSpec, slot: Slot) -> usize;
|
|
}
|
|
|
|
impl<E: EthSpec> ObservableDataSidecar for BlobSidecar<E> {
|
|
fn slot(&self) -> Slot {
|
|
self.slot()
|
|
}
|
|
|
|
fn index(&self) -> u64 {
|
|
self.index
|
|
}
|
|
|
|
fn proposer_index(&self) -> Option<ValidatorIndex> {
|
|
Some(self.block_proposer_index())
|
|
}
|
|
|
|
fn beacon_block_root(&self) -> BeaconBlockRoot {
|
|
self.block_root()
|
|
}
|
|
|
|
fn max_num_of_items(spec: &ChainSpec, slot: Slot) -> usize {
|
|
spec.max_blobs_per_block(slot.epoch(E::slots_per_epoch())) as usize
|
|
}
|
|
}
|
|
|
|
impl<E: EthSpec> ObservableDataSidecar for DataColumnSidecar<E> {
|
|
fn slot(&self) -> Slot {
|
|
self.slot()
|
|
}
|
|
|
|
fn index(&self) -> u64 {
|
|
*self.index()
|
|
}
|
|
|
|
fn proposer_index(&self) -> Option<ValidatorIndex> {
|
|
self.as_fulu().map(|d| d.block_proposer_index()).ok()
|
|
}
|
|
|
|
fn beacon_block_root(&self) -> BeaconBlockRoot {
|
|
self.block_root()
|
|
}
|
|
|
|
fn max_num_of_items(_spec: &ChainSpec, _slot: Slot) -> usize {
|
|
E::number_of_columns()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
|
pub enum ObservationKey {
|
|
ProposerKey((ValidatorIndex, Slot)),
|
|
BlockRootKey((BeaconBlockRoot, Slot)),
|
|
}
|
|
|
|
impl ObservationKey {
|
|
pub fn new<T: ObservableDataSidecar, E: EthSpec>(
|
|
sidecar: &T,
|
|
spec: &ChainSpec,
|
|
) -> Result<Self, Error> {
|
|
let slot = sidecar.slot();
|
|
|
|
if spec.fork_name_at_slot::<E>(slot).gloas_enabled() {
|
|
Ok(Self::new_block_root_key(sidecar.beacon_block_root(), slot))
|
|
} else if let Some(proposer_index) = sidecar.proposer_index() {
|
|
Ok(Self::new_proposer_key(proposer_index, slot))
|
|
} else {
|
|
Err(Error::UnexpectedVariant)
|
|
}
|
|
}
|
|
|
|
pub fn from_block<E: EthSpec>(
|
|
block: &SignedBeaconBlock<E>,
|
|
block_root: Hash256,
|
|
spec: &ChainSpec,
|
|
) -> Self {
|
|
let slot = block.slot();
|
|
|
|
if spec.fork_name_at_slot::<E>(slot).gloas_enabled() {
|
|
Self::new_block_root_key(block_root, slot)
|
|
} else {
|
|
Self::new_proposer_key(block.message().proposer_index(), slot)
|
|
}
|
|
}
|
|
|
|
pub fn new_proposer_key(proposer_index: ValidatorIndex, slot: Slot) -> Self {
|
|
Self::ProposerKey((proposer_index, slot))
|
|
}
|
|
|
|
pub fn new_block_root_key(beacon_block_root: BeaconBlockRoot, slot: Slot) -> Self {
|
|
Self::BlockRootKey((beacon_block_root, slot))
|
|
}
|
|
|
|
pub fn slot(&self) -> Slot {
|
|
match self {
|
|
ObservationKey::ProposerKey((_, slot)) => *slot,
|
|
ObservationKey::BlockRootKey((_, slot)) => *slot,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Maintains a cache of seen `ObservableDataSidecar`s that are received over gossip
|
|
/// and have been gossip verified.
|
|
///
|
|
/// The cache supports pruning based upon the finalized epoch. It does not automatically prune, you
|
|
/// must call `Self::prune` manually.
|
|
///
|
|
/// Note: To prevent DoS attacks, this cache must include only items that have received some DoS resistance
|
|
/// like checking the proposer signature.
|
|
pub struct ObservedDataSidecars<T: ObservableDataSidecar, E: EthSpec> {
|
|
finalized_slot: Slot,
|
|
/// Stores all received data indices for a given `ObservationKey`.
|
|
items: HashMap<ObservationKey, HashSet<u64>>,
|
|
spec: Arc<ChainSpec>,
|
|
_phantom: PhantomData<(T, E)>,
|
|
}
|
|
|
|
impl<T: ObservableDataSidecar, E: EthSpec> ObservedDataSidecars<T, E> {
|
|
/// Instantiates `Self` with `finalized_slot == 0`.
|
|
pub fn new(spec: Arc<ChainSpec>) -> Self {
|
|
Self {
|
|
finalized_slot: Slot::new(0),
|
|
items: HashMap::new(),
|
|
spec,
|
|
_phantom: PhantomData,
|
|
}
|
|
}
|
|
|
|
/// Observe the `data_sidecar` at `ObservationKey`.
|
|
/// Observes the sidecar, returning `Some(key)` if it was already known, `None` if newly added.
|
|
///
|
|
/// This will update `self` so future calls indicate that this `data_sidecar` is known.
|
|
pub fn observe_sidecar(&mut self, data_sidecar: &T) -> Result<Option<ObservationKey>, Error> {
|
|
self.sanitize_data_sidecar(data_sidecar)?;
|
|
|
|
let observation_key = ObservationKey::new::<T, E>(data_sidecar, &self.spec)?;
|
|
|
|
let data_indices = self
|
|
.items
|
|
.entry(observation_key.clone())
|
|
.or_insert_with(|| {
|
|
HashSet::with_capacity(T::max_num_of_items(&self.spec, data_sidecar.slot()))
|
|
});
|
|
let did_not_exist = data_indices.insert(data_sidecar.index());
|
|
|
|
Ok((!did_not_exist).then_some(observation_key))
|
|
}
|
|
|
|
/// Returns `Some(key)` if the sidecar has already been observed, `None` otherwise.
|
|
pub fn observation_key_is_known(
|
|
&self,
|
|
data_sidecar: &T,
|
|
) -> Result<Option<ObservationKey>, Error> {
|
|
self.sanitize_data_sidecar(data_sidecar)?;
|
|
|
|
let observation_key = ObservationKey::new::<T, E>(data_sidecar, &self.spec)?;
|
|
|
|
let is_known = self
|
|
.items
|
|
.get(&observation_key)
|
|
.is_some_and(|indices| indices.contains(&data_sidecar.index()));
|
|
|
|
Ok(is_known.then_some(observation_key))
|
|
}
|
|
|
|
pub fn known_for_observation_key(
|
|
&self,
|
|
observation_key: &ObservationKey,
|
|
) -> Option<&HashSet<u64>> {
|
|
self.items.get(observation_key)
|
|
}
|
|
|
|
fn sanitize_data_sidecar(&self, data_sidecar: &T) -> Result<(), Error> {
|
|
if data_sidecar.index() >= T::max_num_of_items(&self.spec, data_sidecar.slot()) as u64 {
|
|
return Err(Error::InvalidDataIndex(data_sidecar.index()));
|
|
}
|
|
let finalized_slot = self.finalized_slot;
|
|
if finalized_slot > 0 && data_sidecar.slot() <= finalized_slot {
|
|
return Err(Error::FinalizedDataSidecar {
|
|
slot: data_sidecar.slot(),
|
|
finalized_slot,
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Prune `data_sidecar` observations for slots less than or equal to the given slot.
|
|
pub fn prune(&mut self, finalized_slot: Slot) {
|
|
if finalized_slot == 0 {
|
|
return;
|
|
}
|
|
|
|
self.finalized_slot = finalized_slot;
|
|
self.items.retain(|k, _| k.slot() > finalized_slot);
|
|
}
|
|
}
|
|
|
|
/// Abstraction to control "observation" of gossip messages (currently just blobs and data columns).
|
|
///
|
|
/// If a type returns `false` for `observe` then the message will not be immediately added to its
|
|
/// respective gossip observation cache. Unobserved messages should usually be observed later.
|
|
pub trait ObservationStrategy {
|
|
fn observe() -> bool;
|
|
}
|
|
|
|
/// Type for messages that are observed immediately.
|
|
pub struct Observe;
|
|
/// Type for messages that have not been observed.
|
|
#[derive(Debug)]
|
|
pub struct DoNotObserve;
|
|
|
|
impl ObservationStrategy for Observe {
|
|
fn observe() -> bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
impl ObservationStrategy for DoNotObserve {
|
|
fn observe() -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::test_utils::test_spec;
|
|
|
|
use super::*;
|
|
use bls::{FixedBytesExtended, Signature};
|
|
use std::sync::Arc;
|
|
use types::{
|
|
BeaconBlockHeader, DataColumnSidecarFulu, DataColumnSidecarGloas, ForkName, MainnetEthSpec,
|
|
SignedBeaconBlockHeader,
|
|
};
|
|
|
|
type E = MainnetEthSpec;
|
|
|
|
/// Creates a Fulu DataColumnSidecar for testing.
|
|
/// Keyed by (proposer_index, slot) in the observation cache.
|
|
fn get_data_column_sidecar_fulu(
|
|
slot: u64,
|
|
proposer_index: u64,
|
|
index: u64,
|
|
) -> Arc<DataColumnSidecar<E>> {
|
|
let signed_block_header = SignedBeaconBlockHeader {
|
|
message: BeaconBlockHeader {
|
|
slot: slot.into(),
|
|
proposer_index,
|
|
parent_root: Hash256::ZERO,
|
|
state_root: Hash256::ZERO,
|
|
// Use proposer_index as a simple way to generate different block roots
|
|
body_root: Hash256::from_low_u64_be(proposer_index),
|
|
},
|
|
signature: Signature::empty(),
|
|
};
|
|
Arc::new(DataColumnSidecar::Fulu(DataColumnSidecarFulu {
|
|
index,
|
|
column: vec![].try_into().unwrap(),
|
|
kzg_commitments: vec![].try_into().unwrap(),
|
|
kzg_proofs: vec![].try_into().unwrap(),
|
|
signed_block_header,
|
|
kzg_commitments_inclusion_proof: vec![
|
|
Hash256::ZERO;
|
|
E::kzg_commitments_inclusion_proof_depth()
|
|
]
|
|
.try_into()
|
|
.unwrap(),
|
|
}))
|
|
}
|
|
|
|
/// Creates a Gloas DataColumnSidecar for testing.
|
|
/// Keyed by (beacon_block_root, slot) in the observation cache.
|
|
fn get_data_column_sidecar_gloas(
|
|
slot: u64,
|
|
beacon_block_root: Hash256,
|
|
index: u64,
|
|
) -> Arc<DataColumnSidecar<E>> {
|
|
Arc::new(DataColumnSidecar::Gloas(DataColumnSidecarGloas {
|
|
index,
|
|
column: vec![].try_into().unwrap(),
|
|
kzg_commitments: vec![].try_into().unwrap(),
|
|
kzg_proofs: vec![].try_into().unwrap(),
|
|
slot: slot.into(),
|
|
beacon_block_root,
|
|
}))
|
|
}
|
|
|
|
fn get_sidecar(
|
|
slot: u64,
|
|
key: u64,
|
|
index: u64,
|
|
fork_name: ForkName,
|
|
) -> Arc<DataColumnSidecar<E>> {
|
|
if fork_name.gloas_enabled() {
|
|
get_data_column_sidecar_gloas(slot, Hash256::from_low_u64_be(key), index)
|
|
} else {
|
|
get_data_column_sidecar_fulu(slot, key, index)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pruning() {
|
|
let spec = Arc::new(test_spec::<E>());
|
|
let fork_name = spec.fork_name_at_slot::<E>(Slot::new(0));
|
|
|
|
let mut cache = ObservedDataSidecars::<DataColumnSidecar<E>, E>::new(spec.clone());
|
|
|
|
assert_eq!(cache.finalized_slot, 0, "finalized slot is zero");
|
|
assert_eq!(cache.items.len(), 0, "no slots should be present");
|
|
|
|
// Slot 0, index 0
|
|
let key_a = 420;
|
|
let sidecar_a = get_sidecar(0, key_a, 0, fork_name);
|
|
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_a.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"can observe sidecar, indicates sidecar unobserved"
|
|
);
|
|
|
|
/*
|
|
* Preconditions.
|
|
*/
|
|
|
|
assert_eq!(cache.finalized_slot, 0, "finalized slot is zero");
|
|
assert_eq!(
|
|
cache.items.len(),
|
|
1,
|
|
"only one observation key should be present"
|
|
);
|
|
|
|
let observation_key =
|
|
&ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_a.as_ref(), &spec).unwrap();
|
|
|
|
let cached_indices = cache
|
|
.items
|
|
.get(observation_key)
|
|
.expect("slot zero should be present");
|
|
assert_eq!(cached_indices.len(), 1, "only one index should be present");
|
|
|
|
/*
|
|
* Check that a prune at the genesis slot does nothing.
|
|
*/
|
|
|
|
cache.prune(Slot::new(0));
|
|
|
|
let observation_key =
|
|
ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_a.as_ref(), &spec).unwrap();
|
|
|
|
assert_eq!(cache.finalized_slot, 0, "finalized slot is zero");
|
|
assert_eq!(cache.items.len(), 1, "only one slot should be present");
|
|
let cached_indices = cache
|
|
.items
|
|
.get(&observation_key)
|
|
.expect("slot zero should be present");
|
|
assert_eq!(cached_indices.len(), 1, "only one index should be present");
|
|
|
|
/*
|
|
* Check that a prune empties the cache
|
|
*/
|
|
|
|
cache.prune(E::slots_per_epoch().into());
|
|
assert_eq!(
|
|
cache.finalized_slot,
|
|
Slot::from(E::slots_per_epoch()),
|
|
"finalized slot is updated"
|
|
);
|
|
assert_eq!(cache.items.len(), 0, "no items left");
|
|
|
|
/*
|
|
* Check that we can't insert a finalized sidecar
|
|
*/
|
|
|
|
// First slot of finalized epoch
|
|
let sidecar_b = get_sidecar(E::slots_per_epoch(), 419, 0, fork_name);
|
|
|
|
assert_eq!(
|
|
cache.observe_sidecar(sidecar_b.as_ref()),
|
|
Err(Error::FinalizedDataSidecar {
|
|
slot: E::slots_per_epoch().into(),
|
|
finalized_slot: E::slots_per_epoch().into(),
|
|
}),
|
|
"cant insert finalized sidecar"
|
|
);
|
|
|
|
assert_eq!(cache.items.len(), 0, "sidecar was not added");
|
|
|
|
/*
|
|
* Check that we _can_ insert a non-finalized sidecar
|
|
*/
|
|
|
|
let three_epochs = E::slots_per_epoch() * 3;
|
|
|
|
let key_b = 421;
|
|
let sidecar_b = get_sidecar(three_epochs, key_b, 0, fork_name);
|
|
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_b.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"can insert non-finalized sidecar"
|
|
);
|
|
|
|
let observation_key =
|
|
ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_b.as_ref(), &spec).unwrap();
|
|
|
|
assert_eq!(cache.items.len(), 1, "only one slot should be present");
|
|
let cached_indices = cache
|
|
.items
|
|
.get(&observation_key)
|
|
.expect("the three epochs slot should be present");
|
|
assert_eq!(cached_indices.len(), 1, "only one index should be present");
|
|
|
|
/*
|
|
* Check that a prune doesnt wipe later sidecars
|
|
*/
|
|
|
|
let two_epochs = E::slots_per_epoch() * 2;
|
|
cache.prune(two_epochs.into());
|
|
|
|
assert_eq!(
|
|
cache.finalized_slot,
|
|
Slot::from(two_epochs),
|
|
"finalized slot is updated"
|
|
);
|
|
|
|
let observation_key =
|
|
ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_b.as_ref(), &spec).unwrap();
|
|
|
|
assert_eq!(cache.items.len(), 1, "only one slot should be present");
|
|
let cached_indices = cache
|
|
.items
|
|
.get(&observation_key)
|
|
.expect("the three epochs slot should be present");
|
|
assert_eq!(cached_indices.len(), 1, "only one index should be present");
|
|
}
|
|
|
|
#[test]
|
|
fn simple_observations() {
|
|
let spec = Arc::new(test_spec::<E>());
|
|
let fork_name = spec.fork_name_at_slot::<E>(Slot::new(0));
|
|
|
|
let mut cache = ObservedDataSidecars::<DataColumnSidecar<E>, E>::new(spec.clone());
|
|
|
|
// Slot 0, index 0
|
|
let key_a = 420;
|
|
let sidecar_a = get_sidecar(0, key_a, 0, fork_name);
|
|
|
|
assert_eq!(
|
|
cache
|
|
.observation_key_is_known(sidecar_a.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"no observation in empty cache"
|
|
);
|
|
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_a.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"can observe sidecar, indicates sidecar unobserved"
|
|
);
|
|
|
|
assert_eq!(
|
|
cache
|
|
.observation_key_is_known(sidecar_a.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(true),
|
|
"observed sidecar is indicated as true"
|
|
);
|
|
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_a.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(true),
|
|
"observing again indicates true"
|
|
);
|
|
|
|
assert_eq!(cache.finalized_slot, 0, "finalized slot is zero");
|
|
assert_eq!(cache.items.len(), 1, "only one slot should be present");
|
|
let cached_indices = cache
|
|
.items
|
|
.get(
|
|
&ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_a.as_ref(), &spec).unwrap(),
|
|
)
|
|
.expect("slot zero should be present");
|
|
assert_eq!(cached_indices.len(), 1, "only one index should be present");
|
|
|
|
// Slot 1, different key
|
|
|
|
let key_b = 421;
|
|
let sidecar_b = get_sidecar(1, key_b, 0, fork_name);
|
|
|
|
assert_eq!(
|
|
cache
|
|
.observation_key_is_known(sidecar_b.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"no observation for new slot"
|
|
);
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_b.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"can observe sidecar for new slot, indicates sidecar unobserved"
|
|
);
|
|
assert_eq!(
|
|
cache
|
|
.observation_key_is_known(sidecar_b.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(true),
|
|
"observed sidecar in slot 1 is indicated as true"
|
|
);
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_b.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(true),
|
|
"observing slot 1 again indicates true"
|
|
);
|
|
|
|
assert_eq!(cache.finalized_slot, 0, "finalized slot is zero");
|
|
assert_eq!(cache.items.len(), 2, "two slots should be present");
|
|
let cached_indices = cache
|
|
.items
|
|
.get(
|
|
&ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_a.as_ref(), &spec).unwrap(),
|
|
)
|
|
.expect("slot zero should be present");
|
|
assert_eq!(
|
|
cached_indices.len(),
|
|
1,
|
|
"only one index should be present in slot 0"
|
|
);
|
|
let cached_indices = cache
|
|
.items
|
|
.get(
|
|
&ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_b.as_ref(), &spec).unwrap(),
|
|
)
|
|
.expect("slot one should be present");
|
|
assert_eq!(
|
|
cached_indices.len(),
|
|
1,
|
|
"only one index should be present in slot 1"
|
|
);
|
|
|
|
// Slot 0, index 1 (same key as sidecar_a)
|
|
let sidecar_c = get_sidecar(0, key_a, 1, fork_name);
|
|
|
|
assert_eq!(
|
|
cache
|
|
.observation_key_is_known(sidecar_c.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"no observation for new index"
|
|
);
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_c.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"can observe new index, indicates sidecar unobserved for new index"
|
|
);
|
|
assert_eq!(
|
|
cache
|
|
.observation_key_is_known(sidecar_c.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(true),
|
|
"observed new sidecar is indicated as true"
|
|
);
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_c.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(true),
|
|
"observing new sidecar again indicates true"
|
|
);
|
|
|
|
assert_eq!(cache.finalized_slot, 0, "finalized slot is zero");
|
|
assert_eq!(cache.items.len(), 2, "two slots should be present");
|
|
let cached_indices = cache
|
|
.items
|
|
.get(
|
|
&ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_a.as_ref(), &spec).unwrap(),
|
|
)
|
|
.expect("slot zero should be present");
|
|
assert_eq!(
|
|
cached_indices.len(),
|
|
2,
|
|
"two indices should be present in slot 0"
|
|
);
|
|
|
|
// Create a sidecar with a different key at the same slot
|
|
// For Fulu: different proposer_index creates a different observation key
|
|
// For Gloas: different block_root creates a different observation key
|
|
let key_c = 422;
|
|
let sidecar_d = get_sidecar(0, key_c, 0, fork_name);
|
|
assert_eq!(
|
|
cache
|
|
.observation_key_is_known(sidecar_d.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"no observation for new key"
|
|
);
|
|
assert_eq!(
|
|
cache
|
|
.observe_sidecar(sidecar_d.as_ref())
|
|
.map(|o| o.is_some()),
|
|
Ok(false),
|
|
"can observe sidecar, indicates sidecar unobserved for new key"
|
|
);
|
|
let cached_indices = cache
|
|
.items
|
|
.get(
|
|
&ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_d.as_ref(), &spec).unwrap(),
|
|
)
|
|
.expect("sidecar_d's observation key should be present");
|
|
assert_eq!(
|
|
cached_indices.len(),
|
|
1,
|
|
"one index should be present for sidecar_d's observation key"
|
|
);
|
|
|
|
// Try adding an out of bounds index
|
|
let invalid_index = E::number_of_columns() as u64;
|
|
let sidecar_e = get_sidecar(0, key_a, invalid_index, fork_name);
|
|
assert_eq!(
|
|
cache.observe_sidecar(sidecar_e.as_ref()),
|
|
Err(Error::InvalidDataIndex(invalid_index)),
|
|
"cannot add an index >= NUMBER_OF_COLUMNS"
|
|
);
|
|
}
|
|
|
|
/// Test that sidecars with the same observation key but different indices
|
|
/// are tracked correctly.
|
|
#[test]
|
|
fn multiple_indices_same_key() {
|
|
let spec = Arc::new(test_spec::<E>());
|
|
let fork_name = spec.fork_name_at_slot::<E>(Slot::new(0));
|
|
|
|
let mut cache = ObservedDataSidecars::<DataColumnSidecar<E>, E>::new(spec.clone());
|
|
|
|
let key = 420;
|
|
|
|
// Add multiple indices for the same observation key
|
|
for index in 0..5 {
|
|
let sidecar = get_sidecar(0, key, index, fork_name);
|
|
assert_eq!(
|
|
cache.observe_sidecar(sidecar.as_ref()).map(|o| o.is_some()),
|
|
Ok(false),
|
|
"index {index} should be new"
|
|
);
|
|
}
|
|
|
|
// Verify all indices are tracked under one observation key
|
|
assert_eq!(cache.items.len(), 1, "only one observation key");
|
|
|
|
let sidecar_for_key = get_sidecar(0, key, 0, fork_name);
|
|
let observation_key =
|
|
ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar_for_key.as_ref(), &spec)
|
|
.unwrap();
|
|
let cached_indices = cache.items.get(&observation_key).unwrap();
|
|
assert_eq!(cached_indices.len(), 5, "five indices should be tracked");
|
|
|
|
// Re-observing should indicate they're already known
|
|
for index in 0..5 {
|
|
let sidecar = get_sidecar(0, key, index, fork_name);
|
|
assert_eq!(
|
|
cache.observe_sidecar(sidecar.as_ref()).map(|o| o.is_some()),
|
|
Ok(true),
|
|
"index {index} should already be known"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Test the known_for_observation_key method
|
|
#[test]
|
|
fn known_for_observation_key() {
|
|
let spec = Arc::new(test_spec::<E>());
|
|
let fork_name = spec.fork_name_at_slot::<E>(Slot::new(0));
|
|
|
|
let mut cache = ObservedDataSidecars::<DataColumnSidecar<E>, E>::new(spec.clone());
|
|
|
|
let key = 420;
|
|
let sidecar = get_sidecar(0, key, 0, fork_name);
|
|
let observation_key =
|
|
ObservationKey::new::<DataColumnSidecar<E>, E>(sidecar.as_ref(), &spec).unwrap();
|
|
|
|
// Before observation, should return None
|
|
assert!(cache.known_for_observation_key(&observation_key).is_none());
|
|
|
|
// After observation, should return the set of indices
|
|
cache.observe_sidecar(sidecar.as_ref()).unwrap();
|
|
let known = cache
|
|
.known_for_observation_key(&observation_key)
|
|
.expect("should be known");
|
|
assert!(known.contains(&0));
|
|
assert_eq!(known.len(), 1);
|
|
|
|
// Add more indices
|
|
let sidecar_1 = get_sidecar(0, key, 1, fork_name);
|
|
let sidecar_2 = get_sidecar(0, key, 2, fork_name);
|
|
cache.observe_sidecar(sidecar_1.as_ref()).unwrap();
|
|
cache.observe_sidecar(sidecar_2.as_ref()).unwrap();
|
|
|
|
let known = cache
|
|
.known_for_observation_key(&observation_key)
|
|
.expect("should be known");
|
|
assert!(known.contains(&0));
|
|
assert!(known.contains(&1));
|
|
assert!(known.contains(&2));
|
|
assert_eq!(known.len(), 3);
|
|
}
|
|
}
|