Skip to content

Commit 398efc3

Browse files
authored
Use dedicated cache for HTTP API route (#9318)
- PR #9305 wants to store PTCs in the committee cache. BUT the http API route wants to use the committee cache and insert historical committees (i.e. given state at epoch 1000, compute and store the committee for epoch 900). If we want a single cache to serve both use cases we need to: - Have entries in the committee cache that have no PTC: Makes reading PTCs from the cache not deterministic - Compute historical PTC: A bunch of complicated code that's useless Instead we can add a separate cache for the API, very simple one, that caches committees only. And have the one in the beacon chain compute and cache PTCs always. ### Performance impact Slightly additional memory cost for users of the `beacon/states/committees` route. Caching is almost equivalent, except for queries of recent committees that may already exist in the beacon chain's committee cache. ### AI disclousure This PR was written by hand 90%. Claude fixed some warp type issues Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>
1 parent fd0852a commit 398efc3

6 files changed

Lines changed: 115 additions & 41 deletions

File tree

beacon_node/client/src/builder.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use rand::SeedableRng;
3636
use rand::rngs::{OsRng, StdRng};
3737
use slasher::Slasher;
3838
use slasher_service::SlasherService;
39+
use std::num::NonZeroUsize;
3940
use std::path::{Path, PathBuf};
4041
use std::sync::Arc;
4142
use std::time::Duration;
@@ -639,6 +640,10 @@ where
639640
network_globals: self.network_globals.clone(),
640641
beacon_processor_send: Some(beacon_processor_channels.beacon_processor_tx.clone()),
641642
sse_logging_components: runtime_context.sse_logging_components.clone(),
643+
historical_committee_cache: Arc::new(http_api::HistoricalCommitteeCache::new(
644+
NonZeroUsize::new(self.http_api_config.historical_committee_cache_size)
645+
.unwrap_or(NonZeroUsize::MIN),
646+
)),
642647
});
643648

644649
let exit = runtime_context.executor.exit();

beacon_node/http_api/src/beacon/states.rs

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::StateId;
2+
use crate::caches::{HistoricalCommitteeCache, HistoricalShufflingId};
23
use crate::task_spawner::{Priority, TaskSpawner};
34
use crate::utils::ResponseFilter;
45
use crate::validator::pubkey_to_validator_index;
@@ -13,7 +14,10 @@ use eth2::types::{
1314
};
1415
use ssz::Encode;
1516
use std::sync::Arc;
16-
use types::{AttestationShufflingId, BeaconStateError, CommitteeCache, EthSpec, RelativeEpoch};
17+
use types::{
18+
AttestationShufflingId, BeaconStateError, CommitteeCache, EthSpec, RelativeEpoch,
19+
RelativeEpochError,
20+
};
1721
use warp::filters::BoxedFilter;
1822
use warp::http::Response;
1923
use warp::hyper::Body;
@@ -26,6 +30,8 @@ type BeaconStatesPath<T> = BoxedFilter<(
2630
Arc<BeaconChain<T>>,
2731
)>;
2832

