global: next block template (+ diff)

This commit is contained in:
nym21
2026-05-09 12:56:11 +02:00
parent 3f2b5d3084
commit e62b0ac2a5
20 changed files with 637 additions and 203 deletions

View File

@@ -8953,7 +8953,7 @@ pub struct BrkClient {
impl BrkClient {
/// Client version.
pub const VERSION: &'static str = "v0.3.0-beta.8";
pub const VERSION: &'static str = "v0.3.0-beta.9";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {
@@ -9734,7 +9734,7 @@ impl BrkClient {
/// Projected mempool blocks
///
/// Get projected blocks from the mempool for fee estimation.
/// Projected blocks for fee estimation. Block 0 reflects Bitcoin Core's actual next-block selection; blocks 1+ are a fee-tier approximation.
///
/// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*
///
@@ -9745,7 +9745,7 @@ impl BrkClient {
/// Recommended fees
///
/// Get recommended fee rates for different confirmation targets.
/// Recommended fee rates by confirmation target.
///
/// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*
///
@@ -9756,7 +9756,7 @@ impl BrkClient {
/// Precise recommended fees
///
/// Get recommended fee rates with up to 3 decimal places.
/// Recommended fee rates with sub-integer precision.
///
/// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)*
///
@@ -9778,10 +9778,10 @@ impl BrkClient {
/// Mempool content hash
///
/// Returns an opaque `u64` that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled.
/// Returns an opaque hash that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled.
///
/// Endpoint: `GET /api/mempool/hash`
pub fn get_mempool_hash(&self) -> Result<i64> {
pub fn get_mempool_hash(&self) -> Result<NextBlockHash> {
self.base.get_json(&format!("/api/mempool/hash"))
}
@@ -9829,6 +9829,24 @@ impl BrkClient {
self.base.get_json(&format!("/api/v1/fullrbf/replacements"))
}
/// Projected next block template
///
/// Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `<hash>` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template.
///
/// Endpoint: `GET /api/v1/mempool/block-template`
pub fn get_block_template(&self) -> Result<BlockTemplate> {
self.base.get_json(&format!("/api/v1/mempool/block-template"))
}
/// Block template diff since hash
///
/// Delta of the projected next block since `<hash>`. `added` carries full transaction bodies; `removed` is just txids. Returns `404` when `<hash>` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`.
///
/// Endpoint: `GET /api/v1/mempool/block-template/diff/{hash}`
pub fn get_block_template_diff(&self, hash: NextBlockHash) -> Result<BlockTemplateDiff> {
self.base.get_json(&format!("/api/v1/mempool/block-template/diff/{hash}"))
}
/// Live BTC/USD price
///
/// Returns the current BTC/USD price in dollars, derived from on-chain round-dollar output patterns in the last 12 blocks plus mempool.

View File

@@ -23,6 +23,7 @@ const MAX_LOG_AGE_DAYS: u64 = 7;
/// `*.txt` file older than 7 days is pruned on startup.
pub fn init(dir: Option<&Path>) -> io::Result<()> {
tracing_log::LogTracer::init().ok();
install_panic_hook();
#[cfg(debug_assertions)]
const DEFAULT_LEVEL: &str = "debug";
@@ -65,6 +66,24 @@ pub fn init(dir: Option<&Path>) -> io::Result<()> {
Ok(())
}
fn install_panic_hook() {
std::panic::set_hook(Box::new(|info| {
let location = info
.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
.unwrap_or_else(|| "unknown".to_string());
let payload = info.payload();
let msg = payload
.downcast_ref::<&str>()
.copied()
.map(str::to_owned)
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Box<dyn Any>".to_owned());
let backtrace = std::backtrace::Backtrace::capture();
tracing::error!(location, backtrace = %backtrace, "panic: {msg}");
}));
}
/// Register a hook that gets called for every log message.
pub fn register_hook<F>(hook: F) -> Result<(), &'static str>
where

View File

@@ -30,9 +30,11 @@ use std::{
use brk_error::Result;
use brk_rpc::Client;
use brk_types::{
AddrBytes, AddrMempoolStats, FeeRate, MempoolInfo, MempoolRecentTx, OutpointPrefix, OutputType,
Sats, Timestamp, Transaction, TxOut, Txid, TxidPrefix, Vin, Vout,
AddrBytes, AddrMempoolStats, BlockTemplate, BlockTemplateDiff, FeeRate, MempoolBlock,
MempoolInfo, MempoolRecentTx, NextBlockHash, OutpointPrefix, OutputType, Sats, Timestamp,
Transaction, TxOut, Txid, TxidPrefix, Vin, Vout,
};
use rustc_hash::FxHashSet;
use parking_lot::{RwLock, RwLockReadGuard};
use tracing::error;
@@ -102,10 +104,58 @@ impl Mempool {
self.snapshot().block_stats.clone()
}
pub fn next_block_hash(&self) -> u64 {
pub fn next_block_hash(&self) -> NextBlockHash {
self.snapshot().next_block_hash
}
/// Full projected next block: Core's `getblocktemplate` selection
/// (block 0) with aggregate stats and full tx bodies in GBT order.
pub fn block_template(&self) -> BlockTemplate {
let snap = self.snapshot();
let stats = MempoolBlock::from(&snap.block_stats[0]);
let txids: Vec<Txid> = snap.blocks[0]
.iter()
.map(|idx| snap.txs[idx.as_usize()].txid)
.collect();
let transactions = self.collect_txs(&txids);
BlockTemplate {
hash: snap.next_block_hash,
stats,
transactions,
}
}
/// Delta of the projected next block since `since`. `None` when
/// `since` has aged out of the rebuilder's history (server should
/// 404 → client falls back to `block_template`). `removed` is just
/// txids; `added` carries full bodies so clients can patch their
/// local view in one round trip.
pub fn block_template_diff(&self, since: NextBlockHash) -> Option<BlockTemplateDiff> {
let past = self.0.rebuilder.historical_block0(since)?;
let snap = self.snapshot();
let current: FxHashSet<Txid> = snap.blocks[0]
.iter()
.map(|idx| snap.txs[idx.as_usize()].txid)
.collect();
let added_txids: Vec<Txid> = current.difference(&past).copied().collect();
let removed: Vec<Txid> = past.difference(&current).copied().collect();
let added = self.collect_txs(&added_txids);
Some(BlockTemplateDiff {
hash: snap.next_block_hash,
since,
added,
removed,
})
}
fn collect_txs(&self, txids: &[Txid]) -> Vec<Transaction> {
let state = self.read();
txids
.iter()
.filter_map(|txid| state.txs.get(txid).cloned())
.collect()
}
pub fn addr_state_hash(&self, addr: &AddrBytes) -> u64 {
self.read().addrs.stats_hash(addr)
}

View File

@@ -1,10 +1,13 @@
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
use std::{
collections::VecDeque,
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
};
use brk_rpc::BlockTemplateTx;
use brk_types::{FeeRate, TxidPrefix};
use brk_types::{FeeRate, NextBlockHash, Txid, TxidPrefix};
use parking_lot::RwLock;
use rustc_hash::FxHashSet;
@@ -20,10 +23,13 @@ pub use brk_types::RecommendedFees;
pub use snapshot::{BlockStats, SnapTx, Snapshot, TxIndex};
const NUM_BLOCKS: usize = 8;
const HISTORY: usize = 10;
#[derive(Default)]
pub struct Rebuilder {
snapshot: RwLock<Arc<Snapshot>>,
/// Past block-0 txid sets keyed by `next_block_hash`, oldest first.
history: RwLock<VecDeque<(NextBlockHash, FxHashSet<Txid>)>>,
dirty: AtomicBool,
rebuild_count: AtomicU64,
skip_clean: AtomicU64,
@@ -49,11 +55,38 @@ impl Rebuilder {
self.skip_clean.fetch_add(1, Ordering::Relaxed);
return;
}
*self.snapshot.write() = Arc::new(Self::build_snapshot(lock, gbt, min_fee));
let snap = Self::build_snapshot(lock, gbt, min_fee);
let block0_set: FxHashSet<Txid> = snap.blocks[0]
.iter()
.map(|idx| snap.txs[idx.as_usize()].txid)
.collect();
let next_hash = snap.next_block_hash;
*self.snapshot.write() = Arc::new(snap);
self.push_history(next_hash, block0_set);
self.dirty.store(false, Ordering::Release);
self.rebuild_count.fetch_add(1, Ordering::Relaxed);
}
fn push_history(&self, hash: NextBlockHash, set: FxHashSet<Txid>) {
let mut hist = self.history.write();
hist.retain(|(h, _)| *h != hash);
hist.push_back((hash, set));
while hist.len() > HISTORY {
hist.pop_front();
}
}
/// Past block-0 txid set for `hash`, or `None` if it has aged out
/// (or was never seen). Used by `block_template_diff` to decide
/// 200 vs 404.
pub fn historical_block0(&self, hash: NextBlockHash) -> Option<FxHashSet<Txid>> {
self.history
.read()
.iter()
.find(|(h, _)| *h == hash)
.map(|(_, set)| set.clone())
}
pub fn rebuild_count(&self) -> u64 {
self.rebuild_count.load(Ordering::Relaxed)
}

View File

@@ -11,7 +11,7 @@ pub use tx_index::TxIndex;
use std::hash::{DefaultHasher, Hash, Hasher};
use brk_types::{FeeRate, RecommendedFees, TxidPrefix};
use brk_types::{FeeRate, NextBlockHash, RecommendedFees, TxidPrefix};
use fees::Fees;
@@ -30,7 +30,7 @@ pub struct Snapshot {
pub fees: RecommendedFees,
/// Content hash of the projected next block. Same value as the
/// mempool ETag.
pub next_block_hash: u64,
pub next_block_hash: NextBlockHash,
/// Per-snapshot `TxidPrefix -> TxIndex` index, so live queries can
/// resolve a prefix to the snapshot's compact index without
/// re-walking `txs`. Built once by `builder::build_txs` and reused
@@ -70,13 +70,13 @@ impl Snapshot {
}
}
fn hash_next_block(blocks: &[Vec<TxIndex>]) -> u64 {
fn hash_next_block(blocks: &[Vec<TxIndex>]) -> NextBlockHash {
let Some(block) = blocks.first() else {
return 0;
return NextBlockHash::ZERO;
};
let mut hasher = DefaultHasher::new();
block.hash(&mut hasher);
hasher.finish()
NextBlockHash::new(hasher.finish())
}
pub fn tx(&self, idx: TxIndex) -> Option<&SnapTx> {

View File

@@ -1,4 +1,4 @@
use brk_types::{FeeRate, Sats, VSize, get_weighted_percentile};
use brk_types::{FeeRate, MempoolBlock, Sats, VSize, get_weighted_percentile};
use super::{SnapTx, TxIndex};
@@ -83,3 +83,9 @@ impl BlockStats {
self.fee_range[3]
}
}
impl From<&BlockStats> for MempoolBlock {
fn from(s: &BlockStats) -> Self {
Self::new(s.tx_count, s.total_size, s.total_vsize, s.total_fee, s.fee_range)
}
}

View File

@@ -2,9 +2,9 @@ use crate::Query;
use brk_error::{Error, Result};
use brk_mempool::{Mempool, PrevoutResolver, RbfForTx, RbfNode};
use brk_types::{
CheckedSub, FeeRate, MempoolBlock, MempoolInfo, MempoolRecentTx, OutputType, RbfResponse,
RbfTx, RecommendedFees, ReplacementNode, Sats, Timestamp, TxOut, TxOutIndex, Txid, TxidPrefix,
TypeIndex,
BlockTemplate, BlockTemplateDiff, CheckedSub, FeeRate, MempoolBlock, MempoolInfo,
MempoolRecentTx, NextBlockHash, OutputType, RbfResponse, RbfTx, RecommendedFees,
ReplacementNode, Sats, Timestamp, TxOut, TxOutIndex, Txid, TxidPrefix, TypeIndex,
};
const RECENT_REPLACEMENTS_LIMIT: usize = 25;
@@ -28,23 +28,7 @@ impl Query {
pub fn mempool_blocks(&self) -> Result<Vec<MempoolBlock>> {
let mempool = self.require_mempool()?;
let block_stats = mempool.block_stats();
let blocks = block_stats
.into_iter()
.map(|stats| {
MempoolBlock::new(
stats.tx_count,
stats.total_size,
stats.total_vsize,
stats.total_fee,
stats.fee_range,
)
})
.collect();
Ok(blocks)
Ok(mempool.block_stats().iter().map(MempoolBlock::from).collect())
}
/// Indexer-backed resolver for confirmed-parent prevouts. Pass
@@ -172,7 +156,22 @@ impl Query {
/// Content hash of the projected next block. Same value as the
/// mempool ETag. Polling lets monitors detect a stalled sync.
pub fn mempool_hash(&self) -> Result<u64> {
pub fn mempool_hash(&self) -> Result<NextBlockHash> {
Ok(self.require_mempool()?.next_block_hash())
}
/// Full projected next block (Core's `getblocktemplate` selection)
/// with stats and full tx bodies in GBT order.
pub fn block_template(&self) -> Result<BlockTemplate> {
Ok(self.require_mempool()?.block_template())
}
/// Delta of the projected next block since `since`. `NotFound`
/// when `since` has aged out (client should fall back to
/// `block_template`).
pub fn block_template_diff(&self, since: NextBlockHash) -> Result<BlockTemplateDiff> {
self.require_mempool()?
.block_template_diff(since)
.ok_or_else(|| Error::NotFound(format!("unknown since hash: {since}")))
}
}

View File

@@ -373,22 +373,25 @@ impl Client {
/// Verbose mempool listing + Core's projected next block + live
/// `mempoolminfee`, fetched in a single bitcoind round-trip.
/// Validates that every GBT txid is present in the verbose listing
/// and returns `Ok(None)` on mismatch so the caller can skip the
/// cycle (within-batch races inside bitcoind are rare; persistent
/// drift is bug-shaped). Other failures bubble up as `Err`.
/// `getblocktemplate` runs first so that any tx arriving between
/// the two intra-batch calls lands in the verbose listing only,
/// preserving GBT ⊆ verbose for the common race. Validates that
/// every GBT txid is present in the verbose listing and returns
/// `Ok(None)` on mismatch so the caller can skip the cycle:
/// republishing block 0 with missing txids would diverge from
/// Core's exact selection. Other failures bubble up as `Err`.
pub fn fetch_mempool_state(&self) -> Result<Option<MempoolState>> {
let requests: [(&str, Vec<Value>); 3] = [
("getrawmempool", vec![Value::Bool(true)]),
(
"getblocktemplate",
vec![serde_json::json!({ "rules": ["segwit"] })],
),
("getrawmempool", vec![Value::Bool(true)]),
("getmempoolinfo", vec![]),
];
let mut out = self.0.call_mixed_batch(&requests)?.into_iter();
let verbose_raw = out.next().ok_or(Error::Internal("missing verbose"))??;
let gbt_raw = out.next().ok_or(Error::Internal("missing gbt"))??;
let verbose_raw = out.next().ok_or(Error::Internal("missing verbose"))??;
let info_raw = out.next().ok_or(Error::Internal("missing mempoolinfo"))??;
let verbose: FxHashMap<String, VerboseEntryRaw> = serde_json::from_str(verbose_raw.get())?;

View File

@@ -27,7 +27,7 @@ impl FeesRoutes for ApiRouter<AppState> {
op.id("get_mempool_blocks")
.fees_tag()
.summary("Projected mempool blocks")
.description("Get projected blocks from the mempool for fee estimation.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*")
.description("Projected blocks for fee estimation. Block 0 reflects Bitcoin Core's actual next-block selection; blocks 1+ are a fee-tier approximation.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)*")
.json_response::<Vec<MempoolBlock>>()
.not_modified()
.server_error()
@@ -48,7 +48,7 @@ impl FeesRoutes for ApiRouter<AppState> {
op.id("get_recommended_fees")
.fees_tag()
.summary("Recommended fees")
.description("Get recommended fee rates for different confirmation targets.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
.description("Recommended fee rates by confirmation target.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
.json_response::<RecommendedFees>()
.not_modified()
.server_error()
@@ -69,7 +69,7 @@ impl FeesRoutes for ApiRouter<AppState> {
op.id("get_precise_fees")
.fees_tag()
.summary("Precise recommended fees")
.description("Get recommended fee rates with up to 3 decimal places.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)*")
.description("Recommended fee rates with sub-integer precision.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)*")
.json_response::<RecommendedFees>()
.not_modified()
.server_error()

View File

@@ -1,11 +1,18 @@
use aide::axum::{ApiRouter, routing::get_with};
use axum::{
extract::State,
extract::{Path, State},
http::{HeaderMap, Uri},
};
use brk_types::{Dollars, MempoolInfo, MempoolRecentTx, ReplacementNode, Txid};
use brk_types::{
BlockTemplate, BlockTemplateDiff, Dollars, MempoolInfo, MempoolRecentTx, NextBlockHash,
ReplacementNode, Txid,
};
use crate::{AppState, extended::TransformResponseExtended, params::Empty};
use crate::{
AppState,
extended::TransformResponseExtended,
params::{Empty, NextBlockHashParam},
};
pub trait MempoolRoutes {
fn add_mempool_routes(self) -> Self;
@@ -44,8 +51,8 @@ impl MempoolRoutes for ApiRouter<AppState> {
op.id("get_mempool_hash")
.mempool_tag()
.summary("Mempool content hash")
.description("Returns an opaque `u64` that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled.")
.json_response::<u64>()
.description("Returns an opaque hash that changes whenever the projected next block changes. Same value as the mempool ETag. Useful as a freshness/liveness signal: if it stays constant for tens of seconds on a live network, the mempool sync loop has stalled.")
.json_response::<NextBlockHash>()
.not_modified()
.server_error()
},
@@ -131,6 +138,53 @@ impl MempoolRoutes for ApiRouter<AppState> {
},
),
)
.api_route(
"/api/v1/mempool/block-template",
get_with(
async |uri: Uri, headers: HeaderMap, _: Empty, State(state): State<AppState>| {
state
.respond_json(&headers, state.mempool_strategy(), &uri, |q| {
q.block_template()
})
.await
},
|op| {
op.id("get_block_template")
.mempool_tag()
.summary("Projected next block template")
.description("Bitcoin Core's `getblocktemplate` selection: full transaction bodies in GBT order with aggregate stats. The returned `hash` is an opaque content token; pass it as `<hash>` on `/api/v1/mempool/block-template/diff/{hash}` to fetch deltas instead of refetching the whole template.")
.json_response::<BlockTemplate>()
.not_modified()
.server_error()
},
),
)
.api_route(
"/api/v1/mempool/block-template/diff/{hash}",
get_with(
async |uri: Uri,
headers: HeaderMap,
Path(path): Path<NextBlockHashParam>,
_: Empty,
State(state): State<AppState>| {
state
.respond_json(&headers, state.mempool_strategy(), &uri, move |q| {
q.block_template_diff(path.hash)
})
.await
},
|op| {
op.id("get_block_template_diff")
.mempool_tag()
.summary("Block template diff since hash")
.description("Delta of the projected next block since `<hash>`. `added` carries full transaction bodies; `removed` is just txids. Returns `404` when `<hash>` has aged out of server history; clients should fall back to `/api/v1/mempool/block-template`.")
.json_response::<BlockTemplateDiff>()
.not_modified()
.not_found()
.server_error()
},
),
)
.api_route(
"/api/mempool/price",
get_with(

View File

@@ -102,9 +102,11 @@ impl Server {
let response_time_layer = axum::middleware::from_fn(
async |request: Request<Body>, next: Next| -> Response<Body> {
let uri = request.uri().clone();
let method = request.method().clone();
let start = Instant::now();
let mut response = next.run(request).await;
response.extensions_mut().insert(uri);
response.extensions_mut().insert(method);
response.headers_mut().insert(
"X-Response-Time",
format!("{}us", start.elapsed().as_micros())
@@ -182,14 +184,19 @@ impl Server {
.on_response(
|response: &Response<Body>, latency: Duration, _: &tracing::Span| {
let status = response.status().as_u16();
let unknown = Uri::from_static("/unknown");
let uri = response.extensions().get::<Uri>().unwrap_or(&unknown);
let unknown_uri = Uri::from_static("/unknown");
let unknown_method = axum::http::Method::default();
let uri = response.extensions().get::<Uri>().unwrap_or(&unknown_uri);
let method = response
.extensions()
.get::<axum::http::Method>()
.unwrap_or(&unknown_method);
match response.status() {
StatusCode::OK => info!(status, %uri, ?latency),
StatusCode::OK => info!(%method, status, %uri, ?latency),
StatusCode::NOT_MODIFIED
| StatusCode::TEMPORARY_REDIRECT
| StatusCode::PERMANENT_REDIRECT => info!(status, %uri, ?latency),
_ => error!(status, %uri, ?latency),
| StatusCode::PERMANENT_REDIRECT => info!(%method, status, %uri, ?latency),
_ => error!(%method, status, %uri, ?latency),
}
},
)
@@ -209,8 +216,6 @@ impl Server {
let router = router
.with_state(state)
.merge(website_router)
.layer(response_time_layer)
.layer(trace_layer)
.layer(TimeoutLayer::with_status_code(
StatusCode::GATEWAY_TIMEOUT,
REQUEST_TIMEOUT,
@@ -242,7 +247,9 @@ impl Server {
.or_else(|| panic.downcast_ref::<&str>().copied())
.unwrap_or("Unknown panic");
Error::internal(msg).into_response()
}));
}))
.layer(response_time_layer)
.layer(trace_layer);
let (listener, port) = match port {
Some(port) => {

View File

@@ -6,6 +6,7 @@ mod blockhash_start_index;
mod blockhash_tx_index;
mod empty;
mod height_param;
mod next_block_hash_param;
mod pool_slug_param;
mod series_param;
mod time_period_param;
@@ -25,6 +26,7 @@ pub use blockhash_start_index::*;
pub use blockhash_tx_index::*;
pub use empty::*;
pub use height_param::*;
pub use next_block_hash_param::*;
pub use pool_slug_param::*;
pub use series_param::*;
pub use time_period_param::*;

View File

@@ -0,0 +1,10 @@
use schemars::JsonSchema;
use serde::Deserialize;
use brk_types::NextBlockHash;
/// `since` hash for `/api/v1/mining/block-template/diff/{hash}`.
#[derive(Deserialize, JsonSchema)]
pub struct NextBlockHashParam {
pub hash: NextBlockHash,
}

View File

@@ -124,7 +124,7 @@ impl AppState {
if let Some(mempool) = q.mempool()
&& mempool.contains_txid(txid)
{
return CacheStrategy::MempoolHash(mempool.next_block_hash());
return CacheStrategy::MempoolHash(mempool.next_block_hash().into());
}
if let Ok((_, height)) = q.resolve_tx(txid)
&& let Ok(block_hash) = q.block_hash_by_height(height)
@@ -136,7 +136,7 @@ impl AppState {
}
pub fn mempool_strategy(&self) -> CacheStrategy {
let hash = self.sync(|q| q.mempool().map(|m| m.next_block_hash()).unwrap_or(0));
let hash = self.sync(|q| q.mempool().map(|m| m.next_block_hash().into()).unwrap_or(0));
CacheStrategy::MempoolHash(hash)
}

View File

@@ -0,0 +1,21 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{MempoolBlock, NextBlockHash, Transaction};
/// Projected next-block contents from Bitcoin Core's `getblocktemplate`
/// (block 0 of the snapshot). Returned by
/// `GET /api/v1/mining/block-template`.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BlockTemplate {
/// Pass back as `<hash>` on
/// `/api/v1/mining/block-template/diff/{hash}` to fetch deltas.
pub hash: NextBlockHash,
/// Aggregate stats for this block (size, vsize, fee range, ...).
pub stats: MempoolBlock,
/// Full transaction bodies in `getblocktemplate` order.
pub transactions: Vec<Transaction>,
}

View File

@@ -0,0 +1,25 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{NextBlockHash, Transaction, Txid};
/// Delta between the current `getblocktemplate` projection and a prior
/// one identified by `since`. Returned by
/// `GET /api/v1/mining/block-template/diff/{hash}`.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BlockTemplateDiff {
/// Current next-block hash. Use as `since` on the next diff call.
pub hash: NextBlockHash,
/// Echoed prior hash the diff was computed against.
pub since: NextBlockHash,
/// Full bodies of transactions that joined the projected next
/// block since `since`.
pub added: Vec<Transaction>,
/// Txids that left the projected next block since `since`
/// (confirmed, evicted, replaced, or pushed past block 0).
pub removed: Vec<Txid>,
}

View File

@@ -80,6 +80,8 @@ mod hour4;
mod index;
mod index_info;
mod limit;
mod block_template;
mod block_template_diff;
mod mempool_block;
mod mempool_entry_info;
mod mempool_info;
@@ -90,6 +92,7 @@ mod minute30;
mod month1;
mod month3;
mod month6;
mod next_block_hash;
mod ohlc;
mod op_return_index;
mod option_ext;
@@ -225,6 +228,8 @@ pub use block_rewards_entry::*;
pub use block_size_entry::*;
pub use block_sizes_weights::*;
pub use block_status::*;
pub use block_template::*;
pub use block_template_diff::*;
pub use block_timestamp::*;
pub use block_tx_index::*;
pub use block_weight_entry::*;
@@ -283,6 +288,7 @@ pub use minute30::*;
pub use month1::*;
pub use month3::*;
pub use month6::*;
pub use next_block_hash::*;
pub use ohlc::*;
pub use op_return_index::*;
pub use option_ext::*;

View File

@@ -0,0 +1,51 @@
use std::fmt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Content hash of the projected next block (block 0 of the mempool
/// snapshot). Same value as the mempool ETag. Opaque token: pass back
/// as `since` on `/api/v1/mining/block-template/diff/{hash}` to fetch
/// deltas.
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Serialize,
Deserialize,
JsonSchema,
)]
#[serde(transparent)]
pub struct NextBlockHash(u64);
impl NextBlockHash {
pub const ZERO: Self = Self(0);
pub const fn new(value: u64) -> Self {
Self(value)
}
}
impl fmt::Display for NextBlockHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<u64> for NextBlockHash {
fn from(value: u64) -> Self {
Self(value)
}
}
impl From<NextBlockHash> for u64 {
fn from(value: NextBlockHash) -> Self {
value.0
}
}