Compare commits

..

10 Commits

Author SHA1 Message Date
Alan Szepieniec
8f900fd70a deps: Use new upstream types
Also:
 - Fix parse errors resulting from upstream changes.
 - Upgrade dependencies (except for potentially breaking ones).
2025-10-27 10:03:19 +01:00
Alan Szepieniec
edf7800a88 security: Add attack binary
Add a binary that deploys a flooding attack.

Also, add a binary that periodically queries a random URL from the website. This
scraper will be used to measure whether the attack (or its countermeasure) is
effective.
2025-10-09 18:32:51 +02:00
sword-smith
ec6d99e9d0 refactor: Change supply return type from f64 to i32
On the request of some aggregator that allegedly wanted the number in
total number of coins, not in the atomic units.
2025-10-03 00:13:08 +02:00
sword-smith
349f0705d3 chore: Update neptune-cash dependency 2025-10-03 00:03:01 +02:00
sword_smith
21bb64a276
feat: Add endpoint for total supply
Total supply is defined as liquid supply plus timelocked supply.
2025-10-02 17:29:04 +02:00
sword_smith
d22fe50de6
feat: Add endpoint for circulating supply 2025-10-02 15:57:38 +02:00
sword_smith
552923ae40
chore: Update neptune-core dependency
Also: format imports.
2025-10-02 15:56:49 +02:00
Alan Szepieniec
817835302b fix(RPC): Encode hashmap as list for transmission
In commit 1096a293 on neptune-core, the return type of RPC endpoint
`addition_records_for_block` changes from `HashMap` to `Vec`. This
commit applies the matching change on the side of the client,
including building the original hashmap from the transmitted list.

These commits (the present one and the one referenced) fix the issue
that transparent transaction info could not be transferred across the
RPC layer.
2025-08-23 10:31:34 +02:00
Alan Szepieniec
847e4f1e3d feat: Add link to source code
Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
2025-08-22 16:40:26 +02:00
Alan Szepieniec
1de0c69405 chore(README.md): Clarify roles of site name and domain 2025-08-22 16:38:30 +02:00
31 changed files with 1399 additions and 617 deletions

