mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 22:34:46 -07:00
global: big snapshot
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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_())),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user