mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-26 18:28:11 -07:00
global: snapshot
This commit is contained in:
@@ -7,12 +7,13 @@
|
||||
//! | `receiving` | Unique addresses that received this block |
|
||||
//! | `sending` | Unique addresses that sent this block |
|
||||
//! | `reactivated` | Addresses that were empty and now have funds |
|
||||
//! | `both` | Addresses that both sent AND received same block |
|
||||
//! | `bidirectional` | Addresses that both sent AND received in same block |
|
||||
//! | `active` | Distinct addresses involved (sent ∪ received) |
|
||||
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU32, Version};
|
||||
use brk_types::{Height, StoredU32, StoredU64, Version};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
@@ -28,7 +29,7 @@ pub struct BlockActivityCounts {
|
||||
pub reactivated: u32,
|
||||
pub sending: u32,
|
||||
pub receiving: u32,
|
||||
pub both: u32,
|
||||
pub bidirectional: u32,
|
||||
}
|
||||
|
||||
impl BlockActivityCounts {
|
||||
@@ -56,7 +57,7 @@ impl AddrTypeToActivityCounts {
|
||||
total.reactivated += counts.reactivated;
|
||||
total.sending += counts.sending;
|
||||
total.receiving += counts.receiving;
|
||||
total.both += counts.both;
|
||||
total.bidirectional += counts.bidirectional;
|
||||
}
|
||||
total
|
||||
}
|
||||
@@ -65,16 +66,25 @@ impl AddrTypeToActivityCounts {
|
||||
/// Activity count vectors for a single category (e.g., one address type or "all").
|
||||
#[derive(Traversable)]
|
||||
pub struct ActivityCountVecs<M: StorageMode = Rw> {
|
||||
pub reactivated: PerBlockRollingAverage<StoredU32, M>,
|
||||
pub sending: PerBlockRollingAverage<StoredU32, M>,
|
||||
pub receiving: PerBlockRollingAverage<StoredU32, M>,
|
||||
pub both: PerBlockRollingAverage<StoredU32, M>,
|
||||
pub reactivated: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
pub sending: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
pub receiving: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
pub bidirectional: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
/// Distinct addresses involved in this block (sent ∪ received),
|
||||
/// computed at push time as `sending + receiving - bidirectional`
|
||||
/// via inclusion-exclusion. For per-type instances this is
|
||||
/// per-type. For the `all` aggregate it's the cross-type total.
|
||||
pub active: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
}
|
||||
|
||||
impl ActivityCountVecs {
|
||||
/// `prefix` is prepended to each field's disk name. Use `""` for the
|
||||
/// "all" aggregate and `"{type}_"` for per-address-type instances.
|
||||
/// Field names are suffixed with `_addrs` so the final disk series
|
||||
/// are e.g. `active_addrs`, `p2tr_bidirectional_addrs`.
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
prefix: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
@@ -82,28 +92,35 @@ impl ActivityCountVecs {
|
||||
Ok(Self {
|
||||
reactivated: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{name}_reactivated"),
|
||||
&format!("{prefix}reactivated_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
sending: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{name}_sending"),
|
||||
&format!("{prefix}sending_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
receiving: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{name}_receiving"),
|
||||
&format!("{prefix}receiving_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
both: PerBlockRollingAverage::forced_import(
|
||||
bidirectional: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{name}_both"),
|
||||
&format!("{prefix}bidirectional_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?,
|
||||
active: PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
&format!("{prefix}active_addrs"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
@@ -117,7 +134,8 @@ impl ActivityCountVecs {
|
||||
.len()
|
||||
.min(self.sending.block.len())
|
||||
.min(self.receiving.block.len())
|
||||
.min(self.both.block.len())
|
||||
.min(self.bidirectional.block.len())
|
||||
.min(self.active.block.len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
@@ -125,9 +143,10 @@ impl ActivityCountVecs {
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
[
|
||||
&mut self.reactivated.block as &mut dyn AnyStoredVec,
|
||||
&mut self.sending.block as &mut dyn AnyStoredVec,
|
||||
&mut self.receiving.block as &mut dyn AnyStoredVec,
|
||||
&mut self.both.block as &mut dyn AnyStoredVec,
|
||||
&mut self.sending.block,
|
||||
&mut self.receiving.block,
|
||||
&mut self.bidirectional.block,
|
||||
&mut self.active.block,
|
||||
]
|
||||
.into_par_iter()
|
||||
}
|
||||
@@ -136,7 +155,8 @@ impl ActivityCountVecs {
|
||||
self.reactivated.block.reset()?;
|
||||
self.sending.block.reset()?;
|
||||
self.receiving.block.reset()?;
|
||||
self.both.block.reset()?;
|
||||
self.bidirectional.block.reset()?;
|
||||
self.active.block.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -145,14 +165,19 @@ impl ActivityCountVecs {
|
||||
self.reactivated.block.push(counts.reactivated.into());
|
||||
self.sending.block.push(counts.sending.into());
|
||||
self.receiving.block.push(counts.receiving.into());
|
||||
self.both.block.push(counts.both.into());
|
||||
self.bidirectional
|
||||
.block
|
||||
.push(counts.bidirectional.into());
|
||||
let active = counts.sending + counts.receiving - counts.bidirectional;
|
||||
self.active.block.push(active.into());
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(&mut self, max_from: Height, exit: &Exit) -> Result<()> {
|
||||
self.reactivated.compute_rest(max_from, exit)?;
|
||||
self.sending.compute_rest(max_from, exit)?;
|
||||
self.receiving.compute_rest(max_from, exit)?;
|
||||
self.both.compute_rest(max_from, exit)?;
|
||||
self.bidirectional.compute_rest(max_from, exit)?;
|
||||
self.active.compute_rest(max_from, exit)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -171,7 +196,6 @@ impl From<ByAddrType<ActivityCountVecs>> for AddrTypeToActivityCountVecs {
|
||||
impl AddrTypeToActivityCountVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
@@ -180,7 +204,7 @@ impl AddrTypeToActivityCountVecs {
|
||||
|type_name| {
|
||||
ActivityCountVecs::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{name}"),
|
||||
&format!("{type_name}_"),
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
@@ -205,7 +229,8 @@ impl AddrTypeToActivityCountVecs {
|
||||
vecs.push(&mut type_vecs.reactivated.block);
|
||||
vecs.push(&mut type_vecs.sending.block);
|
||||
vecs.push(&mut type_vecs.receiving.block);
|
||||
vecs.push(&mut type_vecs.both.block);
|
||||
vecs.push(&mut type_vecs.bidirectional.block);
|
||||
vecs.push(&mut type_vecs.active.block);
|
||||
}
|
||||
vecs.into_par_iter()
|
||||
}
|
||||
@@ -243,16 +268,14 @@ pub struct AddrActivityVecs<M: StorageMode = Rw> {
|
||||
impl AddrActivityVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
name: &str,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
all: ActivityCountVecs::forced_import(db, name, version, indexes, cached_starts)?,
|
||||
all: ActivityCountVecs::forced_import(db, "", version, indexes, cached_starts)?,
|
||||
by_addr_type: AddrTypeToActivityCountVecs::forced_import(
|
||||
db,
|
||||
name,
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
|
||||
@@ -19,7 +19,7 @@ pub use exposed::{
|
||||
pub use indexes::AnyAddrIndexesVecs;
|
||||
pub use new_addr_count::NewAddrCountVecs;
|
||||
pub use reused::{
|
||||
AddrTypeToReusedAddrCount, AddrTypeToReusedAddrUseCount, ReusedAddrVecs,
|
||||
AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, ReusedAddrVecs,
|
||||
};
|
||||
pub use total_addr_count::TotalAddrCountVecs;
|
||||
pub use type_map::{AddrTypeToTypeIndexMap, AddrTypeToVec, HeightToAddrTypeToVec};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Per-block reused-address event tracking. Holds both the output-side
|
||||
//! ("an output landed on a previously-used address") and input-side
|
||||
//! ("an input spent from an address in the reused set") event counters.
|
||||
//! See [`vecs::ReusedAddrEventsVecs`] for the full description of each
|
||||
//! metric.
|
||||
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use state::AddrTypeToReusedAddrEventCount;
|
||||
pub use vecs::ReusedAddrEventsVecs;
|
||||
@@ -0,0 +1,28 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
/// Per-block running counter of reused-address events, per address type.
|
||||
/// Shared runtime container for both output-side events
|
||||
/// (`output_to_reused_addr_count`, outputs landing on addresses that
|
||||
/// had already received ≥ 1 prior output) and input-side events
|
||||
/// (`input_from_reused_addr_count`, inputs spending from addresses
|
||||
/// with lifetime `funded_txo_count > 1`). Reset at the start of each
|
||||
/// block (no disk recovery needed since per-block flow is
|
||||
/// reconstructed deterministically from `process_received` /
|
||||
/// `process_sent`).
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToReusedAddrEventCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToReusedAddrEventCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn reset(&mut self) {
|
||||
for v in self.0.values_mut() {
|
||||
*v = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, Indexes, OutputType, StoredF32, StoredU32, StoredU64, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
|
||||
|
||||
use crate::{
|
||||
indexes, inputs,
|
||||
internal::{
|
||||
PerBlockCumulativeRolling, PerBlockRollingAverage, PercentCumulativeRolling,
|
||||
WindowStartVec, Windows, WithAddrTypes,
|
||||
},
|
||||
outputs,
|
||||
};
|
||||
|
||||
use super::state::AddrTypeToReusedAddrEventCount;
|
||||
|
||||
/// Per-block reused-address event metrics. Holds three families of
|
||||
/// signals: output-level (use), input-level (spend), and address-level
|
||||
/// (active in block).
|
||||
///
|
||||
/// `output_to_reused_addr_count`: every output landing on an address that had
|
||||
/// already received at least one prior output anywhere in its lifetime,
|
||||
/// i.e. an output-level reuse event. Outputs are not deduplicated per
|
||||
/// address within a block: an address receiving N outputs in one block
|
||||
/// that had `before` lifetime outputs contributes
|
||||
/// `max(0, N - max(0, 1 - before))` events. Only the very first output
|
||||
/// an address ever sees is excluded. Every subsequent output counts,
|
||||
/// matching the standard "% of outputs to previously-used addresses"
|
||||
/// reuse ratio reported by external sources. `output_to_reused_addr_share`
|
||||
/// uses `outputs::ByTypeVecs::output_count` (all 12 output types) as
|
||||
/// denominator. `spendable_output_to_reused_addr_share` uses the
|
||||
/// op_return-excluded 11-type aggregate (`spendable_output_count`).
|
||||
///
|
||||
/// `input_from_reused_addr_count`: every input spending from an address
|
||||
/// whose lifetime `funded_txo_count > 1` at the time of the spend (i.e.
|
||||
/// the address is in the same reused set tracked by
|
||||
/// `reused_addr_count`). Every input is checked independently. If a
|
||||
/// single address has multiple inputs in one block each one counts.
|
||||
/// This is a *stable-predicate* signal about the sending address, not
|
||||
/// an output-level repeat event: the first spend from a reused address
|
||||
/// counts just as much as the tenth. Denominator
|
||||
/// (`input_from_reused_addr_share`): `inputs::ByTypeVecs::input_count` (11
|
||||
/// spendable types, where `p2ms`, `unknown`, `empty` count as true
|
||||
/// negatives).
|
||||
///
|
||||
/// `active_reused_addr_count` / `active_reused_addr_share`: block-level
|
||||
/// *address* signals (single aggregate, not per-type).
|
||||
/// `active_reused_addr_count` is the count of distinct addresses
|
||||
/// involved in this block (sent ∪ received) that satisfy `is_reused()`
|
||||
/// after the block's events, populated inline in `process_received`
|
||||
/// (each receiver, post-receive) and in `process_sent` (each
|
||||
/// first-encounter sender, deduped against `received_addrs` so
|
||||
/// addresses that did both aren't double-counted).
|
||||
/// `active_reused_addr_share` is the per-block ratio
|
||||
/// `reused / active * 100` as a percentage in `[0, 100]` (or `0.0` for
|
||||
/// empty blocks). The denominator (distinct active addrs per block)
|
||||
/// lives on `ActivityCountVecs::active` (`addrs.activity.all.active`),
|
||||
/// derived from `sending + receiving - bidirectional`. Both fields
|
||||
/// use `PerBlockRollingAverage` so their lazy 24h/1w/1m/1y series are
|
||||
/// rolling *averages* of the per-block values. Sums and cumulatives of
|
||||
/// distinct-address counts would be misleading because the same
|
||||
/// address can appear in multiple blocks.
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrEventsVecs<M: StorageMode = Rw> {
|
||||
pub output_to_reused_addr_count:
|
||||
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
|
||||
pub output_to_reused_addr_share: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
|
||||
pub spendable_output_to_reused_addr_share: PercentCumulativeRolling<BasisPoints16, M>,
|
||||
pub input_from_reused_addr_count:
|
||||
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
|
||||
pub input_from_reused_addr_share: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
|
||||
pub active_reused_addr_count: PerBlockRollingAverage<StoredU32, StoredU64, M>,
|
||||
pub active_reused_addr_share: PerBlockRollingAverage<StoredF32, StoredF32, M>,
|
||||
}
|
||||
|
||||
impl ReusedAddrEventsVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let import_count = |name: &str| {
|
||||
WithAddrTypes::<PerBlockCumulativeRolling<StoredU64, StoredU64>>::forced_import(
|
||||
db,
|
||||
name,
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)
|
||||
};
|
||||
let import_percent = |name: &str| -> Result<WithAddrTypes<
|
||||
PercentCumulativeRolling<BasisPoints16>,
|
||||
>> {
|
||||
Ok(WithAddrTypes {
|
||||
all: PercentCumulativeRolling::forced_import(db, name, version, indexes)?,
|
||||
by_addr_type: ByAddrType::new_with_name(|type_name| {
|
||||
PercentCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{name}"),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
})?,
|
||||
})
|
||||
};
|
||||
|
||||
let output_to_reused_addr_count = import_count("output_to_reused_addr_count")?;
|
||||
let output_to_reused_addr_share = import_percent("output_to_reused_addr_share")?;
|
||||
let spendable_output_to_reused_addr_share = PercentCumulativeRolling::forced_import(
|
||||
db,
|
||||
"spendable_output_to_reused_addr_share",
|
||||
version,
|
||||
indexes,
|
||||
)?;
|
||||
let input_from_reused_addr_count = import_count("input_from_reused_addr_count")?;
|
||||
let input_from_reused_addr_share = import_percent("input_from_reused_addr_share")?;
|
||||
|
||||
let active_reused_addr_count = PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
"active_reused_addr_count",
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?;
|
||||
let active_reused_addr_share = PerBlockRollingAverage::forced_import(
|
||||
db,
|
||||
"active_reused_addr_share",
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
output_to_reused_addr_count,
|
||||
output_to_reused_addr_share,
|
||||
spendable_output_to_reused_addr_share,
|
||||
input_from_reused_addr_count,
|
||||
input_from_reused_addr_share,
|
||||
active_reused_addr_count,
|
||||
active_reused_addr_share,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.output_to_reused_addr_count
|
||||
.min_stateful_len()
|
||||
.min(self.input_from_reused_addr_count.min_stateful_len())
|
||||
.min(self.active_reused_addr_count.block.len())
|
||||
.min(self.active_reused_addr_share.block.len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.output_to_reused_addr_count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.input_from_reused_addr_count.par_iter_height_mut())
|
||||
.chain([
|
||||
&mut self.active_reused_addr_count.block as &mut dyn AnyStoredVec,
|
||||
&mut self.active_reused_addr_share.block as &mut dyn AnyStoredVec,
|
||||
])
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.output_to_reused_addr_count.reset_height()?;
|
||||
self.input_from_reused_addr_count.reset_height()?;
|
||||
self.active_reused_addr_count.block.reset()?;
|
||||
self.active_reused_addr_share.block.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(
|
||||
&mut self,
|
||||
uses: &AddrTypeToReusedAddrEventCount,
|
||||
spends: &AddrTypeToReusedAddrEventCount,
|
||||
active_addr_count: u32,
|
||||
active_reused_addr_count: u32,
|
||||
) {
|
||||
self.output_to_reused_addr_count
|
||||
.push_height(uses.sum(), uses.values().copied());
|
||||
self.input_from_reused_addr_count
|
||||
.push_height(spends.sum(), spends.values().copied());
|
||||
self.active_reused_addr_count
|
||||
.block
|
||||
.push(StoredU32::from(active_reused_addr_count));
|
||||
// Stored as a percentage in [0, 100] to match the rest of the
|
||||
// codebase (Unit.percentage on the website expects 0..100). The
|
||||
// `active_addr_count` denominator lives on `ActivityCountVecs`
|
||||
// (`addrs.activity.all.active`), passed in here so we can
|
||||
// compute the per-block ratio inline.
|
||||
let share = if active_addr_count > 0 {
|
||||
100.0 * (active_reused_addr_count as f32 / active_addr_count as f32)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.active_reused_addr_share
|
||||
.block
|
||||
.push(StoredF32::from(share));
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
outputs_by_type: &outputs::ByTypeVecs,
|
||||
inputs_by_type: &inputs::ByTypeVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.output_to_reused_addr_count
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
self.input_from_reused_addr_count
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
self.active_reused_addr_count
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
self.active_reused_addr_share
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
|
||||
self.output_to_reused_addr_share.all.compute_count_ratio(
|
||||
&self.output_to_reused_addr_count.all,
|
||||
&outputs_by_type.output_count.all,
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
self.spendable_output_to_reused_addr_share.compute_count_ratio(
|
||||
&self.output_to_reused_addr_count.all,
|
||||
&outputs_by_type.spendable_output_count,
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
self.input_from_reused_addr_share.all.compute_count_ratio(
|
||||
&self.input_from_reused_addr_count.all,
|
||||
&inputs_by_type.input_count.all,
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
for otype in OutputType::ADDR_TYPES {
|
||||
self.output_to_reused_addr_share
|
||||
.by_addr_type
|
||||
.get_mut_unwrap(otype)
|
||||
.compute_count_ratio(
|
||||
self.output_to_reused_addr_count.by_addr_type.get_unwrap(otype),
|
||||
outputs_by_type.output_count.by_type.get(otype),
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
self.input_from_reused_addr_share
|
||||
.by_addr_type
|
||||
.get_mut_unwrap(otype)
|
||||
.compute_count_ratio(
|
||||
self.input_from_reused_addr_count.by_addr_type.get_unwrap(otype),
|
||||
inputs_by_type.input_count.by_type.get(otype),
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,26 @@
|
||||
//! Reused address tracking.
|
||||
//!
|
||||
//! An address is "reused" if its lifetime `funded_txo_count > 1` — i.e. it
|
||||
//! has received more than one output across its lifetime. This is the
|
||||
//! simplest output-multiplicity proxy for address linkability.
|
||||
//! An address is "reused" if its lifetime `funded_txo_count > 1`, i.e.
|
||||
//! it has received more than one output across its lifetime. This is
|
||||
//! the simplest output-multiplicity proxy for address linkability.
|
||||
//!
|
||||
//! Two facets are tracked here:
|
||||
//! - [`count`] — how many distinct addresses are currently reused (funded)
|
||||
//! and how many have *ever* been reused (total). Per address type plus
|
||||
//! an aggregated `all`.
|
||||
//! - [`uses`] — per-block count of outputs going to addresses that were
|
||||
//! already reused, plus the derived percent over total address-output
|
||||
//! count (denominator from `outputs::by_type`).
|
||||
//! - [`count`]: how many distinct addresses are currently reused
|
||||
//! (funded) and how many have *ever* been reused (total). Per address
|
||||
//! type plus an aggregated `all`.
|
||||
//! - [`events`]: per-block address-reuse event counts on both sides.
|
||||
//! Output-side (`output_to_reused_addr_count`, outputs landing on
|
||||
//! addresses that had already received ≥ 1 prior output) and
|
||||
//! input-side (`input_from_reused_addr_count`, inputs spending from
|
||||
//! addresses with lifetime `funded_txo_count > 1`). Each count is
|
||||
//! paired with a percent over the matching block-level output/input
|
||||
//! total.
|
||||
|
||||
mod count;
|
||||
mod uses;
|
||||
mod events;
|
||||
|
||||
pub use count::{AddrTypeToReusedAddrCount, ReusedAddrCountsVecs};
|
||||
pub use uses::{AddrTypeToReusedAddrUseCount, ReusedAddrUsesVecs};
|
||||
pub use events::{AddrTypeToReusedAddrEventCount, ReusedAddrEventsVecs};
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
@@ -25,17 +29,17 @@ use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
indexes, inputs,
|
||||
internal::{WindowStartVec, Windows},
|
||||
outputs,
|
||||
};
|
||||
|
||||
/// Top-level container for all reused address tracking: counts (funded +
|
||||
/// total) plus per-block uses (count + percent).
|
||||
/// total) plus per-block reuse events (output-side + input-side).
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrVecs<M: StorageMode = Rw> {
|
||||
pub count: ReusedAddrCountsVecs<M>,
|
||||
pub uses: ReusedAddrUsesVecs<M>,
|
||||
pub events: ReusedAddrEventsVecs<M>,
|
||||
}
|
||||
|
||||
impl ReusedAddrVecs {
|
||||
@@ -47,14 +51,14 @@ impl ReusedAddrVecs {
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
count: ReusedAddrCountsVecs::forced_import(db, version, indexes)?,
|
||||
uses: ReusedAddrUsesVecs::forced_import(db, version, indexes, cached_starts)?,
|
||||
events: ReusedAddrEventsVecs::forced_import(db, version, indexes, cached_starts)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.count
|
||||
.min_stateful_len()
|
||||
.min(self.uses.min_stateful_len())
|
||||
.min(self.events.min_stateful_len())
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
@@ -62,12 +66,12 @@ impl ReusedAddrVecs {
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.count
|
||||
.par_iter_height_mut()
|
||||
.chain(self.uses.par_iter_height_mut())
|
||||
.chain(self.events.par_iter_height_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.count.reset_height()?;
|
||||
self.uses.reset_height()?;
|
||||
self.events.reset_height()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -75,11 +79,16 @@ impl ReusedAddrVecs {
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
outputs_by_type: &outputs::ByTypeVecs,
|
||||
inputs_by_type: &inputs::ByTypeVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.count.compute_rest(starting_indexes, exit)?;
|
||||
self.uses
|
||||
.compute_rest(starting_indexes, outputs_by_type, exit)?;
|
||||
self.events.compute_rest(
|
||||
starting_indexes,
|
||||
outputs_by_type,
|
||||
inputs_by_type,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
//! Per-block reused-address-use tracking. See [`vecs::ReusedAddrUsesVecs`]
|
||||
//! for the full description of the metric.
|
||||
|
||||
mod state;
|
||||
mod vecs;
|
||||
|
||||
pub use state::AddrTypeToReusedAddrUseCount;
|
||||
pub use vecs::ReusedAddrUsesVecs;
|
||||
@@ -1,22 +0,0 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
/// Per-block running counter of reused address uses, per address type.
|
||||
/// Reset at the start of each block (no disk recovery needed since the
|
||||
/// per-block flow is reconstructed from `process_received` deterministically).
|
||||
#[derive(Debug, Default, Deref, DerefMut)]
|
||||
pub struct AddrTypeToReusedAddrUseCount(ByAddrType<u64>);
|
||||
|
||||
impl AddrTypeToReusedAddrUseCount {
|
||||
#[inline]
|
||||
pub(crate) fn sum(&self) -> u64 {
|
||||
self.0.values().sum()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn reset(&mut self) {
|
||||
for v in self.0.values_mut() {
|
||||
*v = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
use brk_cohort::ByAddrType;
|
||||
use brk_error::Result;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{BasisPoints16, Indexes, OutputType, StoredU64, Version};
|
||||
use rayon::prelude::*;
|
||||
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
|
||||
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{
|
||||
PerBlockCumulativeRolling, PercentCumulativeRolling, WindowStartVec, Windows,
|
||||
WithAddrTypes,
|
||||
},
|
||||
outputs,
|
||||
};
|
||||
|
||||
use super::state::AddrTypeToReusedAddrUseCount;
|
||||
|
||||
/// Per-block reused-address-use metrics. A "use" is a single output going
|
||||
/// to an address (not deduplicated): an address receiving N outputs in one
|
||||
/// block contributes N. The count only includes uses going to addresses
|
||||
/// that were *already* reused at the moment of the use, so the use that
|
||||
/// makes an address reused is not itself counted.
|
||||
///
|
||||
/// The denominator for the percent (per-type and aggregate address-output
|
||||
/// counts) is read from `outputs::ByTypeVecs::output_count` rather than
|
||||
/// duplicated here.
|
||||
#[derive(Traversable)]
|
||||
pub struct ReusedAddrUsesVecs<M: StorageMode = Rw> {
|
||||
pub reused_addr_use_count:
|
||||
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
|
||||
pub reused_addr_use_percent: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
|
||||
}
|
||||
|
||||
impl ReusedAddrUsesVecs {
|
||||
pub(crate) fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
cached_starts: &Windows<&WindowStartVec>,
|
||||
) -> Result<Self> {
|
||||
let reused_addr_use_count =
|
||||
WithAddrTypes::<PerBlockCumulativeRolling<StoredU64, StoredU64>>::forced_import(
|
||||
db,
|
||||
"reused_addr_use_count",
|
||||
version,
|
||||
indexes,
|
||||
cached_starts,
|
||||
)?;
|
||||
let percent_name = "reused_addr_use_percent";
|
||||
let reused_addr_use_percent = WithAddrTypes {
|
||||
all: PercentCumulativeRolling::forced_import(db, percent_name, version, indexes)?,
|
||||
by_addr_type: ByAddrType::new_with_name(|type_name| {
|
||||
PercentCumulativeRolling::forced_import(
|
||||
db,
|
||||
&format!("{type_name}_{percent_name}"),
|
||||
version,
|
||||
indexes,
|
||||
)
|
||||
})?,
|
||||
};
|
||||
Ok(Self {
|
||||
reused_addr_use_count,
|
||||
reused_addr_use_percent,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn min_stateful_len(&self) -> usize {
|
||||
self.reused_addr_use_count.min_stateful_len()
|
||||
}
|
||||
|
||||
pub(crate) fn par_iter_height_mut(
|
||||
&mut self,
|
||||
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
|
||||
self.reused_addr_use_count.par_iter_height_mut()
|
||||
}
|
||||
|
||||
pub(crate) fn reset_height(&mut self) -> Result<()> {
|
||||
self.reused_addr_use_count.reset_height()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn push_height(&mut self, reused: &AddrTypeToReusedAddrUseCount) {
|
||||
self.reused_addr_use_count
|
||||
.push_height(reused.sum(), reused.values().copied());
|
||||
}
|
||||
|
||||
pub(crate) fn compute_rest(
|
||||
&mut self,
|
||||
starting_indexes: &Indexes,
|
||||
outputs_by_type: &outputs::ByTypeVecs,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.reused_addr_use_count
|
||||
.compute_rest(starting_indexes.height, exit)?;
|
||||
|
||||
self.reused_addr_use_percent.all.compute_count_ratio(
|
||||
&self.reused_addr_use_count.all,
|
||||
&outputs_by_type.output_count.all,
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
for otype in OutputType::ADDR_TYPES {
|
||||
self.reused_addr_use_percent
|
||||
.by_addr_type
|
||||
.get_mut_unwrap(otype)
|
||||
.compute_count_ratio(
|
||||
self.reused_addr_use_count.by_addr_type.get_unwrap(otype),
|
||||
outputs_by_type.output_count.by_type.get(otype),
|
||||
starting_indexes.height,
|
||||
exit,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user