1326
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,53 +2,70 @@
name = "neptune-explorer"
version = "0.1.0"
edition = "2021"
default-run = "neptune-explorer"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "high_rate_attack"
path = "src/bin/high_rate_attack.rs"
required-features = ["attacks"]
[[bin]]
name = "scraper"
path = "src/bin/scraper.rs"
required-features = ["attacks"]
[dependencies]
axum = { version = "0.7.9", features = ["macros"] }
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
tokio = { version = "1.37.0", features = ["full", "tracing"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
tokio = { version = "1.48.0", features = ["full", "tracing"] }
tracing = "0.1"
tracing-subscriber = "0.3"
neptune-cash = "0.3.0"
neptune-cash = "0.4.0"
tarpc = { version = "^0.34", features = [
"tokio1",
"serde-transport",
"serde-transport-json",
"tcp",
] }
clap = "4.5.4"
thiserror = "1.0.59"
boilerplate = { version = "1.0.0" }
clap = "4.5.50"
thiserror = "1.0.69"
boilerplate = { version = "1.0.1" }
html-escaper = "0.2.0"
tower-http = { version = "0.5.2", features = ["fs"] }
readonly = "0.2.12"
url = "2.5.0"
lettre = {version = "0.11.7", features = ["tokio1-native-tls"]}
chrono = "0.4.34"
readonly = "0.2.13"
url = "2.5.7"
lettre = { version = "0.11.19", features = ["tokio1-native-tls"] }
chrono = "0.4.42"
# only should be used inside main.rs, for the binary.
anyhow = "1.0.86"
anyhow = "1.0.100"
arc-swap = "1.7.1"
derive_more = { version = "1.0.0", features = ["display"] }
# not a direct dep. workaround for weird "could not resolve" cargo error
indexmap = "2.7.0"
indexmap = "2.12.0"
blake3 = { version = "1.8.2", optional = true }
rand = { version = "0.9.2", optional = true }
reqwest = { version = "0.12.24", optional = true }
log = {version = "0.4.28", optional = true}
env_logger = {version = "0.11.8", optional = true}
regex = {version = "1.12.2", optional = true }
futures = {version = "0.3.31", optional = true }
#[dev-dependencies]
test-strategy = "0.4.3"
proptest = "1.7.0"
arbitrary = "1.4.1"
proptest = "1.9.0"
arbitrary = "1.4.2"
proptest-arbitrary-interop = "0.1.0"
[patch.crates-io]
neptune-cash = { git = "https://github.com/Neptune-Crypto/neptune-core.git", branch = "asz/transparent-transactions" }
neptune-cash = { git = "https://github.com/Neptune-Crypto/neptune-core.git", rev = "71f471a526a13ddd41ab4400e1a873471d03ede6" }
[features]
mock = ["dep:blake3", "dep:rand"]
attacks = ["reqwest", "log", "env_logger", "regex", "dep:rand", "futures"]

View File

@ -34,14 +34,15 @@ not tested or supported. Please let us know if you get it work. patches accep
2. start neptune-explorer
```
nohup neptune-explorer 2>&1 > /path/to/logs/neptune-explorer.log &
nohup neptune-explorer --site-domain testdomain 2>&1 > /path/to/logs/neptune-explorer.log &
```
Notes:
* The block-explorer automatically uses the same network (mainnet, testnet, etc) as the neptune-core instance it is connected to, and the network is displayed in the web interface.
* If neptune-core RPC server is running on a non-standard port, you can provide it with the `--neptune-rpc-port` flag.
* neptune-explorer listens for http requests on port 3000 by default. This can be changed with the `--listen-port` flag.
* Site name must be specified with the `--site-name` flag.
* Site name can be specified with the --site-name flag.
* Site domain *must* be specified with the `--site-domain` flag.
## Connecting via Browser
@ -52,7 +53,7 @@ Just navigate to http://localhost:3000/
When connected to an out-of-date or unsynced neptune-core node, it might be a good idea to turn on mocking so that whenever a resource is unavailable, a random one is generated and returned. To do this, compile with the feature flag "mock" and make sure that the "MOCK" environment variable is set.
In one command: `MOCK=1 cargo run --features "mock" -- --site-name testname`
In one command: `MOCK=1 cargo run --features "mock" -- --site-domain testdomain`
## SSL/TLS, Nginx, etc.

View File

@ -1,14 +1,15 @@
use clap::Parser;
use lettre::AsyncSmtpTransport;
use lettre::AsyncTransport;
use lettre::Message;
use lettre::Tokio1Executor;
use tracing::info;
use tracing::warn;
use crate::model::app_state::AppState;
use crate::model::config::AlertConfig;
use crate::model::config::Config;
use crate::model::config::SmtpMode;
use clap::Parser;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use tracing::{info, warn};
// pub fn alert_params_configured() -> bool {
// Config::parse().alert_config().is_some()
// }
pub fn check_alert_params() -> bool {
match Config::parse().alert_config() {

View File

@ -0,0 +1,27 @@
use futures::future::join_all;
use reqwest::Client;
use tokio::time::Instant;
#[tokio::main]
async fn main() {
let client = Client::new();
let url = "http://127.0.0.1:3000/your_endpoint";
let num_requests = 200; // adjust as needed
let concurrency = 20; // parallel tasks
let start = Instant::now();
let futures = (0..num_requests).map(|_| {
let client = &client;
async move {
let _ = client.get(url).send().await;
}
});
join_all(futures).await;
let elapsed = start.elapsed();
println!(
"Sent {} requests in {:.2?} (concurrency {})",
num_requests, elapsed, concurrency
);
}

102
src/bin/scraper.rs Normal file
View File

@ -0,0 +1,102 @@
use log::LevelFilter;
use log::{error, info, warn};
use rand::seq::IteratorRandom;
use regex::Regex;
use reqwest::Client;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::{signal, time};
use url::Url;
/// Scrape the explorer website when running locally.
///
/// This program maintains a dictionary of URLs, which is initially populated
/// with 'http://localhost:3000'. It fetches a random URL from the dictionary in
/// each iteration, logs positive messages if successful, extracts new URLs from
/// the response body to add to the dictionary, logs warnings or errors for
/// request failures or timeouts, sleeps for a bit, and continues until Ctrl-C
/// is pressed.
///
/// Run with:
/// `> cargo run --bin scraper`
#[tokio::main]
async fn main() {
// Initialize logger
env_logger::builder().filter_level(LevelFilter::Info).init();
let client = Client::builder()
.timeout(Duration::from_millis(300))
.build()
.expect("Failed to build HTTP client");
let root_url = "http://localhost:3000".to_string();
let urls = Arc::new(Mutex::new(HashSet::from([root_url.clone()])));
let href_regex = Regex::new(r#"<a\s+(?:[^>]*?\s+)?href=['\"](.*?)['\"]"#).unwrap();
info!("Starting fetch loop. Press Ctrl-C to stop.");
let urls_clone = Arc::clone(&urls);
let fetch_loop = async move {
loop {
// Pick a random URL safely
let url_opt = {
let urls_guard = urls_clone.lock().unwrap();
urls_guard.iter().choose(&mut rand::rng()).cloned()
};
if let Some(url) = url_opt {
match client.get(&url).send().await {
Ok(resp) => {
if resp.status().is_success() {
match resp.text().await {
Ok(text) => {
info!("Success fetching {}", url);
let mut urls_guard = urls_clone.lock().unwrap();
for cap in href_regex.captures_iter(&text) {
let href = &cap[1];
if let Ok(parsed_url) =
Url::parse(&[&root_url.clone(), href].concat())
{
let normalized = parsed_url.as_str();
if urls_guard.insert(normalized.to_owned()) {
info!(
"Added new URL to dictionary: {}",
normalized
);
}
}
}
}
Err(e) => {
warn!("Failed to read response body from {}: {}", url, e);
}
}
} else {
warn!("Non-success status {} from {}", resp.status(), url);
}
}
Err(err) => {
if err.is_timeout() {
warn!("Timeout fetching {}", url);
} else {
error!("Error fetching {}: {}", url, err);
}
}
}
} else {
warn!("URL dictionary is empty, no URL to fetch");
}
time::sleep(Duration::from_millis(500)).await;
}
};
tokio::select! {
_ = fetch_loop => {}, // This runs indefinitely unless stopped
_ = signal::ctrl_c() => {
info!("Ctrl-C received, stopping...");
}
}
}

View File

@ -1,6 +1,7 @@
use crate::model::app_state::AppStateInner;
use html_escaper::Escape;
use crate::model::app_state::AppStateInner;
#[derive(Debug, Clone, boilerplate::Boilerplate)]
#[boilerplate(filename = "web/html/components/header.html")]
pub struct HeaderHtml<'a> {

View File

@ -1,10 +1,6 @@
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::announcement_selector::AnnouncementSelector;
use crate::model::announcement_type::AnnouncementType;
use crate::model::app_state::AppState;
use crate::model::transparent_utxo_tuple::TransparentUtxoTuple;
use std::collections::HashMap;
use std::sync::Arc;
use axum::extract::rejection::PathRejection;
use axum::extract::Path;
use axum::extract::State;
@ -17,10 +13,16 @@ 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;
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::announcement_selector::AnnouncementSelector;
use crate::model::announcement_type::AnnouncementType;
use crate::model::app_state::AppState;
use crate::model::transparent_utxo_tuple::TransparentUtxoTuple;
#[axum::debug_handler]
pub async fn announcement_page(
maybe_path: Result<Path<AnnouncementSelector>, PathRejection>,
@ -84,12 +86,19 @@ pub async fn announcement_page(
.iter()
.map(|output| output.addition_record())
.collect::<Vec<_>>();
addition_record_indices = state.rpc_client.addition_record_indices_for_block(context::current(), state.token(), block_selector, &addition_records).await
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",
);
.into_iter()
.collect::<HashMap<_, _>>();
let mut transparent_utxos_cache = state.transparent_utxos_cache.lock().await;

View File

@ -1,8 +1,5 @@
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::block_selector_extended::BlockSelectorExtended;
use std::sync::Arc;
use axum::extract::rejection::PathRejection;
use axum::extract::Path;
use axum::extract::State;
@ -10,10 +7,15 @@ use axum::response::Html;
use axum::response::Response;
use html_escaper::Escape;
use html_escaper::Trusted;
use neptune_cash::models::blockchain::block::block_info::BlockInfo;
use std::sync::Arc;
use neptune_cash::protocol::consensus::block::block_info::BlockInfo;
use tarpc::context;
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::block_selector_extended::BlockSelectorExtended;
#[axum::debug_handler]
pub async fn block_page(
user_input_maybe: Result<Path<BlockSelectorExtended>, PathRejection>,

View File

@ -1,11 +1,12 @@
use crate::http_util::not_found_html_err;
use crate::http_util::not_found_html_handler;
use crate::model::app_state::AppStateInner;
use axum::http::StatusCode;
use axum::response::Html;
use axum::response::Response;
use html_escaper::Escape;
use crate::http_util::not_found_html_err;
use crate::http_util::not_found_html_handler;
use crate::model::app_state::AppStateInner;
pub fn not_found_page(error_msg: Option<String>) -> Html<String> {
#[derive(boilerplate::Boilerplate)]
#[boilerplate(filename = "web/html/page/not_found.html")]

View File

@ -1,17 +1,14 @@
use std::collections::HashSet;
use std::sync::Arc;
use axum::extract::RawQuery;
use axum::extract::State;
use axum::response::IntoResponse;
use axum::response::Redirect;
use axum::response::Response;
use std::sync::Arc;
// use axum::routing::get;
// use axum::routing::Router;
use super::not_found::not_found_html_response;
use axum::response::IntoResponse;
use std::collections::HashSet;
// use super::root::root;
// use super::utxo::utxo_page;
use crate::model::app_state::AppState;
// use neptune_explorer::model::config::Config;
/// This converts a query string into a path and redirects browser.
///

View File

@ -1,15 +1,17 @@
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::app_state::AppStateInner;
use std::sync::Arc;
use axum::extract::State;
use axum::response::Html;
use axum::response::Response;
use html_escaper::Escape;
use neptune_cash::models::blockchain::block::block_height::BlockHeight;
use std::sync::Arc;
use neptune_cash::api::export::BlockHeight;
use tarpc::context;
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::app_state::AppStateInner;
#[axum::debug_handler]
pub async fn root(State(state_rw): State<Arc<AppState>>) -> Result<Html<String>, Response> {
#[derive(boilerplate::Boilerplate)]

View File

@ -1,8 +1,5 @@
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 std::sync::Arc;
use axum::extract::rejection::PathRejection;
use axum::extract::Path;
use axum::extract::State;
@ -12,9 +9,14 @@ 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;
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;
#[axum::debug_handler]
pub async fn utxo_page(
index_maybe: Result<Path<u64>, PathRejection>,

View File

@ -2,7 +2,7 @@ use axum::http::StatusCode;
use axum::response::Html;
use axum::response::IntoResponse;
use axum::response::Response;
use neptune_cash::rpc_server::error::RpcError;
use neptune_cash::application::rpc::server::error::RpcError;
use tarpc::client::RpcError as TarpcError;
// note: http StatusCodes are defined at:

View File

@ -4,3 +4,4 @@ pub mod http_util;
pub mod model;
pub mod neptune_rpc;
pub mod rpc;
pub mod shared;

View File

@ -13,8 +13,10 @@ use neptune_explorer::model::app_state::AppState;
use neptune_explorer::neptune_rpc;
use neptune_explorer::rpc::block_digest::block_digest;
use neptune_explorer::rpc::block_info::block_info;
use neptune_explorer::rpc::circulating_supply::circulating_supply;
use neptune_explorer::rpc::pow_puzzle::pow_puzzle;
use neptune_explorer::rpc::provide_pow_solution::provide_pow_solution;
use neptune_explorer::rpc::total_supply::total_supply;
use neptune_explorer::rpc::utxo_digest::utxo_digest;
use tower_http::services::ServeDir;
use tracing::info;
@ -58,6 +60,8 @@ pub fn setup_routes(app_state: AppState) -> Router {
.route("/rpc/block_digest/*selector", get(block_digest))
.route("/rpc/utxo_digest/:index", get(utxo_digest))
.route("/rpc/pow_puzzle/*address", get(pow_puzzle))
.route("/rpc/circulating_supply", get(circulating_supply))
.route("/rpc/total_supply", get(total_supply))
.route("/rpc/provide_pow_solution", post(provide_pow_solution))
// -- Dynamic HTML pages --
.route("/", get(root))

View File

@ -1,11 +1,13 @@
use std::fmt::Display;
use std::str::FromStr;
use neptune_cash::api::export::BlockHeight;
use neptune_cash::models::blockchain::block::block_selector::BlockSelector;
use neptune_cash::prelude::tasm_lib::prelude::Digest;
use neptune_cash::protocol::consensus::block::block_selector::BlockSelector;
use neptune_cash::protocol::consensus::block::block_selector::BlockSelectorLiteral;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use std::fmt::Display;
use std::str::FromStr;
/// newtype for `BlockSelector` that provides ability to parse `height_or_digest/value`.
///
@ -58,11 +60,11 @@ impl FromStr for AnnouncementSelector {
let (block_selector, index) = match parts.as_slice() {
["tip", index] => {
let index = index.parse::<u64>().map_err(Self::Err::TipIndex)?;
(BlockSelector::Tip, index)
(BlockSelector::Special(BlockSelectorLiteral::Tip), index)
}
["genesis", index] => index
.parse::<u64>()
.map(|i| (BlockSelector::Genesis, i))
.map(|i| (BlockSelector::Special(BlockSelectorLiteral::Genesis), i))
.map_err(Self::Err::GenesisIndex)?,
["height", number, index] => {
let height_as_u64 = number.parse::<u64>().map_err(Self::Err::BlockHeight)?;
@ -128,8 +130,10 @@ impl Display for AnnouncementSelector {
BlockSelector::Height(block_height) => {
write!(f, "height/{}/{}", block_height, self.index)
}
BlockSelector::Genesis => write!(f, "genesis/{}", self.index),
BlockSelector::Tip => write!(f, "tip/{}", self.index),
BlockSelector::Special(BlockSelectorLiteral::Genesis) => {
write!(f, "genesis/{}", self.index)
}
BlockSelector::Special(BlockSelectorLiteral::Tip) => write!(f, "tip/{}", self.index),
}
}
}
@ -147,15 +151,18 @@ impl<'de> Deserialize<'de> for AnnouncementSelector {
#[cfg(test)]
mod tests {
use super::*;
use arbitrary::{Arbitrary, Unstructured};
use proptest::string::string_regex;
use proptest::{prop_assert, prop_assert_eq};
use proptest_arbitrary_interop::arb;
use std::str::FromStr;
use arbitrary::Arbitrary;
use arbitrary::Unstructured;
use proptest::prop_assert;
use proptest::prop_assert_eq;
use proptest::string::string_regex;
use proptest_arbitrary_interop::arb;
use test_strategy::proptest;
use super::*;
impl<'a> Arbitrary<'a> for AnnouncementSelector {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
// Pick one of the variants randomly
@ -185,7 +192,7 @@ mod tests {
// Genesis selector
let index = u64::arbitrary(u)? as usize;
AnnouncementSelector {
block_selector: BlockSelector::Genesis,
block_selector: BlockSelector::Special(BlockSelectorLiteral::Genesis),
index,
}
}
@ -193,7 +200,7 @@ mod tests {
// Tip selector
let index = u64::arbitrary(u)? as usize;
AnnouncementSelector {
block_selector: BlockSelector::Tip,
block_selector: BlockSelector::Special(BlockSelectorLiteral::Tip),
index,
}
}

View File

@ -1,16 +1,20 @@
use crate::model::config::Config;
use crate::model::transparent_utxo_tuple::TransparentUtxoTuple;
use crate::neptune_rpc;
use std::sync::Arc;
use anyhow::Context;
use arc_swap::ArcSwap;
use clap::Parser;
use neptune_cash::config_models::network::Network;
use neptune_cash::models::blockchain::block::block_selector::BlockSelector;
use neptune_cash::api::export::Network;
use neptune_cash::application::rpc::auth;
use neptune_cash::prelude::twenty_first::tip5::Digest;
use neptune_cash::rpc_auth;
use std::sync::Arc;
use neptune_cash::protocol::consensus::block::block_selector::{
BlockSelector, BlockSelectorLiteral,
};
use tokio::sync::Mutex;
use crate::model::config::Config;
use crate::model::transparent_utxo_tuple::TransparentUtxoTuple;
use crate::neptune_rpc;
#[derive(Debug, Clone)]
pub struct AppStateInner {
pub network: Network,
@ -26,7 +30,7 @@ pub struct AppStateInner {
}
impl AppStateInner {
pub fn token(&self) -> rpc_auth::Token {
pub fn token(&self) -> auth::Token {
self.rpc_client.token
}
}
@ -57,7 +61,7 @@ impl AppState {
.block_digest(
tarpc::context::current(),
rpc_client.token,
BlockSelector::Genesis,
BlockSelector::Special(BlockSelectorLiteral::Genesis),
)
.await
.with_context(|| "Failed calling neptune-core api: block_digest")?

View File

@ -1,10 +1,16 @@
use super::height_or_digest::HeightOrDigest;
use neptune_cash::models::blockchain::block::block_selector::BlockSelector;
use neptune_cash::models::blockchain::block::block_selector::BlockSelectorParseError;
use std::num::ParseIntError;
use std::str::FromStr;
use neptune_cash::api::export::BlockHeight;
use neptune_cash::api::export::Digest;
use neptune_cash::prelude::triton_vm::prelude::BFieldElement;
use neptune_cash::protocol::consensus::block::block_selector::BlockSelector;
use neptune_cash::protocol::consensus::block::block_selector::BlockSelectorParseError;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
use std::str::FromStr;
use super::height_or_digest::HeightOrDigest;
/// newtype for `BlockSelector` that provides ability to parse `height_or_digest/value`.
///
@ -27,17 +33,34 @@ impl FromStr for BlockSelectorExtended {
// note: this parses BlockSelector, plus height_or_digest/<value>
fn from_str(s: &str) -> Result<Self, Self::Err> {
match BlockSelector::from_str(s) {
let res = match BlockSelector::from_str(s) {
Ok(bs) => Ok(Self::from(bs)),
Err(e) => {
let parts: Vec<_> = s.split('/').collect();
if parts.len() == 2 && parts[0] == "height_or_digest" {
if parts.len() == 2 {
if parts[0] == "height_or_digest" {
Ok(Self::from(HeightOrDigest::from_str(parts[1])?))
} else if parts[0] == "digest" {
Ok(Self(BlockSelector::Digest(
Digest::try_from_hex(parts[1]).map_err(|tfhde| {
BlockSelectorParseError::InvalidSelector(tfhde.to_string())
})?,
)))
} else if parts[0] == "height" {
Ok(Self(BlockSelector::Height(BlockHeight::new(
BFieldElement::new(parts[1].parse().map_err(|e: ParseIntError| {
BlockSelectorParseError::InvalidSelector(e.to_string())
})?),
))))
} else {
Err(e)
}
} else {
Err(e)
}
}
}
};
res
}
}

View File

@ -1,10 +1,12 @@
use neptune_cash::models::blockchain::block::block_height::BlockHeight;
use neptune_cash::models::blockchain::block::block_selector::BlockSelector;
use neptune_cash::models::blockchain::block::block_selector::BlockSelectorParseError;
use neptune_cash::prelude::tasm_lib::prelude::Digest;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use neptune_cash::api::export::BlockHeight;
use neptune_cash::prelude::tasm_lib::prelude::Digest;
use neptune_cash::protocol::consensus::block::block_selector::BlockSelector;
use neptune_cash::protocol::consensus::block::block_selector::BlockSelectorParseError;
use serde::Deserialize;
use serde::Serialize;
/// represents either a block-height or a block digest
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HeightOrDigest {
@ -32,7 +34,11 @@ impl FromStr for HeightOrDigest {
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.parse::<u64>() {
Ok(h) => Self::Height(h.into()),
Err(_) => Self::Digest(Digest::try_from_hex(s)?),
Err(_) => {
let digest = Digest::try_from_hex(s)
.map_err(|_| BlockSelectorParseError::InvalidSelector(s.to_string()))?;
Self::Digest(digest)
}
})
}
}

View File

@ -1,33 +1,36 @@
use crate::alert_email;
use crate::model::app_state::AppState;
use crate::model::config::Config;
use crate::model::transparent_utxo_tuple::TransparentUtxoTuple;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::Context;
use chrono::DateTime;
use chrono::TimeDelta;
use chrono::Utc;
use clap::Parser;
use neptune_cash::api::export::Announcement;
use neptune_cash::config_models::data_directory::DataDirectory;
use neptune_cash::config_models::network::Network;
use neptune_cash::models::blockchain::block::block_height::BlockHeight;
use neptune_cash::models::blockchain::block::block_info::BlockInfo;
use neptune_cash::models::blockchain::block::block_selector::BlockSelector;
use neptune_cash::api::export::Network;
use neptune_cash::application::config::data_directory::DataDirectory;
use neptune_cash::application::rpc::auth;
use neptune_cash::application::rpc::server::error::RpcError;
use neptune_cash::application::rpc::server::RPCClient;
use neptune_cash::application::rpc::server::RpcResult;
use neptune_cash::prelude::tasm_lib::prelude::Digest;
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::protocol::consensus::block::block_height::BlockHeight;
use neptune_cash::protocol::consensus::block::block_info::BlockInfo;
use neptune_cash::protocol::consensus::block::block_selector::BlockSelector;
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};
use tracing::debug;
use tracing::info;
use tracing::warn;
use crate::alert_email;
use crate::model::app_state::AppState;
use crate::model::config::Config;
use crate::model::transparent_utxo_tuple::TransparentUtxoTuple;
#[cfg(feature = "mock")]
const MOCK_KEY: &str = "MOCK";
@ -35,7 +38,7 @@ const MOCK_KEY: &str = "MOCK";
#[derive(Debug, Clone)]
pub struct AuthenticatedClient {
pub client: RPCClient,
pub token: rpc_auth::Token,
pub token: auth::Token,
pub network: Network,
}
@ -52,7 +55,7 @@ impl AuthenticatedClient {
pub async fn block_info(
&self,
ctx: ::tarpc::context::Context,
token: rpc_auth::Token,
token: auth::Token,
block_selector: BlockSelector,
) -> ::core::result::Result<RpcResult<Option<BlockInfo>>, ::tarpc::client::RpcError> {
let rpc_result = self.client.block_info(ctx, token, block_selector).await;
@ -95,7 +98,7 @@ impl AuthenticatedClient {
pub async fn utxo_digest(
&self,
ctx: ::tarpc::context::Context,
token: rpc_auth::Token,
token: auth::Token,
leaf_index: u64,
_transparent_utxos_cache: Arc<Mutex<Vec<TransparentUtxoTuple>>>,
) -> ::core::result::Result<RpcResult<Option<Digest>>, ::tarpc::client::RpcError> {
@ -125,7 +128,7 @@ impl AuthenticatedClient {
pub async fn announcements_in_block(
&self,
ctx: ::tarpc::context::Context,
token: rpc_auth::Token,
token: auth::Token,
block_selector: BlockSelector,
) -> Result<Result<Option<Vec<Announcement>>, RpcError>, ::tarpc::client::RpcError> {
let rpc_result = self
@ -191,11 +194,11 @@ impl AuthenticatedClient {
pub async fn addition_record_indices_for_block(
&self,
ctx: ::tarpc::context::Context,
token: rpc_auth::Token,
token: auth::Token,
block_selector: BlockSelector,
_addition_records: &[AdditionRecord],
) -> ::core::result::Result<
RpcResult<Option<HashMap<AdditionRecord, Option<u64>>>>,
RpcResult<Vec<(AdditionRecord, Option<u64>)>>,
::tarpc::client::RpcError,
> {
let rpc_result = self
@ -204,7 +207,7 @@ impl AuthenticatedClient {
.await;
// if the RPC call was successful, return that
if let Ok(Ok(Some(_))) = rpc_result {
if let Ok(Ok(_)) = rpc_result {
return rpc_result;
}
@ -237,8 +240,8 @@ impl AuthenticatedClient {
},
)
})
.collect::<HashMap<_, _>>();
return Ok(Ok(Some(addition_record_indices)));
.collect::<Vec<(_, _)>>();
return Ok(Ok(addition_record_indices));
}
// otherwise, return the original error
@ -250,12 +253,12 @@ impl AuthenticatedClient {
pub async fn gen_authenticated_rpc_client() -> Result<AuthenticatedClient, anyhow::Error> {
let client = gen_rpc_client().await?;
let rpc_auth::CookieHint {
let auth::CookieHint {
data_directory,
network,
} = get_cookie_hint(&client, &None).await?;
let token: rpc_auth::Token = rpc_auth::Cookie::try_load(&data_directory).await?.into();
let token: auth::Token = auth::Cookie::try_load(&data_directory).await?.into();
Ok(AuthenticatedClient {
client,
@ -289,14 +292,14 @@ pub async fn gen_rpc_client() -> Result<RPCClient, anyhow::Error> {
async fn get_cookie_hint(
client: &RPCClient,
data_dir: &Option<std::path::PathBuf>,
) -> anyhow::Result<rpc_auth::CookieHint> {
) -> anyhow::Result<auth::CookieHint> {
async fn fallback(
client: &RPCClient,
data_dir: &Option<std::path::PathBuf>,
) -> anyhow::Result<rpc_auth::CookieHint> {
) -> anyhow::Result<auth::CookieHint> {
let network = client.network(context::current()).await??;
let data_directory = DataDirectory::get(data_dir.to_owned(), network)?;
Ok(rpc_auth::CookieHint {
Ok(auth::CookieHint {
data_directory,
network,
})

View File

@ -1,16 +1,18 @@
use crate::http_util::not_found_err;
use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err;
use crate::model::app_state::AppState;
use crate::model::block_selector_extended::BlockSelectorExtended;
use std::sync::Arc;
use axum::extract::Path;
use axum::extract::State;
use axum::response::IntoResponse;
use axum::response::Json;
use neptune_cash::prelude::twenty_first::tip5::Digest;
use std::sync::Arc;
use tarpc::context;
use crate::http_util::not_found_err;
use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err;
use crate::model::app_state::AppState;
use crate::model::block_selector_extended::BlockSelectorExtended;
#[axum::debug_handler]
pub async fn block_digest(
Path(selector): Path<BlockSelectorExtended>,

View File

@ -1,15 +1,17 @@
use std::sync::Arc;
use axum::extract::Path;
use axum::extract::State;
use axum::response::Json;
use axum::response::Response;
use neptune_cash::protocol::consensus::block::block_info::BlockInfo;
use tarpc::context;
use crate::http_util::not_found_err;
use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err;
use crate::model::app_state::AppState;
use crate::model::block_selector_extended::BlockSelectorExtended;
use axum::extract::Path;
use axum::extract::State;
use axum::response::Json;
use axum::response::Response;
use neptune_cash::models::blockchain::block::block_info::BlockInfo;
use std::sync::Arc;
use tarpc::context;
#[axum::debug_handler]
pub async fn block_info(

View File

@ -0,0 +1,30 @@
use std::sync::Arc;
use axum::extract::State;
use axum::response::Json;
use axum::response::Response;
use tarpc::context;
use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err;
use crate::model::app_state::AppState;
use crate::shared::monetary_supplies;
/// Return the current monetary amount that is liquid, assuming all redemptions
/// on the old chain have successfully been made. Returned unit is in number of
/// coins. To convert to number of nau, multiply by $4*10^{30}$/
#[axum::debug_handler]
pub async fn circulating_supply(State(state): State<Arc<AppState>>) -> Result<Json<i32>, Response> {
let s = state.load();
let block_height = s
.rpc_client
.block_height(context::current(), s.token())
.await
.map_err(rpc_err)?
.map_err(rpc_method_err)?;
let (liquid_supply, _) = monetary_supplies(block_height);
Ok(Json(liquid_supply.ceil_num_whole_coins()))
}

View File

@ -1,5 +1,7 @@
pub mod block_digest;
pub mod block_info;
pub mod circulating_supply;
pub mod pow_puzzle;
pub mod provide_pow_solution;
pub mod total_supply;
pub mod utxo_digest;

View File

@ -1,11 +1,12 @@
use std::sync::Arc;
use axum::extract::Path;
use axum::extract::State;
use axum::response::IntoResponse;
use axum::response::Json;
use neptune_cash::models::state::wallet::address::generation_address::GenerationReceivingAddress;
use neptune_cash::rpc_server::error::RpcError;
use neptune_cash::rpc_server::ProofOfWorkPuzzle;
use std::sync::Arc;
use neptune_cash::application::rpc::server::error::RpcError;
use neptune_cash::application::rpc::server::proof_of_work_puzzle::ProofOfWorkPuzzle;
use neptune_cash::state::wallet::address::generation_address::GenerationReceivingAddress;
use tarpc::context;
use crate::http_util::not_found_err;

View File

@ -1,16 +1,18 @@
use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err;
use crate::model::app_state::AppState;
use std::sync::Arc;
use axum::extract::State;
use axum::response::Json;
use axum::response::Response;
use neptune_cash::models::blockchain::block::block_header::BlockPow;
use neptune_cash::prelude::twenty_first::tip5::Digest;
use neptune_cash::protocol::consensus::block::block_header::BlockPow;
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
use tarpc::context;
use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err;
use crate::model::app_state::AppState;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PowSolution {
pow: BlockPow,

31
src/rpc/total_supply.rs Normal file
View File

@ -0,0 +1,31 @@
use std::sync::Arc;
use axum::extract::State;
use axum::response::Json;
use axum::response::Response;
use tarpc::context;
use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err;
use crate::model::app_state::AppState;
use crate::shared::monetary_supplies;
/// Return the current total monetary supply, the sum of the timeloced and
/// liquid supply. Assumes all redemptions on the old chain have successfully
/// been made. Returned unit is in number of coins. To convert to number of
/// nau, multiply by $4*10^{30}$.
#[axum::debug_handler]
pub async fn total_supply(State(state): State<Arc<AppState>>) -> Result<Json<i32>, Response> {
let s = state.load();
let block_height = s
.rpc_client
.block_height(context::current(), s.token())
.await
.map_err(rpc_err)?
.map_err(rpc_method_err)?;
let (_, total_supply) = monetary_supplies(block_height);
Ok(Json(total_supply.ceil_num_whole_coins()))
}

View File

@ -1,16 +1,16 @@
use std::sync::Arc;
use axum::extract::Path;
use axum::extract::State;
use axum::response::IntoResponse;
use axum::response::Json;
use neptune_cash::prelude::twenty_first::tip5::Digest;
use std::sync::Arc;
use tarpc::context;
use crate::http_util::not_found_err;
use crate::http_util::rpc_err;
use crate::http_util::rpc_method_err;
use crate::{
http_util::{not_found_err, rpc_err},
model::app_state::AppState,
};
use crate::model::app_state::AppState;
#[axum::debug_handler]
pub async fn utxo_digest(

60
src/shared.rs Normal file
View File

@ -0,0 +1,60 @@
use neptune_cash::api::export::BlockHeight;
use neptune_cash::api::export::NativeCurrencyAmount;
use neptune_cash::protocol::consensus::block::block_height::BLOCKS_PER_GENERATION;
use neptune_cash::protocol::consensus::block::block_height::NUM_BLOCKS_SKIPPED_BECAUSE_REBOOT;
use neptune_cash::protocol::consensus::block::Block;
use neptune_cash::protocol::consensus::block::PREMINE_MAX_SIZE;
/// Return the pair (liquid supply, total supply)
///
/// Assumes all redemption claims have been rewarded.
pub(crate) fn monetary_supplies(
block_height: BlockHeight,
) -> (NativeCurrencyAmount, NativeCurrencyAmount) {
let block_height: u64 = block_height.into();
let generation_0_subsidy = Block::block_subsidy(BlockHeight::genesis().next());
let effective_block_height = block_height + NUM_BLOCKS_SKIPPED_BECAUSE_REBOOT;
let (num_generations, num_blocks_in_curr_gen): (u64, u32) = (
effective_block_height / BLOCKS_PER_GENERATION,
(effective_block_height % BLOCKS_PER_GENERATION)
.try_into()
.expect("There are fewer than u32::MAX blocks per generation"),
);
let mut liquid_supply = PREMINE_MAX_SIZE;
let mut liquid_subsidy = generation_0_subsidy.half();
let mut total_supply = PREMINE_MAX_SIZE;
let blocks_per_generation: u32 = BLOCKS_PER_GENERATION
.try_into()
.expect("There are fewer than u32::MAX blocks per generation");
for _ in 0..num_generations {
liquid_supply += liquid_subsidy.scalar_mul(blocks_per_generation);
total_supply += liquid_subsidy.scalar_mul(2);
liquid_subsidy = liquid_subsidy.half();
}
let liquid_supply_current_generation = liquid_subsidy.scalar_mul(num_blocks_in_curr_gen);
liquid_supply += liquid_supply_current_generation;
total_supply += liquid_supply_current_generation.scalar_mul(2);
// How much of timelocked miner rewards have been unlocked? Assume that the
// timelock is exactly one generation long. In reality the timelock is
// is defined in relation to timestamp and not block heights, so this is
// only a (good) approximation.
let mut released_subsidy = generation_0_subsidy.half();
for _ in 1..num_generations {
liquid_supply += released_subsidy.scalar_mul(blocks_per_generation);
released_subsidy = released_subsidy.half();
}
if num_generations > 0 {
liquid_supply += released_subsidy.scalar_mul(num_blocks_in_curr_gen);
}
// If you want correct results for anything but main net, and claims are
// only being refunded on main net, the size of the claims pool must be
// subtracted here, if that the network is not main net.
(liquid_supply, total_supply)
}

View File

@ -147,6 +147,10 @@
</main>
<footer class="container" style="margin-top: 2em; font-size: 0.9em; text-align: center;">
<a href="https://github.com/Neptune-Crypto/neptune-explorer" target="_blank">Source code</a>
</footer>
</body>
</html>