global: big snapshot

This commit is contained in:
nym21
2026-04-26 23:12:17 +02:00
parent 2210443e37
commit 7a0b4b5890
125 changed files with 3833 additions and 3129 deletions

View File

@@ -18,7 +18,7 @@ brk_error = { workspace = true, features = ["jiff", "vecdb"] }
brk_indexer = { workspace = true }
brk_mempool = { workspace = true }
brk_reader = { workspace = true }
brk_rpc = { workspace = true, features = ["corepc"] }
brk_rpc = { workspace = true }
brk_traversable = { workspace = true }
brk_types = { workspace = true }
derive_more = { workspace = true }

View File

@@ -85,13 +85,8 @@ impl Query {
tx_count: addr_data.tx_count,
realized_price,
},
mempool_stats: self.mempool().map(|mempool| {
mempool
.get_addrs()
.get(&bytes)
.map(|(stats, _)| stats)
.cloned()
.unwrap_or_default()
mempool_stats: self.mempool().and_then(|m| {
m.addrs().get(&bytes).map(|(stats, _)| stats.clone())
}),
})
}
@@ -221,21 +216,17 @@ impl Query {
let Ok(bytes) = AddrBytes::from_str(addr) else {
return 0;
};
mempool.addr_hash(&bytes)
mempool.addr_state_hash(&bytes)
}
pub fn addr_mempool_txids(&self, addr: Addr) -> Result<Vec<Txid>> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
let bytes = AddrBytes::from_str(&addr)?;
let addrs = mempool.get_addrs();
let txids: Vec<Txid> = addrs
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
Ok(mempool
.addrs()
.get(&bytes)
.map(|(_, txids)| txids.iter().take(MAX_MEMPOOL_TXIDS).cloned().collect())
.unwrap_or_default();
Ok(txids)
.unwrap_or_default())
}
/// Height of the last on-chain activity for an address (last tx_index → height).

View File

