iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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:
Msrc/adapters/http.rs | 2+-
Msrc/adapters/http/api.rs | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/adapters/http/state.rs | 4+++-
Msrc/adapters/http/tests.rs | 9++++++++-
Msrc/adapters/http/ui.rs | 29+++++++++++++++++++++--------
Msrc/app/ledger_view.rs | 4++++
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() }