diff --git a/src/html/page/announcement.rs b/src/html/page/announcement.rs index 0077faf..d14cf10 100644 --- a/src/html/page/announcement.rs +++ b/src/html/page/announcement.rs @@ -4,6 +4,7 @@ use crate::http_util::rpc_method_err; use crate::model::announcement_selector::AnnouncementSelector; use crate::model::announcement_type::AnnouncementType; use crate::model::app_state::AppState; +use crate::model::transparent_utxo_tuple::TransparentUtxoTuple; use axum::extract::rejection::PathRejection; use axum::extract::Path; use axum::extract::State; @@ -15,6 +16,8 @@ use neptune_cash::api::export::BlockHeight; use neptune_cash::prelude::tasm_lib::prelude::Digest; use neptune_cash::prelude::triton_vm::prelude::BFieldCodec; use neptune_cash::prelude::twenty_first::tip5::Tip5; +use neptune_cash::util_types::mutator_set::addition_record::AdditionRecord; +use std::collections::HashMap; use std::sync::Arc; use tarpc::context; @@ -32,6 +35,7 @@ pub async fn announcement_page( block_hash: Digest, block_height: BlockHeight, announcement_type: AnnouncementType, + addition_record_indices: HashMap>, } let state = &state_rw.load(); @@ -73,6 +77,58 @@ pub async fn announcement_page( .clone(); let announcement_type = AnnouncementType::parse(announcement); + let mut addition_record_indices = HashMap::>::new(); + if let AnnouncementType::TransparentTxInfo(tx_info) = announcement_type.clone() { + let addition_records = tx_info + .outputs + .iter() + .map(|output| output.addition_record()) + .collect::>(); + addition_record_indices = state.rpc_client.addition_record_indices_for_block(context::current(), state.token(), block_selector, &addition_records).await + .map_err(|e| not_found_html_response(state, Some(e.to_string())))? + .map_err(rpc_method_err)? + .expect( + "block guaranteed to exist because we got here; getting its announcements should work", + ); + + let mut transparent_utxos_cache = state.transparent_utxos_cache.lock().await; + + for input in &tx_info.inputs { + let addition_record = input.addition_record(); + if let Some(existing_entry) = transparent_utxos_cache + .iter_mut() + .find(|tu| tu.addition_record() == addition_record) + { + existing_entry.upgrade_with_transparent_input(input, block_hash); + } else { + tracing::info!("Adding transparent UTXO (input side) to cache."); + transparent_utxos_cache.push(TransparentUtxoTuple::new_from_transparent_input( + input, block_hash, + )); + } + } + + for output in &tx_info.outputs { + let addition_record = output.addition_record(); + if let Some(existing_entry) = transparent_utxos_cache + .iter_mut() + .find(|tu| tu.addition_record() == addition_record) + { + existing_entry.upgrade_with_transparent_output(block_hash); + } else { + tracing::info!("Adding transparent UTXO (output side) to cache."); + transparent_utxos_cache.push(TransparentUtxoTuple::new_from_transparent_output( + output, + addition_record_indices + .get(&addition_record) + .cloned() + .unwrap_or(None), + block_hash, + )); + } + } + } + let header = HeaderHtml { state }; let utxo_page = AnnouncementHtmlPage { @@ -82,6 +138,7 @@ pub async fn announcement_page( block_height, num_announcements, announcement_type, + addition_record_indices, }; Ok(Html(utxo_page.to_string())) } diff --git a/src/html/page/utxo.rs b/src/html/page/utxo.rs index 7343a7a..9fbd9ec 100644 --- a/src/html/page/utxo.rs +++ b/src/html/page/utxo.rs @@ -2,6 +2,7 @@ use crate::html::component::header::HeaderHtml; use crate::html::page::not_found::not_found_html_response; use crate::http_util::rpc_method_err; use crate::model::app_state::AppState; +use crate::model::transparent_utxo_tuple::TransparentUtxoTuple; use axum::extract::rejection::PathRejection; use axum::extract::Path; use axum::extract::State; @@ -9,6 +10,7 @@ use axum::response::Html; use axum::response::Response; use html_escaper::Escape; use html_escaper::Trusted; +use neptune_cash::api::export::Tip5; use neptune_cash::prelude::tasm_lib::prelude::Digest; use std::sync::Arc; use tarpc::context; @@ -24,16 +26,18 @@ pub async fn utxo_page( header: HeaderHtml<'a>, index: u64, digest: Digest, + transparent_utxo_info: Option, } let state = &state_rw.load(); + let cache = state.transparent_utxos_cache.clone(); let Path(index) = index_maybe.map_err(|e| not_found_html_response(state, Some(e.to_string())))?; let digest = match state .rpc_client - .utxo_digest(context::current(), state.token(), index) + .utxo_digest(context::current(), state.token(), index, cache) .await .map_err(|e| not_found_html_response(state, Some(e.to_string())))? .map_err(rpc_method_err)? @@ -49,10 +53,19 @@ pub async fn utxo_page( let header = HeaderHtml { state }; + let transparent_utxo_info = state + .transparent_utxos_cache + .lock() + .await + .iter() + .find(|tu| tu.aocl_leaf_index().is_some_and(|li| li == index)) + .cloned(); + let utxo_page = UtxoHtmlPage { index, header, digest, + transparent_utxo_info, }; Ok(Html(utxo_page.to_string())) } diff --git a/src/model/app_state.rs b/src/model/app_state.rs index a5f5855..0f880de 100644 --- a/src/model/app_state.rs +++ b/src/model/app_state.rs @@ -1,4 +1,5 @@ use crate::model::config::Config; +use crate::model::transparent_utxo_tuple::TransparentUtxoTuple; use crate::neptune_rpc; use anyhow::Context; use arc_swap::ArcSwap; @@ -8,6 +9,7 @@ use neptune_cash::models::blockchain::block::block_selector::BlockSelector; use neptune_cash::prelude::twenty_first::tip5::Digest; use neptune_cash::rpc_auth; use std::sync::Arc; +use tokio::sync::Mutex; #[derive(Debug, Clone)] pub struct AppStateInner { @@ -15,6 +17,12 @@ pub struct AppStateInner { pub config: Config, pub rpc_client: neptune_rpc::AuthenticatedClient, pub genesis_digest: Digest, + + /// Whenever an announcement of type transparent transaction info is fetched + /// from the RPC endpoint, we learn information about UTXOs. Since we expect + /// transparent transactions to be rare, it is okay to cache this in RAM + /// instead of storing it on disk. + pub transparent_utxos_cache: Arc>>, } impl AppStateInner { @@ -26,6 +34,12 @@ impl AppStateInner { #[derive(Clone)] pub struct AppState(Arc>); +impl AppState { + fn new(app_state_inner: AppStateInner) -> Self { + Self(Arc::new(ArcSwap::from_pointee(app_state_inner))) + } +} + impl std::ops::Deref for AppState { type Target = Arc>; @@ -34,24 +48,6 @@ impl std::ops::Deref for AppState { } } -impl From<(Network, Config, neptune_rpc::AuthenticatedClient, Digest)> for AppState { - fn from( - (network, config, rpc_client, genesis_digest): ( - Network, - Config, - neptune_rpc::AuthenticatedClient, - Digest, - ), - ) -> Self { - Self(Arc::new(ArcSwap::from_pointee(AppStateInner { - network, - config, - rpc_client, - genesis_digest, - }))) - } -} - impl AppState { pub async fn init() -> Result { let rpc_client = neptune_rpc::gen_authenticated_rpc_client() @@ -68,12 +64,13 @@ impl AppState { .with_context(|| "Failed calling neptune-core api method: block_digest")? .with_context(|| "neptune-core failed to provide a genesis block")?; - Ok(Self::from(( - rpc_client.network, - Config::parse(), + Ok(AppState::new(AppStateInner { + network: rpc_client.network, + config: Config::parse(), rpc_client, genesis_digest, - ))) + transparent_utxos_cache: Arc::new(Mutex::new(vec![])), + })) } /// Sets the rpc_client @@ -95,6 +92,7 @@ impl AppState { rpc_client, config: inner.config.clone(), genesis_digest: inner.genesis_digest, + transparent_utxos_cache: Arc::new(Mutex::new(vec![])), }; self.0.store(Arc::new(new_inner)); } diff --git a/src/model/mod.rs b/src/model/mod.rs index 8f7447a..463d924 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -4,3 +4,4 @@ pub mod app_state; pub mod block_selector_extended; pub mod config; pub mod height_or_digest; +pub mod transparent_utxo_tuple; diff --git a/src/neptune_rpc.rs b/src/neptune_rpc.rs index c6f187b..3d6c9dc 100644 --- a/src/neptune_rpc.rs +++ b/src/neptune_rpc.rs @@ -1,6 +1,7 @@ use crate::alert_email; use crate::model::app_state::AppState; use crate::model::config::Config; +use crate::model::transparent_utxo_tuple::TransparentUtxoTuple; use anyhow::Context; use chrono::DateTime; use chrono::TimeDelta; @@ -17,11 +18,15 @@ use neptune_cash::rpc_auth; use neptune_cash::rpc_server::error::RpcError; use neptune_cash::rpc_server::RPCClient; use neptune_cash::rpc_server::RpcResult; +use neptune_cash::util_types::mutator_set::addition_record::AdditionRecord; +use std::collections::HashMap; use std::net::Ipv4Addr; use std::net::SocketAddr; +use std::sync::Arc; use tarpc::client; use tarpc::context; use tarpc::tokio_serde::formats::Json as RpcJson; +use tokio::sync::Mutex; use tracing::{debug, info, warn}; #[cfg(feature = "mock")] @@ -92,8 +97,32 @@ impl AuthenticatedClient { ctx: ::tarpc::context::Context, token: rpc_auth::Token, leaf_index: u64, + _transparent_utxos_cache: Arc>>, ) -> ::core::result::Result>, ::tarpc::client::RpcError> { - self.client.utxo_digest(ctx, token, leaf_index).await + let rpc_result = self.client.utxo_digest(ctx, token, leaf_index).await; + + if let Ok(Ok(Some(_))) = rpc_result { + return rpc_result; + } + + // if MOCK environment variable is set and feature is enabled, + // imagine some mock UTXO info + #[cfg(feature = "mock")] + if std::env::var(MOCK_KEY).is_ok() { + let cache = _transparent_utxos_cache.lock().await; + tracing::warn!( + "RPC query failed and MOCK flag set, so seeing if we can return a cached utxo; cache has {} objects", cache.len() + ); + if let Some(entry) = cache + .iter() + .find(|tu| tu.aocl_leaf_index().is_some_and(|li| li == leaf_index)) + { + tracing::warn!("returning a cached utxo"); + return Ok(Ok(Some(entry.addition_record().canonical_commitment))); + } + } + + rpc_result } /// Intercept and relay call to [`RPCClient::announcements_in_block`] @@ -123,7 +152,9 @@ impl AuthenticatedClient { use rand::rngs::StdRng; use rand::Rng; use rand::SeedableRng; - tracing::warn!("RPC query failed and MOCK flag set, so returning an imagined block"); + tracing::warn!( + "RPC query failed and MOCK flag set, so returning an imagined announcement" + ); let mut hasher = Hasher::new(); hasher.update(&block_selector.to_string().bytes().collect::>()); let mut rng = StdRng::from_seed(*hasher.finalize().as_bytes()); @@ -156,6 +187,67 @@ impl AuthenticatedClient { // otherwise, return the original error rpc_result } + + /// Intercept and relay call to + /// [`RPCClient::addition_record_indices_for_block`]. + /// + /// Also take an extra argument for mocking purposes. + pub async fn addition_record_indices_for_block( + &self, + ctx: ::tarpc::context::Context, + token: rpc_auth::Token, + block_selector: BlockSelector, + _addition_records: &[AdditionRecord], + ) -> ::core::result::Result< + RpcResult>>>, + ::tarpc::client::RpcError, + > { + let rpc_result = self + .client + .addition_record_indices_for_block(ctx, token, block_selector) + .await; + + // if the RPC call was successful, return that + if let Ok(Ok(Some(_))) = rpc_result { + return rpc_result; + } + + // if MOCK environment variable is set and feature is enabled, + // imagine some mock hash map + #[cfg(feature = "mock")] + if std::env::var(MOCK_KEY).is_ok() { + use blake3::Hasher; + use rand::rngs::StdRng; + use rand::Rng; + use rand::SeedableRng; + tracing::warn!( + "RPC query failed and MOCK flag set, so returning an imagined addition records" + ); + let mut hasher = Hasher::new(); + hasher.update(&block_selector.to_string().bytes().collect::>()); + let mut rng = StdRng::from_seed(*hasher.finalize().as_bytes()); + + let aocl_offset = rng.random::() >> 1; + let addition_record_indices = _addition_records + .iter() + .enumerate() + .map(|(i, ar)| { + ( + *ar, + if rng.random_bool(0.5_f64) { + Some(i as u64 + aocl_offset) + } else { + None + }, + ) + }) + .collect::>(); + return Ok(Ok(Some(addition_record_indices))); + } + + // otherwise, return the original error + rpc_result + } } /// generates RPCClient, for querying neptune-core RPC server. diff --git a/src/rpc/utxo_digest.rs b/src/rpc/utxo_digest.rs index 9a01bb3..ee27c54 100644 --- a/src/rpc/utxo_digest.rs +++ b/src/rpc/utxo_digest.rs @@ -18,9 +18,10 @@ pub async fn utxo_digest( State(state): State>, ) -> Result, impl IntoResponse> { let s = state.load(); + let cache = s.transparent_utxos_cache.clone(); match s .rpc_client - .utxo_digest(context::current(), s.token(), index) + .utxo_digest(context::current(), s.token(), index, cache) .await .map_err(rpc_err)? .map_err(rpc_method_err)? diff --git a/templates/web/html/page/announcement.html b/templates/web/html/page/announcement.html index 32e23ee..f04b750 100644 --- a/templates/web/html/page/announcement.html +++ b/templates/web/html/page/announcement.html @@ -87,7 +87,13 @@
+ {% if let Some(Some(aocl_leaf_index)) = + self.addition_record_indices.get(&output.addition_record()) { %} + {{output.addition_record().canonical_commitment.to_hex()}} + {% } else { %} {{output.addition_record().canonical_commitment.to_hex()}} + {% } %} diff --git a/templates/web/html/page/utxo.html b/templates/web/html/page/utxo.html index d059cb1..4544ba5 100644 --- a/templates/web/html/page/utxo.html +++ b/templates/web/html/page/utxo.html @@ -2,43 +2,92 @@ {{self.header.state.config.site_name}}: Utxo {{self.index}} -{{html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"), "/templates/web/html/components/head.html")))}} + {{html_escaper::Trusted(include_str!( concat!(env!("CARGO_MANIFEST_DIR"), + "/templates/web/html/components/head.html")))}} -{{Trusted(self.header.to_string())}} + {{Trusted(self.header.to_string())}} -
+
-
- - ⓘ - - UTXO = Unspent Transaction Output. It represents an output of transaction A which can also be an input to transaction B. - - - UTXO Information - -
- - - - - - - - -
Index{{self.index}}
Digest{{self.digest.to_hex()}}
- +
+ + ⓘ + + UTXO = Unspent Transaction Output. It represents an output of transaction A which can also be an + input to transaction B. + + + UTXO Information + + + + + + + + + + +
AOCL Leaf Index{{self.index}}
Addition Record{{self.digest.to_hex()}}
+
- + {% if let Some(utxo_info) = &self.transparent_utxo_info { %} +
+ + ⓘ + + UTXOs consumed or produced by a transparent transaction disclose the information they otherwise + hide. + + + Transparent UTXO Information + + + + + + + + + + + + + + + {% if let Some(receiver_preimage) = utxo_info.receiver_preimage() { %} + + + + + {% } %} + {% if utxo_info.utxo().has_native_currency() { %} + + + + + {% } %} + {% if let Some(release_date) = utxo_info.utxo().release_date() { %} + + + + + {% } %} +
UTXO Digest:{{Tip5::hash(&utxo_info.utxo()).to_hex()}}
Sender Randomness:{{utxo_info.sender_randomness().to_hex()}}
Receiver Digest{{utxo_info.receiver_digest().to_hex()}}
Receiver Preimage{{receiver_preimage.to_hex()}}
Amount:{{utxo_info.utxo().get_native_currency_amount().display_n_decimals(5)}} NPT
Time-Locked Until{{release_date.standard_format()}}
+
+ {% } %} - + + + + \ No newline at end of file