@@ -1,6 +1,6 @@
use std::io::Cursor;
use bitcoin::{consensus::Decodable, hex::DisplayHex};
use bitcoin::consensus::Decodable;
use brk_error::{Error, OptionData, Result};
use brk_types::{
BlkPosition, BlockHash, Height, OutPoint, OutputType, RawLockTime, Sats, StoredU32,
@@ -217,11 +217,7 @@ impl Query {
(prev_txid, outpoint.vout(), Some(prev_txout))
};
let witness = txin
.witness
.iter()
.map(|w| w.to_lower_hex_string())
.collect();
let witness = txin.witness.clone().into();
Ok(TxIn {
txid: prev_txid,

View File

@@ -1,35 +1,39 @@
use std::cmp::Ordering;
use brk_error::{Error, Result};
use brk_mempool::{Entry, EntryPool, Removal, Tombstone, TxGraveyard, TxStore};
use brk_types::{
CpfpEntry, CpfpInfo, FeeRate, MempoolBlock, MempoolInfo, MempoolRecentTx, RecommendedFees,
Txid, TxidPrefix, Weight,
CheckedSub, CpfpEntry, CpfpInfo, FeeRate, MempoolBlock, MempoolInfo, MempoolRecentTx, OutputType,
RbfResponse, RbfTx, RecommendedFees, ReplacementNode, Sats, Timestamp, Transaction, TxOut,
TxOutIndex, Txid, TxidPrefix, TypeIndex, VSize, Weight,
};
use rustc_hash::FxHashSet;
use vecdb::VecIndex;
use crate::Query;
impl Query {
pub fn mempool_info(&self) -> Result<MempoolInfo> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
Ok(mempool.get_info())
Ok(mempool.info())
}
pub fn mempool_txids(&self) -> Result<Vec<Txid>> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
let txs = mempool.get_txs();
let txs = mempool.txs();
Ok(txs.keys().cloned().collect())
}
pub fn recommended_fees(&self) -> Result<RecommendedFees> {
self.mempool()
.map(|mempool| mempool.get_fees())
.map(|mempool| mempool.fees())
.ok_or(Error::MempoolNotAvailable)
}
pub fn mempool_blocks(&self) -> Result<Vec<MempoolBlock>> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
let block_stats = mempool.get_block_stats();
let block_stats = mempool.block_stats();
let blocks = block_stats
.into_iter()
@@ -47,25 +51,74 @@ impl Query {
Ok(blocks)
}
/// Fill any `prevout == None` inputs on live mempool txs from the
/// indexer, mutating them in place. Cheap when the unresolved set
/// is empty (the steady-state with `-txindex` on); otherwise resolves
/// each missing prevout via the same lookup chain used for confirmed
/// txs: `txid → tx_index → first_txout_index + vout → output_type
/// / type_index / value → script_pubkey`.
///
/// Driver calls this once per cycle, right after `mempool.update()`.
/// Returns true if at least one prevout was filled.
pub fn fill_mempool_prevouts(&self) -> bool {
let Some(mempool) = self.mempool() else {
return false;
};
let indexer = self.indexer();
let stores = &indexer.stores;
let first_txout_index_reader = indexer.vecs.transactions.first_txout_index.reader();
let output_type_reader = indexer.vecs.outputs.output_type.reader();
let type_index_reader = indexer.vecs.outputs.type_index.reader();
let value_reader = indexer.vecs.outputs.value.reader();
let addr_readers = indexer.vecs.addrs.addr_readers();
mempool.fill_prevouts(|prev_txid, vout| {
let prev_tx_index = stores
.txid_prefix_to_tx_index
.get(&TxidPrefix::from(prev_txid))
.ok()??
.into_owned();
let first_txout: TxOutIndex = first_txout_index_reader.get(prev_tx_index.to_usize());
let txout_index = usize::from(first_txout + vout);
let output_type: OutputType = output_type_reader.get(txout_index);
let type_index: TypeIndex = type_index_reader.get(txout_index);
let value: Sats = value_reader.get(txout_index);
let script_pubkey = addr_readers.script_pubkey(output_type, type_index);
Some(TxOut::from((script_pubkey, value)))
})
}
pub fn mempool_recent(&self) -> Result<Vec<MempoolRecentTx>> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
Ok(mempool.get_txs().recent().to_vec())
Ok(mempool.txs().recent().to_vec())
}
pub fn cpfp(&self, txid: &Txid) -> Result<CpfpInfo> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
let entries = mempool.get_entries();
let entries = mempool.entries();
let prefix = TxidPrefix::from(txid);
let Some(entry) = entries.get(&prefix) else {
return Ok(CpfpInfo::default());
};
// Ancestors: walk up the depends chain
// Ancestor walk doubles as package-rate aggregation. Stale
// `depends` entries pointing at mined/evicted txs are silently
// dropped via the live `entries.get` probe, so the aggregates
// reflect only in-pool ancestors.
let mut ancestors = Vec::new();
let mut visited: FxHashSet<TxidPrefix> = FxHashSet::default();
let mut package_fee = u64::from(entry.fee);
let mut package_vsize = u64::from(entry.vsize);
let mut stack: Vec<TxidPrefix> = entry.depends.to_vec();
while let Some(p) = stack.pop() {
if !visited.insert(p) {
continue;
}
if let Some(anc) = entries.get(&p) {
package_fee += u64::from(anc.fee);
package_vsize += u64::from(anc.vsize);
ancestors.push(CpfpEntry {
txid: anc.txid.clone(),
weight: Weight::from(anc.vsize),
@@ -77,7 +130,7 @@ impl Query {
let mut descendants = Vec::new();
for child_prefix in entries.children(&prefix) {
if let Some(e) = entries.get(child_prefix) {
if let Some(e) = entries.get(&child_prefix) {
descendants.push(CpfpEntry {
txid: e.txid.clone(),
weight: Weight::from(e.vsize),
@@ -86,7 +139,13 @@ impl Query {
}
}
let effective_fee_per_vsize = entry.effective_fee_rate();
let self_rate = entry.fee_rate();
let package_rate = FeeRate::from((Sats::from(package_fee), VSize::from(package_vsize)));
let effective_fee_per_vsize = if package_rate > self_rate {
package_rate
} else {
self_rate
};
let best_descendant = descendants
.iter()
@@ -107,9 +166,109 @@ impl Query {
})
}
/// RBF history for a tx, matching mempool.space's
/// `GET /api/v1/tx/:txid/rbf`. Walks forward through the graveyard
/// to find the latest known replacer (tree root), then recursively
/// walks `predecessors_of` backward to build the tree. `replaces`
/// is the requested tx's own direct predecessors.
pub fn tx_rbf(&self, txid: &Txid) -> Result<RbfResponse> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
let txs = mempool.txs();
let entries = mempool.entries();
let graveyard = mempool.graveyard();
let mut root_txid = txid.clone();
while let Some(Removal::Replaced { by }) = graveyard.get(&root_txid).map(Tombstone::reason) {
root_txid = by.clone();
}
let replaces_vec: Vec<Txid> = graveyard
.predecessors_of(txid)
.map(|(p, _)| p.clone())
.collect();
let replaces = (!replaces_vec.is_empty()).then_some(replaces_vec);
let replacements = Self::build_rbf_node(&root_txid, None, &txs, &entries, &graveyard)
.map(|mut node| {
node.tx.full_rbf = Some(node.full_rbf);
node.interval = None;
node
});
Ok(RbfResponse {
replacements,
replaces,
})
}
/// Resolve a txid to the data we need for an `RbfTx`. The live
/// pool takes priority; the graveyard is the fallback. Returns
/// `None` if the tx has no known data in either.
fn resolve_rbf_node_data<'a>(
txid: &Txid,
txs: &'a TxStore,
entries: &'a EntryPool,
graveyard: &'a TxGraveyard,
) -> Option<(&'a Transaction, &'a Entry)> {
if let (Some(tx), Some(entry)) =
(txs.get(txid), entries.get(&TxidPrefix::from(txid)))
{
return Some((tx, entry));
}
graveyard
.get(txid)
.map(|tomb| (&tomb.tx, &tomb.entry))
}
/// Recursively build an RBF tree node rooted at `txid`.
/// Predecessors are always in the graveyard (that's where
/// `Removal::Replaced` lives), so the recursion only needs the
/// graveyard; the live pool is consulted for the root.
fn build_rbf_node(
txid: &Txid,
successor_time: Option<Timestamp>,
txs: &TxStore,
entries: &EntryPool,
graveyard: &TxGraveyard,
) -> Option<ReplacementNode> {
let (tx, entry) = Self::resolve_rbf_node_data(txid, txs, entries, graveyard)?;
let replaces: Vec<ReplacementNode> = graveyard
.predecessors_of(txid)
.filter_map(|(pred_txid, _)| {
Self::build_rbf_node(pred_txid, Some(entry.first_seen), txs, entries, graveyard)
})
.collect();
let full_rbf = replaces.iter().any(|c| !c.tx.rbf || c.full_rbf);
let interval = successor_time
.and_then(|st| st.checked_sub(entry.first_seen))
.map(|d| usize::from(d) as u32);
let value = Sats::from(tx.output.iter().map(|o| u64::from(o.value)).sum::<u64>());
Some(ReplacementNode {
tx: RbfTx {
txid: txid.clone(),
fee: entry.fee,
vsize: entry.vsize,
value,
rate: entry.fee_rate(),
time: entry.first_seen,
rbf: entry.rbf,
full_rbf: None,
},
time: entry.first_seen,
full_rbf,
interval,
replaces,
})
}
pub fn transaction_times(&self, txids: &[Txid]) -> Result<Vec<u64>> {
let mempool = self.mempool().ok_or(Error::MempoolNotAvailable)?;
let entries = mempool.get_entries();
let entries = mempool.entries();
Ok(txids
.iter()
.map(|txid| {

View File

@@ -9,10 +9,10 @@ impl Query {
let mut oracle = self.computer().prices.live_oracle(self.indexer())?;
if let Some(mempool) = self.mempool() {
let txs = mempool.get_txs();
let txs = mempool.txs();
oracle.process_outputs(
txs.values()
.flat_map(|tx| &tx.tx().output)
.flat_map(|tx| &tx.output)
.map(|txout| (txout.value, txout.type_())),
);
}

View File

@@ -3,7 +3,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
use brk_error::{Error, Result};
use brk_traversable::TreeNode;
use brk_types::{
BlockHashPrefix, Date, DetailedSeriesCount, Epoch, Etag, Format, Halving, Height, Index,
BlockHashPrefix, Date, DetailedSeriesCount, Epoch, Format, Halving, Height, Index,
IndexInfo, LegacyValue, Limit, Output, OutputLegacy, PaginatedSeries, Pagination,
PaginationIndex, RangeIndex, RangeMap, SearchQuery, SeriesData, SeriesInfo, SeriesName,
SeriesOutput, SeriesOutputLegacy, SeriesSelection, Timestamp, Version,
@@ -449,7 +449,8 @@ impl Query {
}
/// A resolved series query ready for formatting.
/// Contains the vecs and metadata needed to build an ETag or format the output.
/// Carries the vecs plus the metadata (version, total, end, hash_prefix) callers
/// need to derive an etag or cache policy.
pub struct ResolvedQuery {
pub vecs: Vec<&'static dyn AnyExportableVec>,
pub format: Format,
@@ -462,10 +463,6 @@ pub struct ResolvedQuery {
}
impl ResolvedQuery {
pub fn etag(&self) -> Etag {
Etag::from_series(self.version, self.total, self.end, self.hash_prefix)
}
pub fn format(&self) -> Format {
self.format
}

View File

@@ -1,4 +1,4 @@
use bitcoin::hex::{DisplayHex, FromHex};
use bitcoin::hex::DisplayHex;
use brk_error::{Error, OptionData, Result};
use brk_types::{
BlockHash, Height, MerkleProof, Timestamp, Transaction, TxInIndex, TxIndex, TxOutIndex,
@@ -87,15 +87,15 @@ impl Query {
pub fn transaction(&self, txid: &Txid) -> Result<Transaction> {
if let Some(mempool) = self.mempool()
&& let Some(tx_with_hex) = mempool.get_txs().get(txid)
&& let Some(tx) = mempool.txs().get(txid)
{
return Ok(tx_with_hex.tx().clone());
return Ok(tx.clone());
}
self.transaction_by_index(self.resolve_tx_index(txid)?)
}
pub fn transaction_status(&self, txid: &Txid) -> Result<TxStatus> {
if self.mempool().is_some_and(|m| m.get_txs().contains_key(txid)) {
if self.mempool().is_some_and(|m| m.txs().contains_key(txid)) {
return Ok(TxStatus::UNCONFIRMED);
}
self.confirmed_status(self.resolve_tx_index(txid)?)
@@ -103,19 +103,18 @@ impl Query {
pub fn transaction_raw(&self, txid: &Txid) -> Result<Vec<u8>> {
if let Some(mempool) = self.mempool()
&& let Some(tx_with_hex) = mempool.get_txs().get(txid)
&& let Some(tx) = mempool.txs().get(txid)
{
return Vec::from_hex(tx_with_hex.hex())
.map_err(|_| Error::Parse("Failed to decode mempool tx hex".into()));
return Ok(tx.encode_bytes());
}
self.transaction_raw_by_index(self.resolve_tx_index(txid)?)
}
pub fn transaction_hex(&self, txid: &Txid) -> Result<String> {
if let Some(mempool) = self.mempool()
&& let Some(tx_with_hex) = mempool.get_txs().get(txid)
&& let Some(tx) = mempool.txs().get(txid)
{
return Ok(tx_with_hex.hex().to_string());
return Ok(tx.encode_bytes().to_lower_hex_string());
}
self.transaction_hex_by_index(self.resolve_tx_index(txid)?)
}
@@ -123,7 +122,7 @@ impl Query {
// ── Outspend queries ───────────────────────────────────────────
pub fn outspend(&self, txid: &Txid, vout: Vout) -> Result<TxOutspend> {
if self.mempool().is_some_and(|m| m.get_txs().contains_key(txid)) {
if self.mempool().is_some_and(|m| m.txs().contains_key(txid)) {
return Ok(TxOutspend::UNSPENT);
}
let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?;
@@ -135,9 +134,9 @@ impl Query {
pub fn outspends(&self, txid: &Txid) -> Result<Vec<TxOutspend>> {
if let Some(mempool) = self.mempool()
&& let Some(tx_with_hex) = mempool.get_txs().get(txid)
&& let Some(tx) = mempool.txs().get(txid)
{
return Ok(vec![TxOutspend::UNSPENT; tx_with_hex.tx().output.len()]);
return Ok(vec![TxOutspend::UNSPENT; tx.output.len()]);
}
let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?;
self.resolve_outspends(first_txout, output_count)