33+
type BeaconStatesCommitteesFilter = BoxedFilter<(Arc<HistoricalCommitteeCache>,)>;
34+
2935
// GET beacon/states/{state_id}/pending_consolidations
3036
pub fn get_beacon_state_pending_consolidations<T: BeaconChainTypes>(
3137
beacon_states_path: BeaconStatesPath<T>,
@@ -337,17 +343,20 @@ pub fn get_beacon_state_sync_committees<T: BeaconChainTypes>(
337343
// GET beacon/states/{state_id}/committees?slot,index,epoch
338344
pub fn get_beacon_state_committees<T: BeaconChainTypes>(
339345
beacon_states_path: BeaconStatesPath<T>,
346+
beacon_states_committees_filter: BeaconStatesCommitteesFilter,
340347
) -> ResponseFilter {
341348
beacon_states_path
342349
.clone()
343350
.and(warp::path("committees"))
344351
.and(warp::query::<eth2::types::CommitteesQuery>())
352+
.and(beacon_states_committees_filter)
345353
.and(warp::path::end())
346354
.then(
347355
|state_id: StateId,
348356
task_spawner: TaskSpawner<T::EthSpec>,
349357
chain: Arc<BeaconChain<T>>,
350-
query: eth2::types::CommitteesQuery| {
358+
query: eth2::types::CommitteesQuery,
359+
historical_committee_cache: Arc<HistoricalCommitteeCache>| {
351360
task_spawner.blocking_json_task(Priority::P1, move || {
352361
let (data, execution_optimistic, finalized) = state_id
353362
.map_state_and_execution_optimistic_and_finalized(
@@ -364,33 +373,33 @@ pub fn get_beacon_state_committees<T: BeaconChainTypes>(
364373
let shuffling_id = if let Ok(Some(shuffling_decision_block)) =
365374
chain.block_root_at_slot(decision_slot, WhenSlotSkipped::Prev)
366375
{
367-
Some(AttestationShufflingId {
368-
shuffling_epoch: epoch,
369-
shuffling_decision_block,
370-
})
376+
Some(HistoricalShufflingId::ShufflingId(
377+
AttestationShufflingId {
378+
shuffling_epoch: epoch,
379+
shuffling_decision_block,
380+
},
381+
))
382+
} else if epoch < chain.head().finalized_checkpoint().epoch {
383+
// Use the case for finalized epochs
384+
Some(HistoricalShufflingId::FinalizedEpoch(epoch))
371385
} else {
372386
None
373387
};
374388

375389
// Attempt to read from the chain cache if there exists a
376390
// shuffling_id
377-
let maybe_cached_shuffling = if let Some(shuffling_id) =
378-
shuffling_id.as_ref()
379-
{
380-
chain
381-
.shuffling_cache
382-
.try_write_for(std::time::Duration::from_secs(1))
383-
.and_then(|mut cache_write| cache_write.get(shuffling_id))
384-
.and_then(|cache_item| cache_item.wait().ok())
385-
} else {
386-
None
387-
};
391+
let maybe_cached_shuffling =
392+
if let Some(shuffling_id) = shuffling_id.as_ref() {
393+
historical_committee_cache.get(shuffling_id)
394+
} else {
395+
None
396+
};
388397

389398
let committee_cache =
390399
if let Some(shuffling) = maybe_cached_shuffling {
391400
shuffling
392401
} else {
393-
let possibly_built_cache = match RelativeEpoch::from_epoch(
402+
let committee_cache = match RelativeEpoch::from_epoch(
394403
current_epoch,
395404
epoch,
396405
) {
@@ -401,11 +410,19 @@ pub fn get_beacon_state_committees<T: BeaconChainTypes>(
401410
{
402411
state.committee_cache(relative_epoch).cloned()
403412
}
404-
_ => CommitteeCache::initialized(
405-
state,
406-
epoch,
407-
&chain.spec,
408-
),
413+
Ok(_) | Err(RelativeEpochError::EpochTooLow { .. }) => {
414+
CommitteeCache::initialized(
415+
state,
416+
epoch,
417+
&chain.spec,
418+
)
419+
}
420+
Err(RelativeEpochError::EpochTooHigh { .. }) => {
421+
Err(BeaconStateError::EpochOutOfBounds)
422+
}
423+
Err(RelativeEpochError::ArithError(e)) => {
424+
Err(BeaconStateError::ArithError(e))
425+
}
409426
}
410427
.map_err(|e| match e {
411428
BeaconStateError::EpochOutOfBounds => {
@@ -419,22 +436,12 @@ pub fn get_beacon_state_committees<T: BeaconChainTypes>(
419436
),
420437
})?;
421438

422-
// Attempt to write to the beacon cache (only if the cache
423-
// size is not the default value).
424-
if chain.config.shuffling_cache_size
425-
!= beacon_chain::shuffling_cache::DEFAULT_CACHE_SIZE
426-
&& let Some(shuffling_id) = shuffling_id
427-
&& let Some(mut cache_write) = chain
428-
.shuffling_cache
429-
.try_write_for(std::time::Duration::from_secs(1))
430-
{
431-
cache_write.insert_committee_cache(
432-
shuffling_id,
433-
&possibly_built_cache,
434-
);
439+
if let Some(shuffling_id) = shuffling_id {
440+
historical_committee_cache
441+
.insert(shuffling_id, committee_cache.clone());
435442
}
436443

437-
possibly_built_cache
444+
committee_cache
438445
};
439446

440447
// Use either the supplied slot or all slots in the epoch.

beacon_node/http_api/src/caches.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use lru::LruCache;
2+
use parking_lot::Mutex;
3+
use std::num::NonZeroUsize;
4+
use std::sync::Arc;
5+
use types::{AttestationShufflingId, CommitteeCache, Epoch};
6+
7+
/// See `shuffling_cache::DEFAULT_CACHE_SIZE` for rationale
8+
pub const DEFAULT_HISTORICAL_COMMITTEE_CACHE_SIZE: usize = 16;
9+
10+
/// Indexes the `HistoricalCommitteeCache`. We can compute committees for very old epochs, and we
11+
/// can't retrieve the decision root cheaply from a state. For those cases we allow the cache to
12+
/// key those committees by finalized epoch.
13+
#[derive(Eq, Hash, PartialEq)]
14+
pub enum HistoricalShufflingId {
15+
FinalizedEpoch(Epoch),
16+
ShufflingId(AttestationShufflingId),
17+
}
18+
19+
/// Dedicated cache for attestation committees, used exclusively by the HTTP API.
20+
///
21+
/// This may contain committees for finalized and unfinalized epochs. The name is slightly
22+
/// missleading :)
23+
pub struct HistoricalCommitteeCache {
24+
committees: Mutex<LruCache<HistoricalShufflingId, Arc<CommitteeCache>>>,
25+
}
26+
27+
impl HistoricalCommitteeCache {
28+
pub fn new(size: NonZeroUsize) -> Self {
29+
Self {
30+
committees: Mutex::new(LruCache::new(size)),
31+
}
32+
}
33+
}
34+
35+
impl HistoricalCommitteeCache {
36+
pub fn get(&self, id: &HistoricalShufflingId) -> Option<Arc<CommitteeCache>> {
37+
self.committees.lock().get(id).cloned()
38+
}
39+
40+
pub fn insert(&self, id: HistoricalShufflingId, cache: Arc<CommitteeCache>) {
41+
self.committees.lock().put(id, cache);
42+
}
43+
}

beacon_node/http_api/src/lib.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod beacon;
1212
mod block_id;
1313
mod build_block_contents;
1414
mod builder_states;
15+
mod caches;
1516
mod custody;
1617
mod database;
1718
mod light_client;
@@ -40,6 +41,8 @@ use crate::beacon::execution_payload_envelope::{
4041
post_beacon_execution_payload_envelope_ssz,
4142
};
4243
use crate::beacon::pool::*;
44+
use crate::caches::DEFAULT_HISTORICAL_COMMITTEE_CACHE_SIZE;
45+
pub use crate::caches::HistoricalCommitteeCache;
4346
use crate::light_client::{get_light_client_bootstrap, get_light_client_updates};
4447
use crate::utils::{AnyVersionFilter, EthV1Filter};
4548
use crate::validator::post_validator_liveness_epoch;
@@ -132,6 +135,7 @@ pub struct Context<T: BeaconChainTypes> {
132135
pub network_globals: Option<Arc<NetworkGlobals<T::EthSpec>>>,
133136
pub beacon_processor_send: Option<BeaconProcessorSend<T::EthSpec>>,
134137
pub sse_logging_components: Option<SSELoggingComponents>,
138+
pub historical_committee_cache: Arc<HistoricalCommitteeCache>,
135139
}
136140

137141
/// Configuration for the HTTP server.
@@ -148,6 +152,7 @@ pub struct Config {
148152
#[serde(with = "eth2::types::serde_status_code")]
149153
pub duplicate_block_status_code: StatusCode,
150154
pub target_peers: usize,
155+
pub historical_committee_cache_size: usize,
151156
}
152157

153158
impl Default for Config {
@@ -163,6 +168,7 @@ impl Default for Config {
163168
enable_beacon_processor: true,
164169
duplicate_block_status_code: StatusCode::ACCEPTED,
165170
target_peers: 100,
171+
historical_committee_cache_size: DEFAULT_HISTORICAL_COMMITTEE_CACHE_SIZE,
166172
}
167173
}
168174
}
@@ -416,6 +422,11 @@ pub fn serve<T: BeaconChainTypes>(
416422
})
417423
.boxed();
418424

425+
let historical_committee_cache = ctx.historical_committee_cache.clone();
426+
let beacon_states_committees_filter = warp::any()
427+
.map(move || historical_committee_cache.clone())
428+
.boxed();
429+
419430
// Create a `warp` filter that provides access to the network sender channel.
420431
let network_tx = ctx
421432
.network_senders
@@ -628,8 +639,10 @@ pub fn serve<T: BeaconChainTypes>(
628639
states::get_beacon_state_validators_id(beacon_states_path.clone());
629640

630641
// GET beacon/states/{state_id}/committees?slot,index,epoch
631-
let get_beacon_state_committees =
632-
states::get_beacon_state_committees(beacon_states_path.clone());
642+
let get_beacon_state_committees = states::get_beacon_state_committees(
643+
beacon_states_path.clone(),
644+
beacon_states_committees_filter,
645+
);
633646

634647
// GET beacon/states/{state_id}/sync_committees?epoch
635648
let get_beacon_state_sync_committees =

beacon_node/http_api/src/test_utils.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Config, Context};
1+
use crate::{Config, Context, caches::HistoricalCommitteeCache};
22
use beacon_chain::{
33
BeaconChain, BeaconChainTypes,
44
custody_context::NodeCustodyType,
@@ -22,10 +22,10 @@ use lighthouse_network::{
2222
};
2323
use network::{NetworkReceivers, NetworkSenders};
2424
use sensitive_url::SensitiveUrl;
25-
use std::future::Future;
2625
use std::net::SocketAddr;
2726
use std::sync::Arc;
2827
use std::time::Duration;
28+
use std::{future::Future, num::NonZeroUsize};
2929
use store::MemoryStore;
3030
use task_executor::test_utils::TestRuntime;
3131
use types::{ChainSpec, EthSpec};
@@ -293,6 +293,9 @@ pub async fn create_api_server_with_config<T: BeaconChainTypes>(
293293
network_globals: Some(network_globals),
294294
beacon_processor_send: Some(beacon_processor_send),
295295
sse_logging_components: None,
296+
historical_committee_cache: Arc::new(HistoricalCommitteeCache::new(
297+
NonZeroUsize::new(http_config.historical_committee_cache_size).unwrap(),
298+
)),
296299
});
297300

298301
let (listening_socket, server) =

beacon_node/src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ pub fn get_config<E: EthSpec>(
215215

216216
if let Some(cache_size) = clap_utils::parse_optional(cli_args, "shuffling-cache-size")? {
217217
client_config.chain.shuffling_cache_size = cache_size;
218+
// Mantain backwards compatibility with users customizing `shuffling_cache_size` to tweak
219+
// the behaviour of the HTTP API route `beacon/states/committees`
220+
client_config.http_api.historical_committee_cache_size = cache_size;
218221
}
219222

220223
if let Some(batches) = clap_utils::parse_optional(cli_args, "blob-publication-batches")? {

0 commit comments

Comments
 (0)