commit 9ab32fa995f6768b68c729a88b2ad7ba58d5cc0f
parent 7b998f3949f793b7201dfb9dca71337f476d4cab
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Tue, 11 Aug 2026 07:24:47 +0200
Improve UI polling performance
Diffstat:
13 files changed, 330 insertions(+), 164 deletions(-)
diff --git a/src/adapters/chain_store.rs b/src/adapters/chain_store.rs
@@ -14,7 +14,8 @@ use crate::domain::{
AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT, Amount, BLINDED_COMMITTER_FEE_BPS,
BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS, BlindedTransaction, Block, ChainSnapshot, Ledger,
MINE_REWARD, OutPoint, REVEAL_COMMITTEE_SIZE, Transaction, TxInput, TxOutput,
- blinded_reveal_finalizer_fee, hex_hash, revealed_blinded_transactions,
+ blinded_reveal_finalizer_fee, hex_hash, reveal_committee_slot_count,
+ revealed_blinded_transactions,
};
mod compact;
@@ -418,6 +419,15 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
let mut active_blinded = BTreeMap::<String, BlindedTransaction>::new();
let mut metric_utxos = metric_genesis_utxos(snapshot);
let mut metric_locked_blinded_inputs = BTreeMap::<String, Amount>::new();
+ let reveal_bundle_slots_by_height = ledger
+ .burn_leader_ranks_for_blocks(snapshot.blocks.iter().map(|block| block.height))
+ .map(|ranks_by_height| {
+ ranks_by_height
+ .into_iter()
+ .map(|(height, ranks)| (height, reveal_committee_slot_count(ranks.len())))
+ .collect::<BTreeMap<_, _>>()
+ })
+ .unwrap_or_default();
for block in &snapshot.blocks {
let revealed_transactions = revealed.get(&block.height).cloned().unwrap_or_default();
@@ -462,9 +472,9 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
&revealed.included_by,
block,
transaction.fee(),
- ledger
- .burn_leader_ranks_for_block(block.height)
- .map(|ranks| ranks.len())
+ reveal_bundle_slots_by_height
+ .get(&block.height)
+ .copied()
.unwrap_or(REVEAL_COMMITTEE_SIZE),
);
fees_amount = fees_amount
@@ -472,9 +482,9 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow>
.context("block metric fees overflow")?;
let committer_fee = blinded_fee_share(transaction.fee(), BLINDED_COMMITTER_FEE_BPS);
let included_reveal_bundle_count = block.included_reveal_bundle_count();
- let available_reveal_bundle_slots = ledger
- .burn_leader_ranks_for_block(block.height)
- .map(|ranks| ranks.len())
+ let available_reveal_bundle_slots = reveal_bundle_slots_by_height
+ .get(&block.height)
+ .copied()
.unwrap_or(REVEAL_COMMITTEE_SIZE);
let reveal_finalizer_fee = blinded_reveal_finalizer_fee(
transaction.fee(),
diff --git a/src/adapters/http.rs b/src/adapters/http.rs
@@ -69,9 +69,9 @@ 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, cached_chain_view, ui_blinded_reveal, ui_blinded_transaction,
- ui_blocks_from_indexes, ui_pending_revealed_transaction, ui_transaction,
- wallet_transaction_rows,
+ add_pending_outputs, burn_leader_ranks_for_blocks, cached_chain_view, ui_blinded_reveal,
+ ui_blinded_transaction, ui_blocks_from_indexes, ui_pending_revealed_transaction,
+ ui_transaction, wallet_transaction_rows,
};
#[cfg(test)]
use ui::{known_output_index, revealed_transactions_by_height, ui_block, ui_blocks};
@@ -98,9 +98,9 @@ const PEER_STALE_AFTER_MS: u64 = 20 * 60 * 1_000;
mod types;
use types::{
ActionResponse, AuthForm, AuthStatusResponse, BlocksQuery, ChangePasswordForm, ConfigForm,
- ConfigResponse, MempoolCounts, MetricsQuery, MetricsResponse, NetworkHealthResponse, Page,
- PageQuery, UiBlock, UiTransaction, WalletTransactionFilters, WalletTransactionRow,
- WalletTransactionsQuery, WalletUtxoRow,
+ ConfigResponse, MempoolCounts, MetricsQuery, MetricsResponse, NetworkHealthLocalState,
+ NetworkHealthResponse, Page, PageQuery, UiBlock, UiTransaction, WalletTransactionFilters,
+ WalletTransactionRow, WalletTransactionsQuery, WalletUtxoRow,
};
#[cfg(test)]
use types::{BurnSettingsForm, TransferForm};
@@ -126,7 +126,7 @@ pub async fn serve(
auth_backoff: Arc::new(Mutex::new(BTreeMap::new())),
ui_cache: Arc::new(Mutex::new(UiChainCache::default())),
};
- tokio::spawn(prewarm_chain_view_cache(state.clone()));
+ prewarm_chain_view_cache(state.clone()).await?;
tokio::spawn(run_owned_blinded_outbox_persistence(state.clone()));
let app = Router::new()
.route("/", get(index))
@@ -206,12 +206,13 @@ pub async fn serve(
.context("serving HTTP management UI")
}
-async fn prewarm_chain_view_cache(state: HttpState) {
+async fn prewarm_chain_view_cache(state: HttpState) -> Result<()> {
let snapshot = {
let node = state.node.lock().await;
node.chain_snapshot()
};
- let _ = cached_chain_view(&state, &snapshot).await;
+ let _ = cached_chain_view(&state, &snapshot).await?;
+ Ok(())
}
async fn api_config_form(
diff --git a/src/adapters/http/api.rs b/src/adapters/http/api.rs
@@ -14,14 +14,14 @@ use crate::{
use super::{
BlocksQuery, ConfigResponse, MempoolCounts, MetricsQuery, MetricsResponse,
- NetworkHealthResponse, Page, PageQuery, UiBlock, UiTransaction, WalletTransactionFilters,
- WalletTransactionRow, WalletTransactionsQuery, WalletUtxoRow,
+ NetworkHealthLocalState, NetworkHealthResponse, Page, PageQuery, UiBlock, UiTransaction,
+ WalletTransactionFilters, WalletTransactionRow, WalletTransactionsQuery, WalletUtxoRow,
};
use super::{
DATASET_LIMIT, DATASET_PAGE_LIMIT, EXPLORER_LIMIT, EXPLORER_PAGE_LIMIT, HttpState,
- add_pending_outputs, cached_chain_view, metrics_response, network_health, ui_blinded_reveal,
- ui_blinded_transaction, ui_blocks_from_indexes, ui_pending_revealed_transaction,
- ui_transaction, wallet_transaction_rows,
+ add_pending_outputs, burn_leader_ranks_for_blocks, cached_chain_view, metrics_response,
+ network_health, ui_blinded_reveal, ui_blinded_transaction, ui_blocks_from_indexes,
+ ui_pending_revealed_transaction, ui_transaction, wallet_transaction_rows,
};
pub(super) async fn api_status(State(state): State<HttpState>) -> Json<NodeStatus> {
@@ -38,7 +38,7 @@ pub(super) async fn api_blocks(
.limit
.unwrap_or(EXPLORER_PAGE_LIMIT)
.min(EXPLORER_LIMIT);
- let (snapshot, pending, blocks, burn_leader_ranks) = {
+ let (snapshot, pending, blocks) = {
let node = state.node.lock().await;
let snapshot = node.chain_snapshot();
let pending = node.pending_transactions();
@@ -46,19 +46,12 @@ pub(super) async fn api_blocks(
Some(before_height) => node.blocks_before(before_height, limit),
None => node.recent_blocks(limit),
};
- let burn_leader_ranks = blocks
- .iter()
- .map(|block| {
- (
- block.hash.clone(),
- node.burn_leader_ranks_for_block(block.height)
- .unwrap_or_default(),
- )
- })
- .collect::<BTreeMap<_, _>>();
- (snapshot, pending, blocks, burn_leader_ranks)
+ (snapshot, pending, blocks)
};
- let view = cached_chain_view(&state, &snapshot).await;
+ let view = cached_chain_view(&state, &snapshot)
+ .await
+ .unwrap_or_default();
+ let burn_leader_ranks = burn_leader_ranks_for_blocks(&snapshot, &blocks);
let mut outputs = view.outputs;
add_pending_outputs(&mut outputs, &pending);
Json(ui_blocks_from_indexes(
@@ -100,7 +93,9 @@ pub(super) async fn api_mempool(
pending_revealed,
)
};
- let view = cached_chain_view(&state, &snapshot).await;
+ let view = cached_chain_view(&state, &snapshot)
+ .await
+ .unwrap_or_default();
let mut outputs = view.outputs;
add_pending_outputs(&mut outputs, &pending);
let mut items = pending
@@ -135,7 +130,9 @@ pub(super) async fn api_wallet_transactions(
node.owned_blinded_payloads(),
)
};
- let view = cached_chain_view(&state, &snapshot).await;
+ let view = cached_chain_view(&state, &snapshot)
+ .await
+ .unwrap_or_default();
let mut outputs = view.outputs;
add_pending_outputs(&mut outputs, &pending);
let page_query = query.page();
@@ -281,17 +278,21 @@ pub(super) async fn api_metrics(
pub(super) async fn api_network_health(
State(state): State<HttpState>,
) -> Json<NetworkHealthResponse> {
- let (status, mempool) = {
+ let (local, mempool) = {
let node = state.node.lock().await;
+ let mempool = MempoolCounts {
+ plain_transactions: node.pending_transactions().len(),
+ blinded_transactions: node.pending_blinded_transactions().len(),
+ blinded_reveals: node.pending_blinded_reveals().len(),
+ };
(
- node.status(),
- MempoolCounts {
- plain_transactions: node.pending_transactions().len(),
- blinded_transactions: node.pending_blinded_transactions().len(),
- blinded_reveals: node.pending_blinded_reveals().len(),
+ NetworkHealthLocalState {
+ height: node.chain_height(),
+ pending_transactions: mempool.total(),
},
+ mempool,
)
};
let peers = state.peers.lock().await.list();
- Json(network_health(&status, &peers, mempool))
+ Json(network_health(local, &peers, mempool))
}
diff --git a/src/adapters/http/metrics.rs b/src/adapters/http/metrics.rs
@@ -1,6 +1,6 @@
use crate::{
adapters::chain_store::BlockMetricRow,
- app::{NodeStatus, PeerDirection, PeerInfo},
+ app::{PeerDirection, PeerInfo},
domain::Amount,
};
@@ -8,16 +8,16 @@ use super::{
PEER_STALE_AFTER_MS, now_ms,
types::{
MempoolCounts, MetricsChart, MetricsPoint, MetricsResponse, MetricsValueKind,
- NetworkHealthResponse,
+ NetworkHealthLocalState, NetworkHealthResponse,
},
};
pub(super) fn network_health(
- status: &NodeStatus,
+ local: NetworkHealthLocalState,
peers: &[PeerInfo],
mempool: MempoolCounts,
) -> NetworkHealthResponse {
- network_health_at(status, peers, mempool, now_ms())
+ network_health_at(local, peers, mempool, now_ms())
}
pub(super) fn metrics_response(enabled: bool, rows: Vec<BlockMetricRow>) -> MetricsResponse {
@@ -152,12 +152,12 @@ fn micro_iuna_as_iuna(amount: Amount) -> f64 {
}
pub(super) fn network_health_at(
- status: &NodeStatus,
+ local: NetworkHealthLocalState,
peers: &[PeerInfo],
mempool: MempoolCounts,
now_ms: u64,
) -> NetworkHealthResponse {
- let local_height = status.chain.height;
+ let local_height = local.height;
let remote_best_height = peers.iter().filter_map(|peer| peer.last_known_height).max();
let best_known_height = remote_best_height.unwrap_or(local_height).max(local_height);
let healthy_heights = peers
@@ -246,7 +246,7 @@ pub(super) fn network_health_at(
failed_peers,
stale_peers,
banned_peers,
- pending_transactions: status.chain.pending_transactions,
+ pending_transactions: local.pending_transactions,
pending_plain_transactions: mempool.plain_transactions,
pending_blinded_transactions: mempool.blinded_transactions,
pending_blinded_reveals: mempool.blinded_reveals,
diff --git a/src/adapters/http/state.rs b/src/adapters/http/state.rs
@@ -45,7 +45,7 @@ pub(super) struct UiChainCache {
pub(super) revealed_by_height: BTreeMap<u64, Vec<RevealedBlindedTransaction>>,
}
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Default)]
pub(super) struct UiChainView {
pub(super) outputs: BTreeMap<OutPoint, TxOutput>,
pub(super) revealed_by_height: BTreeMap<u64, Vec<RevealedBlindedTransaction>>,
diff --git a/src/adapters/http/tests.rs b/src/adapters/http/tests.rs
@@ -865,13 +865,17 @@ async fn network_health_summarizes_sync_and_peer_errors() {
let dir = tempfile::tempdir().unwrap();
let state = auth_test_state(dir.path().join("config.json"), UiConfig::default()).await;
let status = state.node.lock().await.status();
+ let local = super::NetworkHealthLocalState {
+ height: status.chain.height,
+ pending_transactions: status.chain.pending_transactions,
+ };
let mempool = super::MempoolCounts {
plain_transactions: 1,
blinded_transactions: 2,
blinded_reveals: 3,
};
- let isolated = super::network_health(&status, &[], mempool);
+ let isolated = super::network_health(local, &[], mempool);
assert!(!isolated.ok);
assert_eq!(isolated.state, "isolated");
assert_eq!(isolated.local_height, 0);
@@ -893,12 +897,12 @@ async fn network_health_summarizes_sync_and_peer_errors() {
11 * 60 * 1_000,
10_000,
);
- let clock_health = super::network_health_at(&status, &clock_peers.list(), mempool, 10_000);
+ let clock_health = super::network_health_at(local, &clock_peers.list(), mempool, 10_000);
assert_eq!(clock_health.network_time_offset_ms, Some(500));
assert_eq!(clock_health.bad_clock_peers, 1);
let syncing = super::network_health(
- &status,
+ local,
&[PeerInfo {
address: "127.0.0.1:9445".to_string(),
direction: PeerDirection::Outbound,
@@ -925,7 +929,7 @@ async fn network_health_summarizes_sync_and_peer_errors() {
assert_eq!(syncing.lag_blocks, 3);
let peer_errors = super::network_health(
- &status,
+ local,
&[PeerInfo {
address: "127.0.0.1:9446".to_string(),
direction: PeerDirection::Outbound,
@@ -954,7 +958,7 @@ async fn network_health_summarizes_sync_and_peer_errors() {
);
let stale = super::network_health_at(
- &status,
+ local,
&[PeerInfo {
address: "127.0.0.1:9447".to_string(),
direction: PeerDirection::Outbound,
@@ -981,7 +985,7 @@ async fn network_health_summarizes_sync_and_peer_errors() {
assert_eq!(stale.stale_peers, 1);
let banned = super::network_health_at(
- &status,
+ local,
&[PeerInfo {
address: "127.0.0.1:9448".to_string(),
direction: PeerDirection::Outbound,
@@ -1676,6 +1680,26 @@ fn block_detail_finalizer_opens_burn_leader_ranks_modal() {
assert!(super::INDEX_HTML.contains("rank.ticket_id ?? rank.ticketId"));
}
+#[tokio::test]
+async fn startup_prewarm_populates_chain_view_cache_before_first_request() {
+ let dir = tempfile::tempdir().unwrap();
+ let state = auth_test_state(dir.path().join("config.json"), UiConfig::default()).await;
+ let expected_tip = {
+ let node = state.node.lock().await;
+ node.chain_snapshot()
+ .blocks
+ .last()
+ .map(|block| block.hash.clone())
+ };
+ assert!(state.ui_cache.lock().await.tip_hash.is_none());
+
+ super::prewarm_chain_view_cache(state.clone())
+ .await
+ .unwrap();
+
+ assert_eq!(state.ui_cache.lock().await.tip_hash, expected_tip);
+}
+
async fn auth_test_state(config_path: std::path::PathBuf, config: UiConfig) -> HttpState {
config_store::save(&config_path, &config).unwrap();
let wallet_path = config_path.with_file_name("wallet.json");
diff --git a/src/adapters/http/types.rs b/src/adapters/http/types.rs
@@ -52,6 +52,20 @@ pub(super) struct MempoolCounts {
pub(super) blinded_reveals: usize,
}
+impl MempoolCounts {
+ pub(super) fn total(&self) -> usize {
+ self.plain_transactions
+ .saturating_add(self.blinded_transactions)
+ .saturating_add(self.blinded_reveals)
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub(super) struct NetworkHealthLocalState {
+ pub(super) height: u64,
+ pub(super) pending_transactions: usize,
+}
+
#[derive(Debug, Deserialize)]
pub(super) struct BurnSettingsForm {
pub(super) enabled: Option<bool>,
diff --git a/src/adapters/http/ui.rs b/src/adapters/http/ui.rs
@@ -5,7 +5,7 @@ use crate::domain::{
BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS, BlindedReveal, BlindedTransaction, Block, BurnLeaderRank,
ChainSnapshot, Ledger, MINE_REWARD, OutPoint, REVEAL_COMMITTEE_SIZE,
RevealedBlindedTransaction, Transaction, TxInput, TxOutput, blinded_reveal_finalizer_fee,
- hex_hash, revealed_blinded_transactions,
+ hex_hash, reveal_committee_slot_count, revealed_blinded_transactions,
};
use super::{
@@ -240,6 +240,32 @@ pub(super) fn ui_blocks_from_indexes(
.collect()
}
+pub(super) fn burn_leader_ranks_for_blocks(
+ snapshot: &ChainSnapshot,
+ blocks: &[Block],
+) -> BTreeMap<String, Vec<BurnLeaderRank>> {
+ let Some(ranks_by_height) = Ledger::from_persisted_snapshot(snapshot.clone())
+ .ok()
+ .and_then(|ledger| {
+ ledger
+ .burn_leader_ranks_for_blocks(blocks.iter().map(|block| block.height))
+ .ok()
+ })
+ else {
+ return BTreeMap::new();
+ };
+
+ blocks
+ .iter()
+ .filter_map(|block| {
+ ranks_by_height
+ .get(&block.height)
+ .cloned()
+ .map(|ranks| (block.hash.clone(), ranks))
+ })
+ .collect()
+}
+
pub(super) fn ui_block(
block: Block,
outputs: &BTreeMap<OutPoint, TxOutput>,
@@ -566,40 +592,52 @@ pub(super) fn known_output_index(
outputs
}
-pub(super) async fn cached_chain_view(state: &HttpState, snapshot: &ChainSnapshot) -> UiChainView {
+pub(super) async fn cached_chain_view(
+ state: &HttpState,
+ snapshot: &ChainSnapshot,
+) -> anyhow::Result<UiChainView> {
let tip_hash = snapshot.blocks.last().map(|block| block.hash.clone());
{
let cache = state.ui_cache.lock().await;
if cache.tip_hash == tip_hash {
- return UiChainView {
+ return Ok(UiChainView {
outputs: cache.outputs.clone(),
revealed_by_height: cache.revealed_by_height.clone(),
- };
+ });
}
}
- let outputs = known_chain_output_index(snapshot);
- let revealed_by_height = revealed_transactions_by_height(snapshot);
+ let (computed_tip_hash, view) = tokio::task::spawn_blocking({
+ let snapshot = snapshot.clone();
+ move || build_chain_view(&snapshot)
+ })
+ .await?;
let mut cache = state.ui_cache.lock().await;
if cache.tip_hash == tip_hash {
- return UiChainView {
+ return Ok(UiChainView {
outputs: cache.outputs.clone(),
revealed_by_height: cache.revealed_by_height.clone(),
- };
+ });
}
- let view = UiChainView {
- outputs,
- revealed_by_height,
- };
- cache.tip_hash = tip_hash;
+ cache.tip_hash = computed_tip_hash;
cache.outputs = view.outputs.clone();
cache.revealed_by_height = view.revealed_by_height.clone();
- UiChainView {
+ Ok(UiChainView {
outputs: view.outputs,
revealed_by_height: view.revealed_by_height,
- }
+ })
+}
+
+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),
+ },
+ )
}
fn known_chain_output_index(snapshot: &ChainSnapshot) -> BTreeMap<OutPoint, TxOutput> {
@@ -622,22 +660,7 @@ fn known_chain_output_index(snapshot: &ChainSnapshot) -> BTreeMap<OutPoint, TxOu
.iter()
.map(|block| (block.height, block))
.collect::<BTreeMap<_, _>>();
- let reveal_bundle_slots_by_height = Ledger::from_persisted_snapshot(snapshot.clone())
- .ok()
- .map(|ledger| {
- snapshot
- .blocks
- .iter()
- .map(|block| {
- let slots = ledger
- .burn_leader_ranks_for_block(block.height)
- .map(|ranks| ranks.len())
- .unwrap_or(REVEAL_COMMITTEE_SIZE);
- (block.height, slots)
- })
- .collect::<BTreeMap<_, _>>()
- })
- .unwrap_or_default();
+ let reveal_bundle_slots_by_height = reveal_bundle_slots_by_height(snapshot);
let blinded_by_commitment = snapshot
.blocks
.iter()
@@ -718,6 +741,23 @@ fn known_chain_output_index(snapshot: &ChainSnapshot) -> BTreeMap<OutPoint, TxOu
outputs
}
+fn reveal_bundle_slots_by_height(snapshot: &ChainSnapshot) -> BTreeMap<u64, usize> {
+ Ledger::from_persisted_snapshot(snapshot.clone())
+ .ok()
+ .and_then(|ledger| {
+ ledger
+ .burn_leader_ranks_for_blocks(snapshot.blocks.iter().map(|block| block.height))
+ .ok()
+ })
+ .map(|ranks_by_height| {
+ ranks_by_height
+ .into_iter()
+ .map(|(height, ranks)| (height, reveal_committee_slot_count(ranks.len())))
+ .collect()
+ })
+ .unwrap_or_default()
+}
+
pub(super) fn add_pending_outputs(
outputs: &mut BTreeMap<OutPoint, TxOutput>,
pending: &[Transaction],
diff --git a/src/app/automatic_mining.rs b/src/app/automatic_mining.rs
@@ -212,7 +212,7 @@ impl NodeCore {
&mut self,
timestamp_ms: u64,
) -> Result<Option<Transaction>> {
- let current_height = self.ledger.status().height;
+ let current_height = self.ledger.height();
if !self.automatic_mining_enabled {
return Ok(None);
}
@@ -240,7 +240,7 @@ impl NodeCore {
}
fn prepare_automatic_anchor_burn(&mut self, timestamp_ms: u64) -> Result<Option<Transaction>> {
- let current_height = self.ledger.status().height;
+ let current_height = self.ledger.height();
if !self.automatic_burn_needs_plaintext_anchor(timestamp_ms) {
return Ok(None);
}
@@ -377,8 +377,7 @@ impl NodeCore {
if self.ledger.finalizer_rank_count_for_next_block() > 0 {
return false;
}
- let tip_hash = self.ledger.status().tip_hash;
- recovery_vdf_sample_percent(self.wallet.address(), tip_hash.as_str())
+ recovery_vdf_sample_percent(self.wallet.address(), self.ledger.tip_hash())
< self.recovery_vdf_top_rank_percent
}
diff --git a/src/app/gossip.rs b/src/app/gossip.rs
@@ -52,24 +52,22 @@ impl NodeCore {
}
pub fn hello(&self, listen_addr: Option<String>, node_id: Option<String>) -> GossipEnvelope {
- let status = self.ledger.status();
GossipEnvelope::Hello(ProtocolHello {
protocol_version: PROTOCOL_VERSION,
network_id: NETWORK_ID.to_string(),
genesis_hash: self.ledger.genesis_hash().to_string(),
listen_addr,
node_id,
- height: status.height,
- tip_hash: status.tip_hash,
+ height: self.ledger.height(),
+ tip_hash: self.ledger.tip_hash().to_string(),
time_ms: now_ms(),
})
}
pub fn peer_status(&self) -> GossipEnvelope {
- let status = self.ledger.status();
GossipEnvelope::PeerStatus {
- height: status.height,
- tip_hash: status.tip_hash,
+ height: self.ledger.height(),
+ tip_hash: self.ledger.tip_hash().to_string(),
time_ms: now_ms(),
}
}
diff --git a/src/app/status.rs b/src/app/status.rs
@@ -15,7 +15,7 @@ use super::{
impl NodeCore {
pub fn status(&self) -> NodeStatus {
- let chain = self.ledger.status();
+ let chain = self.ledger.light_status();
let launch_profile = self.ledger.launch_profile();
let current_leader = self.ledger.expected_leader_for_next_block();
let wallet_is_current_leader = current_leader
@@ -195,6 +195,24 @@ mod tests {
}
#[test]
+ fn status_omits_full_balance_map_for_ui_polling() {
+ let wallet = Wallet::from_seed("status-light-wallet");
+ let mut allocations = BTreeMap::new();
+ allocations.insert(wallet.address().to_string(), 10);
+ allocations.insert(
+ Wallet::from_seed("status-light-peer").address().to_string(),
+ 5,
+ );
+ let ledger = Ledger::new(allocations, 1);
+ let node = NodeCore::from_ledger(wallet, ledger, 0);
+
+ let status = node.status();
+
+ assert_eq!(status.wallet_balance, 10);
+ assert!(status.chain.balances.is_empty());
+ }
+
+ #[test]
fn automatic_pow_status_reports_setup_placeholder_wait() {
let wallet = Wallet::from_seed("automatic-pow-setup-placeholder-wallet");
let ledger = Ledger::new(BTreeMap::new(), 1);
diff --git a/src/domain/ledger_queries.rs b/src/domain/ledger_queries.rs
@@ -8,7 +8,7 @@ use super::blinded::{
use super::genesis::balances_from_utxos;
use super::mine_policy::mine_anchor;
use super::ticket::{
- apply_finalizer_ticket_effects, genesis_tickets, ranked_tickets_for_height,
+ BurnTicket, apply_finalizer_ticket_effects, genesis_tickets, ranked_tickets_for_height,
tickets_created_by_block, tickets_created_by_transactions,
};
use super::{
@@ -17,6 +17,55 @@ use super::{
Transaction, TxOutput, reveal_committee_slot_count,
};
+fn apply_historical_ticket_block(
+ block: &Block,
+ launch_profile: &LaunchProfile,
+ tickets: &mut Vec<BurnTicket>,
+ active_blinded: &mut BTreeMap<String, ActiveBlindedTransaction>,
+) -> Result<()> {
+ apply_finalizer_ticket_effects(block, tickets)?;
+ tickets.extend(tickets_created_by_block(block, launch_profile)?);
+ let mut revealed_transactions = Vec::new();
+ for reveal in block.all_blinded_reveals() {
+ let active = active_blinded.get(&reveal.commitment).with_context(|| {
+ format!(
+ "block {} reveals unknown blinded transaction {}",
+ block.height, reveal.commitment
+ )
+ })?;
+ let transaction = decrypt_blinded_transaction(&active.transaction, reveal)?;
+ if matches!(transaction, Transaction::Mine { .. }) {
+ bail!("mine actions are public and cannot be blinded");
+ }
+ if blinded_envelope_fee_for_transaction(&transaction) != active.transaction.fee {
+ bail!(
+ "block {} blinded reveal fee does not match envelope",
+ block.height
+ );
+ }
+ revealed_transactions.push(transaction);
+ active_blinded.remove(&reveal.commitment);
+ }
+ tickets.extend(tickets_created_by_transactions(
+ block.height,
+ &revealed_transactions,
+ launch_profile,
+ )?);
+ active_blinded.retain(|_, active| block.height < active.transaction.expires_at_height);
+ for transaction in &block.blinded_transactions {
+ active_blinded.insert(
+ transaction.commitment.clone(),
+ ActiveBlindedTransaction {
+ transaction: transaction.clone(),
+ locked_outputs: Vec::new(),
+ included_height: block.height,
+ included_by: block.miner.clone(),
+ },
+ );
+ }
+ Ok(())
+}
+
impl Ledger {
pub fn snapshot(&self) -> ChainSnapshot {
ChainSnapshot {
@@ -28,6 +77,14 @@ impl Ledger {
}
pub fn status(&self) -> ChainStatus {
+ self.status_with_balances(true)
+ }
+
+ pub fn light_status(&self) -> ChainStatus {
+ self.status_with_balances(false)
+ }
+
+ fn status_with_balances(&self, include_balances: bool) -> ChainStatus {
ChainStatus {
height: self.tip().height,
tip_hash: self.tip().hash.clone(),
@@ -35,91 +92,91 @@ impl Ledger {
launch_profile_hash: self.launch_profile.hash(),
mine_reward: self.mine_reward,
current_mine_difficulty_bits: self.current_mine_difficulty_bits(),
- balances: balances_from_utxos(&self.utxos),
+ balances: include_balances
+ .then(|| balances_from_utxos(&self.utxos))
+ .unwrap_or_default(),
pending_transactions: self.pending.len()
+ self.pending_blinded.len()
+ self.pending_reveals.len(),
}
}
+ pub fn tip_hash(&self) -> &str {
+ &self.tip().hash
+ }
+
pub fn chain(&self) -> &[Block] {
&self.chain
}
pub fn burn_leader_ranks_for_block(&self, height: u64) -> Result<Vec<BurnLeaderRank>> {
- if height == 0 {
- return Ok(Vec::new());
+ Ok(self
+ .burn_leader_ranks_for_blocks([height])?
+ .remove(&height)
+ .unwrap_or_default())
+ }
+
+ pub fn burn_leader_ranks_for_blocks<I>(
+ &self,
+ heights: I,
+ ) -> Result<BTreeMap<u64, Vec<BurnLeaderRank>>>
+ where
+ I: IntoIterator<Item = u64>,
+ {
+ let mut requested = heights.into_iter().collect::<BTreeSet<_>>();
+ let mut ranks_by_height = BTreeMap::new();
+ if requested.remove(&0) {
+ ranks_by_height.insert(0, Vec::new());
}
- let parent_index = height.checked_sub(1).context("block height underflows")? as usize;
- let parent = self
- .chain
- .get(parent_index)
- .with_context(|| format!("missing parent block for height {height}"))?;
+ if requested.is_empty() {
+ return Ok(ranks_by_height);
+ }
+
let mut tickets = genesis_tickets(
&self.genesis_allocations,
&self.chain[0],
&self.launch_profile,
)?;
let mut active_blinded = BTreeMap::<String, ActiveBlindedTransaction>::new();
- for block in self
- .chain
- .iter()
- .skip(1)
- .take_while(|block| block.height < height)
- {
- apply_finalizer_ticket_effects(block, &mut tickets)?;
- tickets.extend(tickets_created_by_block(block, &self.launch_profile)?);
- let mut revealed_transactions = Vec::new();
- for reveal in block.all_blinded_reveals() {
- let active = active_blinded.get(&reveal.commitment).with_context(|| {
- format!(
- "block {} reveals unknown blinded transaction {}",
- block.height, reveal.commitment
- )
- })?;
- let transaction = decrypt_blinded_transaction(&active.transaction, reveal)?;
- if matches!(transaction, Transaction::Mine { .. }) {
- bail!("mine actions are public and cannot be blinded");
+ let mut next_block_index = 1;
+
+ for height in requested {
+ let parent_index = height.checked_sub(1).context("block height underflows")? as usize;
+ let parent = self
+ .chain
+ .get(parent_index)
+ .with_context(|| format!("missing parent block for height {height}"))?;
+ while let Some(block) = self.chain.get(next_block_index) {
+ if block.height >= height {
+ break;
}
- if blinded_envelope_fee_for_transaction(&transaction) != active.transaction.fee {
- bail!(
- "block {} blinded reveal fee does not match envelope",
- block.height
- );
- }
- revealed_transactions.push(transaction);
- active_blinded.remove(&reveal.commitment);
- }
- tickets.extend(tickets_created_by_transactions(
- block.height,
- &revealed_transactions,
- &self.launch_profile,
- )?);
- active_blinded.retain(|_, active| block.height < active.transaction.expires_at_height);
- for transaction in &block.blinded_transactions {
- active_blinded.insert(
- transaction.commitment.clone(),
- ActiveBlindedTransaction {
- transaction: transaction.clone(),
- locked_outputs: Vec::new(),
- included_height: block.height,
- included_by: block.miner.clone(),
- },
- );
+ apply_historical_ticket_block(
+ block,
+ &self.launch_profile,
+ &mut tickets,
+ &mut active_blinded,
+ )?;
+ next_block_index += 1;
}
+
+ ranks_by_height.insert(
+ height,
+ ranked_tickets_for_height(parent, height, &tickets)
+ .into_iter()
+ .enumerate()
+ .map(|(rank, ticket)| BurnLeaderRank {
+ rank: rank as u32,
+ ticket_id: ticket.id,
+ owner: ticket.owner,
+ amount: ticket.amount,
+ eligible_from_height: ticket.eligible_from_height,
+ eligible_until_height: ticket.eligible_until_height,
+ })
+ .collect(),
+ );
}
- Ok(ranked_tickets_for_height(parent, height, &tickets)
- .into_iter()
- .enumerate()
- .map(|(rank, ticket)| BurnLeaderRank {
- rank: rank as u32,
- ticket_id: ticket.id,
- owner: ticket.owner,
- amount: ticket.amount,
- eligible_from_height: ticket.eligible_from_height,
- eligible_until_height: ticket.eligible_until_height,
- })
- .collect())
+
+ Ok(ranks_by_height)
}
pub fn reveal_committee_for_next_block(&self) -> Vec<RevealCommitteeMember> {
diff --git a/src/domain/tests.rs b/src/domain/tests.rs
@@ -1412,6 +1412,10 @@ fn burn_leader_ranks_for_block_reconstructs_historical_ticket_order() {
assert_eq!(ranks[0].owner, leader);
assert!(ranks.iter().all(|rank| rank.amount == MICRO_IUNA));
assert_eq!(ledger.burn_leader_ranks_for_block(0).unwrap(), Vec::new());
+
+ let batch = ledger.burn_leader_ranks_for_blocks([0, 1]).unwrap();
+ assert!(batch.get(&0).unwrap().is_empty());
+ assert_eq!(batch.get(&1), Some(&ranks));
}
#[test]