mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-20 06:44:47 -07:00
mempool: fixes
This commit is contained in:
@@ -8953,7 +8953,7 @@ pub struct BrkClient {
|
|||||||
|
|
||||||
impl BrkClient {
|
impl BrkClient {
|
||||||
/// Client version.
|
/// Client version.
|
||||||
pub const VERSION: &'static str = "v0.3.0-beta.7";
|
pub const VERSION: &'static str = "v0.3.0-beta.8";
|
||||||
|
|
||||||
/// Create a new client with the given base URL.
|
/// Create a new client with the given base URL.
|
||||||
pub fn new(base_url: impl Into<String>) -> Self {
|
pub fn new(base_url: impl Into<String>) -> Self {
|
||||||
|
|||||||
@@ -41,49 +41,122 @@ pub fn linearize(items: &[ChunkInput<'_>]) -> Vec<CpfpClusterChunk> {
|
|||||||
}
|
}
|
||||||
let mut remaining: Vec<bool> = vec![true; n];
|
let mut remaining: Vec<bool> = vec![true; n];
|
||||||
let mut chunks: Vec<CpfpClusterChunk> = Vec::new();
|
let mut chunks: Vec<CpfpClusterChunk> = Vec::new();
|
||||||
|
let empty: FxHashSet<u32> = FxHashSet::default();
|
||||||
|
|
||||||
while remaining.iter().any(|&r| r) {
|
while remaining.iter().any(|&r| r) {
|
||||||
let mut best: Option<(FeeRate, FxHashSet<u32>)> = None;
|
// Pick the top single-anchored ancestor-closed set. On rate
|
||||||
|
// ties the larger set wins so a uniform-rate chain emits one
|
||||||
|
// chunk instead of n singletons. The extension loop below
|
||||||
|
// catches the same case at zero extra cost, but starting big
|
||||||
|
// shaves iterations.
|
||||||
|
let mut best: Option<(FeeRate, FxHashSet<u32>, Sats, VSize)> = None;
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
if !remaining[i] {
|
if !remaining[i] {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut anc: FxHashSet<u32> =
|
let anc = closure(items, &remaining, &empty, i as u32);
|
||||||
FxHashSet::with_capacity_and_hasher(8, FxBuildHasher);
|
let (fee, vsize) = sum_fee_vsize(items, &anc);
|
||||||
let mut stack: Vec<u32> = vec![i as u32];
|
|
||||||
while let Some(x) = stack.pop() {
|
|
||||||
if !anc.insert(x) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for &p in items[x as usize].parents {
|
|
||||||
let pu: u32 = u32::from(p);
|
|
||||||
if remaining[pu as usize] && !anc.contains(&pu) {
|
|
||||||
stack.push(pu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut fee = Sats::ZERO;
|
|
||||||
let mut vsize = VSize::from(0u64);
|
|
||||||
for &x in &anc {
|
|
||||||
fee += items[x as usize].fee;
|
|
||||||
vsize += items[x as usize].vsize;
|
|
||||||
}
|
|
||||||
let rate = FeeRate::from((fee, vsize));
|
let rate = FeeRate::from((fee, vsize));
|
||||||
match &best {
|
let replace = match &best {
|
||||||
Some((br, _)) if *br >= rate => {}
|
None => true,
|
||||||
_ => best = Some((rate, anc)),
|
Some((br, ba, _, _)) => rate > *br || (rate == *br && anc.len() > ba.len()),
|
||||||
|
};
|
||||||
|
if replace {
|
||||||
|
best = Some((rate, anc, fee, vsize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (rate, set) = best.expect("at least one remaining tx");
|
let (mut chunk_rate, mut anc, mut chunk_fee, mut chunk_vsize) =
|
||||||
let mut indices: Vec<u32> = set.into_iter().collect();
|
best.expect("at least one remaining tx");
|
||||||
|
|
||||||
|
// Extend the chunk with any other remaining ancestor-closed
|
||||||
|
// subset whose union keeps the chunk rate >= current rate.
|
||||||
|
// SFL chunks are the *maximum* ancestor-closed set at the top
|
||||||
|
// rate, but a single anchor only sees one connected component
|
||||||
|
// up to the cluster root: a parent with one long chain plus
|
||||||
|
// additional same-rate sibling children leaves the siblings
|
||||||
|
// stranded as same-rate singleton chunks - which can even
|
||||||
|
// appear "above" the main chunk under integer-vsize rounding,
|
||||||
|
// breaking the descending-rate invariant.
|
||||||
|
loop {
|
||||||
|
let mut best_ext: Option<(FeeRate, FxHashSet<u32>, Sats, VSize)> = None;
|
||||||
|
for i in 0..n {
|
||||||
|
if !remaining[i] || anc.contains(&(i as u32)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let extra = closure(items, &remaining, &anc, i as u32);
|
||||||
|
if extra.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (ef, ev) = sum_fee_vsize(items, &extra);
|
||||||
|
let new_fee = chunk_fee + ef;
|
||||||
|
let new_vsize = chunk_vsize + ev;
|
||||||
|
let new_rate = FeeRate::from((new_fee, new_vsize));
|
||||||
|
if new_rate < chunk_rate {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let replace = match &best_ext {
|
||||||
|
None => true,
|
||||||
|
Some((br, _, _, _)) => new_rate > *br,
|
||||||
|
};
|
||||||
|
if replace {
|
||||||
|
best_ext = Some((new_rate, extra, new_fee, new_vsize));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match best_ext {
|
||||||
|
Some((r, e, f, v)) => {
|
||||||
|
anc.extend(&e);
|
||||||
|
chunk_fee = f;
|
||||||
|
chunk_vsize = v;
|
||||||
|
chunk_rate = r;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut indices: Vec<u32> = anc.into_iter().collect();
|
||||||
indices.sort_unstable();
|
indices.sort_unstable();
|
||||||
for &x in &indices {
|
for &x in &indices {
|
||||||
remaining[x as usize] = false;
|
remaining[x as usize] = false;
|
||||||
}
|
}
|
||||||
let txs: Vec<CpfpClusterTxIndex> =
|
let txs: Vec<CpfpClusterTxIndex> =
|
||||||
indices.into_iter().map(CpfpClusterTxIndex::from).collect();
|
indices.into_iter().map(CpfpClusterTxIndex::from).collect();
|
||||||
chunks.push(CpfpClusterChunk { txs, feerate: rate });
|
chunks.push(CpfpClusterChunk { txs, feerate: chunk_rate });
|
||||||
}
|
}
|
||||||
chunks
|
chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn closure(
|
||||||
|
items: &[ChunkInput<'_>],
|
||||||
|
remaining: &[bool],
|
||||||
|
excluded: &FxHashSet<u32>,
|
||||||
|
start: u32,
|
||||||
|
) -> FxHashSet<u32> {
|
||||||
|
let mut set: FxHashSet<u32> = FxHashSet::with_capacity_and_hasher(8, FxBuildHasher);
|
||||||
|
if !remaining[start as usize] || excluded.contains(&start) {
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
let mut stack: Vec<u32> = vec![start];
|
||||||
|
while let Some(x) = stack.pop() {
|
||||||
|
if !set.insert(x) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for &p in items[x as usize].parents {
|
||||||
|
let pu: u32 = u32::from(p);
|
||||||
|
if remaining[pu as usize] && !excluded.contains(&pu) && !set.contains(&pu) {
|
||||||
|
stack.push(pu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sum_fee_vsize(items: &[ChunkInput<'_>], set: &FxHashSet<u32>) -> (Sats, VSize) {
|
||||||
|
let mut fee = Sats::ZERO;
|
||||||
|
let mut vsize = VSize::from(0u64);
|
||||||
|
for &x in set {
|
||||||
|
fee += items[x as usize].fee;
|
||||||
|
vsize += items[x as usize].vsize;
|
||||||
|
}
|
||||||
|
(fee, vsize)
|
||||||
|
}
|
||||||
|
|||||||
@@ -212,12 +212,16 @@ impl Mempool {
|
|||||||
.map(|e| e.fee_rate())
|
.map(|e| e.fee_rate())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fee rate snapshotted into a graveyard tomb at burial.
|
/// Effective fee rate (Core's chunk rate) snapshotted into the
|
||||||
|
/// tomb's entry at burial - same value `live_effective_fee_rate`
|
||||||
|
/// returns while the tx is alive, so an evicted RBF predecessor
|
||||||
|
/// reports the package-effective rate it had in the mempool, not a
|
||||||
|
/// misleading isolated `fee/vsize`.
|
||||||
pub fn graveyard_fee_rate(&self, txid: &Txid) -> Option<FeeRate> {
|
pub fn graveyard_fee_rate(&self, txid: &Txid) -> Option<FeeRate> {
|
||||||
self.read()
|
self.read()
|
||||||
.graveyard
|
.graveyard
|
||||||
.get(txid)
|
.get(txid)
|
||||||
.map(|tomb| tomb.entry.fee_rate())
|
.map(|tomb| tomb.entry.chunk_rate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `first_seen` Unix-second timestamps for `txids`, in input order.
|
/// `first_seen` Unix-second timestamps for `txids`, in input order.
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ impl Query {
|
|||||||
time: first_seen,
|
time: first_seen,
|
||||||
rbf: node.rbf,
|
rbf: node.rbf,
|
||||||
full_rbf: Some(node.full_rbf),
|
full_rbf: Some(node.full_rbf),
|
||||||
mined,
|
|
||||||
},
|
},
|
||||||
time: first_seen,
|
time: first_seen,
|
||||||
full_rbf: node.full_rbf,
|
full_rbf: node.full_rbf,
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ mod methods;
|
|||||||
use client::ClientInner;
|
use client::ClientInner;
|
||||||
pub use methods::MempoolState;
|
pub use methods::MempoolState;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct BlockchainInfo {
|
|
||||||
pub headers: u64,
|
|
||||||
pub blocks: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BlockInfo {
|
pub struct BlockInfo {
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ use brk_types::{
|
|||||||
Weight,
|
Weight,
|
||||||
};
|
};
|
||||||
use corepc_jsonrpc::error::Error as JsonRpcError;
|
use corepc_jsonrpc::error::Error as JsonRpcError;
|
||||||
use corepc_types::v30::{
|
use corepc_types::{
|
||||||
|
v17::{
|
||||||
GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne,
|
GetBlockCount, GetBlockHash, GetBlockHeader, GetBlockHeaderVerbose, GetBlockVerboseOne,
|
||||||
GetBlockVerboseZero, GetBlockchainInfo, GetMempoolInfo, GetRawMempool, GetTxOut,
|
GetBlockVerboseZero, GetRawMempool, GetTxOut,
|
||||||
|
},
|
||||||
|
v24::GetMempoolInfo,
|
||||||
};
|
};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -21,9 +24,7 @@ use tracing::{debug, info};
|
|||||||
/// The mempool fetcher tolerates these per-item failures silently.
|
/// The mempool fetcher tolerates these per-item failures silently.
|
||||||
const RPC_NOT_FOUND: i32 = -5;
|
const RPC_NOT_FOUND: i32 = -5;
|
||||||
|
|
||||||
use crate::{
|
use crate::{BlockHeaderInfo, BlockInfo, BlockTemplateTx, Client, RawTx, TxOutInfo};
|
||||||
BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, Client, RawTx, TxOutInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Per-batch request count for `get_block_hashes_range`. Sized so the
|
/// Per-batch request count for `get_block_hashes_range`. Sized so the
|
||||||
/// JSON request body stays well under a megabyte and bitcoind doesn't
|
/// JSON request body stays well under a megabyte and bitcoind doesn't
|
||||||
@@ -119,14 +120,6 @@ fn build_min_fee(raw: GetMempoolInfo) -> FeeRate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn get_blockchain_info(&self) -> Result<BlockchainInfo> {
|
|
||||||
let r: GetBlockchainInfo = self.0.call_with_retry("getblockchaininfo", &[])?;
|
|
||||||
Ok(BlockchainInfo {
|
|
||||||
headers: r.headers as u64,
|
|
||||||
blocks: r.blocks as u64,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the numbers of block in the longest chain.
|
/// Returns the numbers of block in the longest chain.
|
||||||
pub fn get_block_count(&self) -> Result<u64> {
|
pub fn get_block_count(&self) -> Result<u64> {
|
||||||
let r: GetBlockCount = self.0.call_with_retry("getblockcount", &[])?;
|
let r: GetBlockCount = self.0.call_with_retry("getblockcount", &[])?;
|
||||||
@@ -440,9 +433,14 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait_for_synced_node(&self) -> Result<()> {
|
pub fn wait_for_synced_node(&self) -> Result<()> {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct SyncProgress {
|
||||||
|
headers: u64,
|
||||||
|
blocks: u64,
|
||||||
|
}
|
||||||
let is_synced = || -> Result<bool> {
|
let is_synced = || -> Result<bool> {
|
||||||
let info = self.get_blockchain_info()?;
|
let p: SyncProgress = self.0.call_with_retry("getblockchaininfo", &[])?;
|
||||||
Ok(info.headers == info.blocks)
|
Ok(p.headers == p.blocks)
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_synced()? {
|
if !is_synced()? {
|
||||||
|
|||||||
@@ -21,11 +21,6 @@ pub struct RbfTx {
|
|||||||
/// this tx displaced at least one non-signaling predecessor.
|
/// this tx displaced at least one non-signaling predecessor.
|
||||||
#[serde(rename = "fullRbf", skip_serializing_if = "Option::is_none", default)]
|
#[serde(rename = "fullRbf", skip_serializing_if = "Option::is_none", default)]
|
||||||
pub full_rbf: Option<bool>,
|
pub full_rbf: Option<bool>,
|
||||||
/// `Some(true)` iff the tx is currently confirmed in the indexed
|
|
||||||
/// chain. Absent on serialization when the tx is still pending or
|
|
||||||
/// has been evicted without confirming.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
|
||||||
pub mined: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One node in an RBF replacement tree. The node's `tx` replaced each
|
/// One node in an RBF replacement tree. The node's `tx` replaced each
|
||||||
|
|||||||
@@ -930,9 +930,6 @@ ancestors and no descendants (matches mempool.space).
|
|||||||
* @property {boolean} rbf - BIP-125 signaling: at least one input has sequence < 0xffffffff-1.
|
* @property {boolean} rbf - BIP-125 signaling: at least one input has sequence < 0xffffffff-1.
|
||||||
* @property {?boolean=} fullRbf - Only populated on the root `tx` of an RBF response. `true` iff
|
* @property {?boolean=} fullRbf - Only populated on the root `tx` of an RBF response. `true` iff
|
||||||
this tx displaced at least one non-signaling predecessor.
|
this tx displaced at least one non-signaling predecessor.
|
||||||
* @property {?boolean=} mined - `Some(true)` iff the tx is currently confirmed in the indexed
|
|
||||||
chain. Absent on serialization when the tx is still pending or
|
|
||||||
has been evicted without confirming.
|
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Recommended fee rates in sat/vB
|
* Recommended fee rates in sat/vB
|
||||||
@@ -7502,7 +7499,7 @@ function createTransferPattern(client, acc) {
|
|||||||
* @extends BrkClientBase
|
* @extends BrkClientBase
|
||||||
*/
|
*/
|
||||||
class BrkClient extends BrkClientBase {
|
class BrkClient extends BrkClientBase {
|
||||||
VERSION = "v0.3.0-beta.7";
|
VERSION = "v0.3.0-beta.8";
|
||||||
|
|
||||||
INDEXES = /** @type {const} */ ([
|
INDEXES = /** @type {const} */ ([
|
||||||
"minute10",
|
"minute10",
|
||||||
|
|||||||
@@ -40,5 +40,5 @@
|
|||||||
"url": "git+https://github.com/bitcoinresearchkit/brk.git"
|
"url": "git+https://github.com/bitcoinresearchkit/brk.git"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.3.0-beta.7"
|
"version": "0.3.0-beta.8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1318,9 +1318,6 @@ class RbfTx(TypedDict):
|
|||||||
rbf: BIP-125 signaling: at least one input has sequence < 0xffffffff-1.
|
rbf: BIP-125 signaling: at least one input has sequence < 0xffffffff-1.
|
||||||
fullRbf: Only populated on the root `tx` of an RBF response. `true` iff
|
fullRbf: Only populated on the root `tx` of an RBF response. `true` iff
|
||||||
this tx displaced at least one non-signaling predecessor.
|
this tx displaced at least one non-signaling predecessor.
|
||||||
mined: `Some(true)` iff the tx is currently confirmed in the indexed
|
|
||||||
chain. Absent on serialization when the tx is still pending or
|
|
||||||
has been evicted without confirming.
|
|
||||||
"""
|
"""
|
||||||
txid: Txid
|
txid: Txid
|
||||||
fee: Sats
|
fee: Sats
|
||||||
@@ -1330,7 +1327,6 @@ has been evicted without confirming.
|
|||||||
time: Timestamp
|
time: Timestamp
|
||||||
rbf: bool
|
rbf: bool
|
||||||
fullRbf: Optional[bool]
|
fullRbf: Optional[bool]
|
||||||
mined: Optional[bool]
|
|
||||||
|
|
||||||
class ReplacementNode(TypedDict):
|
class ReplacementNode(TypedDict):
|
||||||
"""
|
"""
|
||||||
@@ -6653,7 +6649,7 @@ class SeriesTree:
|
|||||||
class BrkClient(BrkClientBase):
|
class BrkClient(BrkClientBase):
|
||||||
"""Main BRK client with series tree and API methods."""
|
"""Main BRK client with series tree and API methods."""
|
||||||
|
|
||||||
VERSION = "v0.3.0-beta.7"
|
VERSION = "v0.3.0-beta.8"
|
||||||
|
|
||||||
INDEXES = [
|
INDEXES = [
|
||||||
"minute10",
|
"minute10",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "brk-client"
|
name = "brk-client"
|
||||||
version = "0.3.0-beta.7"
|
version = "0.3.0-beta.8"
|
||||||
description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index"
|
description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
Reference in New Issue
Block a user