commit 69cbfa488b0ed867a426b6c0e27cd75a2b548ab6
parent 258bcdce620fda3c6c67f255afad18b9122e4cd5
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Tue, 11 Aug 2026 09:00:10 +0200
Use warm UI cache for chain endpoints
Diffstat:
6 files changed, 104 insertions(+), 26 deletions(-)
diff --git a/src/adapters/http.rs b/src/adapters/http.rs
@@ -69,7 +69,7 @@ pub use state::ServeOptions;
use state::{AuthClientKey, AuthSession, HttpState, UiChainCache, UiChainView};
use static_assets::{alpine_js, app_js, favicon, index};
use ui::{
- add_pending_outputs, burn_leader_ranks_for_blocks, cached_chain_view, ui_blinded_reveal,
+ add_pending_outputs, cached_chain_view, cached_chain_view_for_tip, ui_blinded_reveal,
ui_blinded_transaction, ui_blocks_from_indexes, ui_pending_revealed_transaction,
ui_transaction, wallet_transaction_rows,
};
diff --git a/src/adapters/http/api.rs b/src/adapters/http/api.rs
@@ -19,7 +19,7 @@ use super::{
};
use super::{
DATASET_LIMIT, DATASET_PAGE_LIMIT, EXPLORER_LIMIT, EXPLORER_PAGE_LIMIT, HttpState,
- add_pending_outputs, burn_leader_ranks_for_blocks, cached_chain_view, metrics_response,
+ add_pending_outputs, cached_chain_view, cached_chain_view_for_tip, metrics_response,
network_health, ui_blinded_reveal, ui_blinded_transaction, ui_blocks_from_indexes,
ui_pending_revealed_transaction, ui_transaction, wallet_transaction_rows,
};
@@ -38,27 +38,42 @@ pub(super) async fn api_blocks(
.limit
.unwrap_or(EXPLORER_PAGE_LIMIT)
.min(EXPLORER_LIMIT);
- let (snapshot, pending, blocks) = {
+ let (tip_hash, pending, blocks) = {
let node = state.node.lock().await;
- let snapshot = node.chain_snapshot();
let pending = node.pending_transactions();
let blocks = match query.before_height {
Some(before_height) => node.blocks_before(before_height, limit),
None => node.recent_blocks(limit),
};
- (snapshot, pending, blocks)
+ (node.chain_tip_hash(), pending, blocks)
};
- let view = cached_chain_view(&state, &snapshot)
- .await
- .unwrap_or_default();
- let burn_leader_ranks = burn_leader_ranks_for_blocks(&snapshot, &blocks);
+ let (view, pending, blocks) =
+ match cached_chain_view_for_tip(&state, Some(tip_hash.as_str())).await {
+ Some(view) => (view, pending, blocks),
+ None => {
+ let (snapshot, pending, blocks) = {
+ let node = state.node.lock().await;
+ let snapshot = node.chain_snapshot();
+ let pending = node.pending_transactions();
+ let blocks = match query.before_height {
+ Some(before_height) => node.blocks_before(before_height, limit),
+ None => node.recent_blocks(limit),
+ };
+ (snapshot, pending, blocks)
+ };
+ let view = cached_chain_view(&state, &snapshot)
+ .await
+ .unwrap_or_default();
+ (view, pending, blocks)
+ }
+ };
let mut outputs = view.outputs;
add_pending_outputs(&mut outputs, &pending);
Json(ui_blocks_from_indexes(
blocks,
&outputs,
&view.revealed_by_height,
- &burn_leader_ranks,
+ &view.burn_leader_ranks_by_hash,
))
}
@@ -74,9 +89,8 @@ pub(super) async fn api_mempool(
State(state): State<HttpState>,
Query(query): Query<PageQuery>,
) -> Json<Page<UiTransaction>> {
- let (snapshot, pending, pending_blinded, pending_reveals, pending_revealed) = {
+ let (tip_hash, pending, pending_blinded, pending_reveals, pending_revealed) = {
let node = state.node.lock().await;
- let snapshot = node.chain_snapshot();
let pending = node.pending_transactions();
let pending_blinded = node.pending_blinded_transactions();
let pending_reveals = node.pending_blinded_reveals();
@@ -86,16 +100,54 @@ pub(super) async fn api_mempool(
.map(|revealed| (revealed.commitment.clone(), revealed))
.collect::<BTreeMap<_, _>>();
(
- snapshot,
+ node.chain_tip_hash(),
pending,
pending_blinded,
pending_reveals,
pending_revealed,
)
};
- let view = cached_chain_view(&state, &snapshot)
- .await
- .unwrap_or_default();
+ let (view, pending, pending_blinded, pending_reveals, pending_revealed) =
+ match cached_chain_view_for_tip(&state, Some(tip_hash.as_str())).await {
+ Some(view) => (
+ view,
+ pending,
+ pending_blinded,
+ pending_reveals,
+ pending_revealed,
+ ),
+ None => {
+ let (snapshot, pending, pending_blinded, pending_reveals, pending_revealed) = {
+ let node = state.node.lock().await;
+ let snapshot = node.chain_snapshot();
+ let pending = node.pending_transactions();
+ let pending_blinded = node.pending_blinded_transactions();
+ let pending_reveals = node.pending_blinded_reveals();
+ let pending_revealed = node
+ .pending_revealed_blinded_transactions()
+ .into_iter()
+ .map(|revealed| (revealed.commitment.clone(), revealed))
+ .collect::<BTreeMap<_, _>>();
+ (
+ snapshot,
+ pending,
+ pending_blinded,
+ pending_reveals,
+ pending_revealed,
+ )
+ };
+ let view = cached_chain_view(&state, &snapshot)
+ .await
+ .unwrap_or_default();
+ (
+ view,
+ pending,
+ pending_blinded,
+ pending_reveals,
+ pending_revealed,
+ )
+ }
+ };
let mut outputs = view.outputs;
add_pending_outputs(&mut outputs, &pending);
let mut items = pending
diff --git a/src/adapters/http/state.rs b/src/adapters/http/state.rs
@@ -5,7 +5,7 @@ use tokio::sync::Mutex;
use crate::{
adapters::{chain_store::SqliteChainStore, config_store::UiConfig, p2p::GossipNetwork},
app::{SharedNode, SharedPeerBook, StratumStatus},
- domain::{OutPoint, RevealedBlindedTransaction, TxOutput},
+ domain::{BurnLeaderRank, OutPoint, RevealedBlindedTransaction, TxOutput},
};
#[derive(Clone)]
@@ -43,12 +43,14 @@ pub(super) struct UiChainCache {
pub(super) tip_hash: Option<String>,
pub(super) outputs: BTreeMap<OutPoint, TxOutput>,
pub(super) revealed_by_height: BTreeMap<u64, Vec<RevealedBlindedTransaction>>,
+ pub(super) burn_leader_ranks_by_hash: BTreeMap<String, Vec<BurnLeaderRank>>,
}
#[derive(Clone, Debug, Default)]
pub(super) struct UiChainView {
pub(super) outputs: BTreeMap<OutPoint, TxOutput>,
pub(super) revealed_by_height: BTreeMap<u64, Vec<RevealedBlindedTransaction>>,
+ pub(super) burn_leader_ranks_by_hash: BTreeMap<String, Vec<BurnLeaderRank>>,
}
pub struct ServeOptions {
diff --git a/src/adapters/http/tests.rs b/src/adapters/http/tests.rs
@@ -1706,7 +1706,14 @@ async fn startup_prewarm_populates_chain_view_cache_before_first_request() {
.await
.unwrap();
- assert_eq!(state.ui_cache.lock().await.tip_hash, expected_tip);
+ let cache = state.ui_cache.lock().await;
+ assert_eq!(cache.tip_hash, expected_tip);
+ assert!(
+ cache
+ .tip_hash
+ .as_ref()
+ .is_some_and(|tip_hash| cache.burn_leader_ranks_by_hash.contains_key(tip_hash))
+ );
}
async fn auth_test_state(config_path: std::path::PathBuf, config: UiConfig) -> HttpState {
diff --git a/src/adapters/http/ui.rs b/src/adapters/http/ui.rs
@@ -600,10 +600,7 @@ pub(super) async fn cached_chain_view(
{
let cache = state.ui_cache.lock().await;
if cache.tip_hash == tip_hash {
- return Ok(UiChainView {
- outputs: cache.outputs.clone(),
- revealed_by_height: cache.revealed_by_height.clone(),
- });
+ return Ok(ui_chain_view_from_cache(&cache));
}
}
@@ -615,27 +612,43 @@ pub(super) async fn cached_chain_view(
let mut cache = state.ui_cache.lock().await;
if cache.tip_hash == tip_hash {
- return Ok(UiChainView {
- outputs: cache.outputs.clone(),
- revealed_by_height: cache.revealed_by_height.clone(),
- });
+ return Ok(ui_chain_view_from_cache(&cache));
}
cache.tip_hash = computed_tip_hash;
cache.outputs = view.outputs.clone();
cache.revealed_by_height = view.revealed_by_height.clone();
+ cache.burn_leader_ranks_by_hash = view.burn_leader_ranks_by_hash.clone();
Ok(UiChainView {
outputs: view.outputs,
revealed_by_height: view.revealed_by_height,
+ burn_leader_ranks_by_hash: view.burn_leader_ranks_by_hash,
})
}
+pub(super) async fn cached_chain_view_for_tip(
+ state: &HttpState,
+ tip_hash: Option<&str>,
+) -> Option<UiChainView> {
+ let cache = state.ui_cache.lock().await;
+ (cache.tip_hash.as_deref() == tip_hash).then(|| ui_chain_view_from_cache(&cache))
+}
+
+fn ui_chain_view_from_cache(cache: &super::UiChainCache) -> UiChainView {
+ UiChainView {
+ outputs: cache.outputs.clone(),
+ revealed_by_height: cache.revealed_by_height.clone(),
+ burn_leader_ranks_by_hash: cache.burn_leader_ranks_by_hash.clone(),
+ }
+}
+
fn build_chain_view(snapshot: &ChainSnapshot) -> (Option<String>, UiChainView) {
(
snapshot.blocks.last().map(|block| block.hash.clone()),
UiChainView {
outputs: known_chain_output_index(snapshot),
revealed_by_height: revealed_transactions_by_height(snapshot),
+ burn_leader_ranks_by_hash: burn_leader_ranks_for_blocks(snapshot, &snapshot.blocks),
},
)
}
diff --git a/src/app/ledger_view.rs b/src/app/ledger_view.rs
@@ -30,6 +30,10 @@ impl NodeCore {
self.ledger.height()
}
+ pub fn chain_tip_hash(&self) -> String {
+ self.ledger.tip_hash().to_string()
+ }
+
pub fn has_real_chain(&self) -> bool {
!self.ledger.is_setup_placeholder()
}