global: reused + mempool + favicon

This commit is contained in:
nym21
2026-04-23 23:13:39 +02:00
parent ce00de5da8
commit e4496742a4
77 changed files with 2631 additions and 1624 deletions
@@ -1,183 +0,0 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode,
WritableVec,
};
use crate::{indexes, internal::PerBlock};
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrCountVecs<M: StorageMode = Rw>(#[traversable(flatten)] pub PerBlock<StoredU64, M>);
impl AddrCountVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(PerBlock::forced_import(db, name, version, indexes)?))
}
}
/// Address count per address type (runtime state).
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToAddrCount(ByAddrType<u64>);
impl AddrTypeToAddrCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
}
}
impl From<(&AddrTypeToAddrCountVecs, Height)> for AddrTypeToAddrCount {
#[inline]
fn from((groups, starting_height): (&AddrTypeToAddrCountVecs, Height)) -> Self {
if let Some(prev_height) = starting_height.decremented() {
Self(ByAddrType {
p2pk65: groups
.p2pk65
.height
.collect_one(prev_height)
.unwrap()
.into(),
p2pk33: groups
.p2pk33
.height
.collect_one(prev_height)
.unwrap()
.into(),
p2pkh: groups.p2pkh.height.collect_one(prev_height).unwrap().into(),
p2sh: groups.p2sh.height.collect_one(prev_height).unwrap().into(),
p2wpkh: groups
.p2wpkh
.height
.collect_one(prev_height)
.unwrap()
.into(),
p2wsh: groups.p2wsh.height.collect_one(prev_height).unwrap().into(),
p2tr: groups.p2tr.height.collect_one(prev_height).unwrap().into(),
p2a: groups.p2a.height.collect_one(prev_height).unwrap().into(),
})
} else {
Default::default()
}
}
}
/// Address count per address type, with height + derived indexes.
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrTypeToAddrCountVecs<M: StorageMode = Rw>(ByAddrType<AddrCountVecs<M>>);
impl From<ByAddrType<AddrCountVecs>> for AddrTypeToAddrCountVecs {
#[inline]
fn from(value: ByAddrType<AddrCountVecs>) -> Self {
Self(value)
}
}
impl AddrTypeToAddrCountVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self::from(ByAddrType::<AddrCountVecs>::new_with_name(
|type_name| {
AddrCountVecs::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
},
)?))
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.0.values().map(|v| v.height.len()).min().unwrap()
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.0
.par_values_mut()
.map(|v| &mut v.height as &mut dyn AnyStoredVec)
}
#[inline(always)]
pub(crate) fn push_height(&mut self, addr_counts: &AddrTypeToAddrCount) {
for (vecs, &count) in self.0.values_mut().zip(addr_counts.values()) {
vecs.height.push(count.into());
}
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
for v in self.0.values_mut() {
v.height.reset()?;
}
Ok(())
}
pub(crate) fn by_height(&self) -> Vec<&EagerVec<PcoVec<Height, StoredU64>>> {
self.0.values().map(|v| &v.height).collect()
}
}
#[derive(Traversable)]
pub struct AddrCountsVecs<M: StorageMode = Rw> {
pub all: AddrCountVecs<M>,
#[traversable(flatten)]
pub by_addr_type: AddrTypeToAddrCountVecs<M>,
}
impl AddrCountsVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
all: AddrCountVecs::forced_import(db, name, version, indexes)?,
by_addr_type: AddrTypeToAddrCountVecs::forced_import(db, name, version, indexes)?,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.all
.height
.len()
.min(self.by_addr_type.min_stateful_len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
rayon::iter::once(&mut self.all.height as &mut dyn AnyStoredVec)
.chain(self.by_addr_type.par_iter_height_mut())
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.all.height.reset()?;
self.by_addr_type.reset_height()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(&mut self, total: u64, addr_counts: &AddrTypeToAddrCount) {
self.all.height.push(total.into());
self.by_addr_type.push_height(addr_counts);
}
pub(crate) fn compute_rest(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
let sources = self.by_addr_type.by_height();
self.all
.height
.compute_sum_of_others(starting_indexes.height, &sources, exit)?;
Ok(())
}
}
@@ -9,13 +9,17 @@ use crate::{
internal::{PerBlock, WithAddrTypes},
};
/// Reused address count (`all` + per-type) for a single variant (funded or total).
use super::AddrTypeToAddrCount;
/// Per-block `StoredU64` counts with an aggregate `all` plus a per-address-type
/// breakdown. Shared primitive backing addr-count, empty-addr-count, and the
/// funded/total pairs used by exposed, reused, and respent.
#[derive(Deref, DerefMut, Traversable)]
pub struct ReusedAddrCountAllVecs<M: StorageMode = Rw>(
pub struct AddrCountsVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
);
impl ReusedAddrCountAllVecs {
impl AddrCountsVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
@@ -26,4 +30,9 @@ impl ReusedAddrCountAllVecs {
db, name, version, indexes,
)?))
}
#[inline(always)]
pub(crate) fn push_counts(&mut self, counts: &AddrTypeToAddrCount) {
self.push_height(counts.sum(), counts.values().copied());
}
}
@@ -1,14 +1,3 @@
//! Exposed address count tracking — running counters of how many addresses
//! are currently in (or have ever been in) the exposed set, per address type
//! plus an aggregated `all`. See the parent [`super`] module for the
//! definition of "exposed" and how it varies by address type.
mod state;
mod vecs;
pub use state::AddrTypeToExposedAddrCount;
pub use vecs::ExposedAddrCountAllVecs;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Indexes, Version};
@@ -17,29 +6,34 @@ use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
use crate::indexes;
/// Exposed address counts: funded (currently at-risk) and total (ever at-risk).
use super::{AddrCountsVecs, AddrTypeToAddrCount};
/// Paired funded + cumulative-total address counts, used by exposed, reused,
/// and respent. On-disk naming: `"{name}_addr_count"` (funded) and
/// `"total_{name}_addr_count"` (total).
#[derive(Traversable)]
pub struct ExposedAddrCountsVecs<M: StorageMode = Rw> {
pub funded: ExposedAddrCountAllVecs<M>,
pub total: ExposedAddrCountAllVecs<M>,
pub struct AddrCountFundedTotalVecs<M: StorageMode = Rw> {
pub funded: AddrCountsVecs<M>,
pub total: AddrCountsVecs<M>,
}
impl ExposedAddrCountsVecs {
impl AddrCountFundedTotalVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
funded: ExposedAddrCountAllVecs::forced_import(
funded: AddrCountsVecs::forced_import(
db,
"exposed_addr_count",
&format!("{name}_addr_count"),
version,
indexes,
)?,
total: ExposedAddrCountAllVecs::forced_import(
total: AddrCountsVecs::forced_import(
db,
"total_exposed_addr_count",
&format!("total_{name}_addr_count"),
version,
indexes,
)?,
@@ -66,6 +60,16 @@ impl ExposedAddrCountsVecs {
Ok(())
}
#[inline(always)]
pub(crate) fn push_counts(
&mut self,
funded: &AddrTypeToAddrCount,
total: &AddrTypeToAddrCount,
) {
self.funded.push_counts(funded);
self.total.push_counts(total);
}
pub(crate) fn compute_rest(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
self.funded.compute_rest(starting_indexes, exit)?;
self.total.compute_rest(starting_indexes, exit)?;
@@ -0,0 +1,7 @@
mod all_vecs;
mod funded_total_vecs;
mod state;
pub use all_vecs::AddrCountsVecs;
pub use funded_total_vecs::AddrCountFundedTotalVecs;
pub use state::AddrTypeToAddrCount;
@@ -0,0 +1,38 @@
use brk_cohort::ByAddrType;
use brk_types::Height;
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use super::AddrCountsVecs;
/// Per-addr-type address-count running total. Shared runtime state across
/// funded / empty / exposed / reused / respent counters; paired with
/// [`AddrCountsVecs`] on disk.
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToAddrCount(ByAddrType<u64>);
impl AddrTypeToAddrCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
}
}
impl From<ByAddrType<u64>> for AddrTypeToAddrCount {
#[inline]
fn from(value: ByAddrType<u64>) -> Self {
Self(value)
}
}
impl From<(&AddrCountsVecs, Height)> for AddrTypeToAddrCount {
#[inline]
fn from((vecs, starting_height): (&AddrCountsVecs, Height)) -> Self {
let Some(prev_height) = starting_height.decremented() else {
return Self::default();
};
vecs.by_addr_type
.map_with_name(|_, v| v.height.collect_one(prev_height).unwrap().into())
.into()
}
}
@@ -26,7 +26,7 @@ impl DeltaVecs {
let all = LazyRollingDeltasFromHeight::new(
"addr_count",
version,
&addr_count.all.0.height,
&addr_count.all.height,
cached_starts,
indexes,
);
@@ -35,7 +35,7 @@ impl DeltaVecs {
LazyRollingDeltasFromHeight::new(
&format!("{name}_addr_count"),
version,
&addr.0.height,
&addr.height,
cached_starts,
indexes,
)
@@ -1,42 +0,0 @@
use brk_cohort::ByAddrType;
use brk_types::{Height, StoredU64};
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use crate::internal::PerBlock;
use super::vecs::ExposedAddrCountAllVecs;
/// Runtime counter for exposed address counts per address type.
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToExposedAddrCount(ByAddrType<u64>);
impl AddrTypeToExposedAddrCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
}
}
impl From<(&ExposedAddrCountAllVecs, Height)> for AddrTypeToExposedAddrCount {
#[inline]
fn from((vecs, starting_height): (&ExposedAddrCountAllVecs, Height)) -> Self {
if let Some(prev_height) = starting_height.decremented() {
let read = |v: &PerBlock<StoredU64>| -> u64 {
v.height.collect_one(prev_height).unwrap().into()
};
Self(ByAddrType {
p2pk65: read(&vecs.by_addr_type.p2pk65),
p2pk33: read(&vecs.by_addr_type.p2pk33),
p2pkh: read(&vecs.by_addr_type.p2pkh),
p2sh: read(&vecs.by_addr_type.p2sh),
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
p2wsh: read(&vecs.by_addr_type.p2wsh),
p2tr: read(&vecs.by_addr_type.p2tr),
p2a: read(&vecs.by_addr_type.p2a),
})
} else {
Default::default()
}
}
}
@@ -1,29 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{StoredU64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Rw, StorageMode};
use crate::{
indexes,
internal::{PerBlock, WithAddrTypes},
};
/// Exposed address count (`all` + per-type) for a single variant (funded or total).
#[derive(Deref, DerefMut, Traversable)]
pub struct ExposedAddrCountAllVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
);
impl ExposedAddrCountAllVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
db, name, version, indexes,
)?))
}
}
@@ -12,7 +12,7 @@
//! - **P2PKH, P2SH, P2WPKH, P2WSH**: the locking script contains a hash of
//! the pubkey/script. The pubkey is only revealed when spending. Note that
//! even the spending tx itself exposes the pubkey while the address still
//! holds funds during the mempool window between broadcast and confirmation,
//! holds funds, during the mempool window between broadcast and confirmation,
//! the pubkey is visible while the UTXO being spent is still unspent on-chain.
//! So every spent address of these types has had at least one moment with
//! funds at quantum risk.
@@ -30,15 +30,9 @@
//! sums these, giving "Bitcoin addresses currently with funds at quantum risk".
//!
//! All metrics are tracked as running counters and require no extra fields
//! on the address data — they're maintained via delta detection in
//! on the address data. They're maintained via delta detection in
//! `process_received` and `process_sent`.
mod count;
mod supply;
pub use count::{AddrTypeToExposedAddrCount, ExposedAddrCountsVecs};
pub use supply::{AddrTypeToExposedSupply, ExposedAddrSupplyVecs, ExposedSupplyShareVecs};
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
@@ -46,16 +40,24 @@ use brk_types::{Height, Indexes, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{indexes, internal::RatioSatsBp16, prices};
use super::{
count::AddrCountFundedTotalVecs,
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
};
use crate::{indexes, prices};
mod state;
pub use state::ExposedAddrState;
/// Top-level container for all exposed address tracking: counts (funded +
/// total), the funded supply, and share of supply.
#[derive(Traversable)]
pub struct ExposedAddrVecs<M: StorageMode = Rw> {
pub count: ExposedAddrCountsVecs<M>,
pub supply: ExposedAddrSupplyVecs<M>,
pub count: AddrCountFundedTotalVecs<M>,
pub supply: AddrSupplyVecs<M>,
#[traversable(wrap = "supply", rename = "share")]
pub supply_share: ExposedSupplyShareVecs<M>,
pub supply_share: AddrSupplyShareVecs<M>,
}
impl ExposedAddrVecs {
@@ -65,9 +67,9 @@ impl ExposedAddrVecs {
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
count: ExposedAddrCountsVecs::forced_import(db, version, indexes)?,
supply: ExposedAddrSupplyVecs::forced_import(db, version, indexes)?,
supply_share: ExposedSupplyShareVecs::forced_import(db, version, indexes)?,
count: AddrCountFundedTotalVecs::forced_import(db, "exposed", version, indexes)?,
supply: AddrSupplyVecs::forced_import(db, "exposed", version, indexes)?,
supply_share: AddrSupplyShareVecs::forced_import(db, "exposed", version, indexes)?,
})
}
@@ -92,6 +94,12 @@ impl ExposedAddrVecs {
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(&mut self, state: &ExposedAddrState) {
self.count.push_counts(&state.funded, &state.total);
self.supply.push_supply(&state.supply);
}
pub(crate) fn compute_rest(
&mut self,
starting_indexes: &Indexes,
@@ -103,32 +111,13 @@ impl ExposedAddrVecs {
self.count.compute_rest(starting_indexes, exit)?;
self.supply
.compute_rest(starting_indexes.height, prices, exit)?;
let max_from = starting_indexes.height;
self.supply_share
.all
.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&self.supply.all.sats.height,
all_supply_sats,
exit,
)?;
for ((_, share), ((_, exposed), (_, denom))) in self
.supply_share
.by_addr_type
.iter_mut()
.zip(self.supply.by_addr_type.iter().zip(type_supply_sats.iter()))
{
share.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&exposed.sats.height,
*denom,
exit,
)?;
}
self.supply_share.compute_rest(
starting_indexes.height,
&self.supply,
all_supply_sats,
type_supply_sats,
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,83 @@
use brk_types::{FundedAddrData, Height, OutputType};
use crate::distribution::{
addr::{AddrReceivePreState, AddrSendPreState, AddrTypeToAddrCount, AddrTypeToSupply},
block::TrackingStatus,
};
use super::ExposedAddrVecs;
/// Runtime running totals for exposed-addr tracking. Mirrors the persistent
/// fields of [`ExposedAddrVecs`]: funded count, total count, funded supply.
/// Recovered from disk at the start of a block-loop run.
#[derive(Debug, Default)]
pub struct ExposedAddrState {
pub funded: AddrTypeToAddrCount,
pub total: AddrTypeToAddrCount,
pub supply: AddrTypeToSupply,
}
impl ExposedAddrState {
/// Apply exposed-addr updates for a received output, AFTER the receive
/// has mutated `addr_data`. `pre` is the snapshot taken before the mutation.
#[inline]
pub(crate) fn on_receive(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrReceivePreState,
status: TrackingStatus,
) {
// Pubkey-exposure state is unchanged by a receive, so `pre.was_pubkey_exposed`
// equals the post-receive value.
if !pre.was_funded && pre.was_pubkey_exposed {
*self.funded.get_mut_unwrap(output_type) += 1;
}
// Total for pk-exposed-at-funding types fires here on first receive.
// Other types fire on first spend in [`Self::on_send`].
if output_type.pubkey_exposed_at_funding() && matches!(status, TrackingStatus::New) {
*self.total.get_mut_unwrap(output_type) += 1;
}
let after = addr_data.exposed_supply_contribution(output_type);
self.supply
.apply_delta(output_type, pre.exposed_contribution, after);
}
/// Apply exposed-addr updates for a spent UTXO, AFTER the send has mutated
/// `addr_data`. `pre` is the snapshot taken before the mutation.
#[inline]
pub(crate) fn on_send(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrSendPreState,
will_be_empty: bool,
) {
let after = addr_data.exposed_supply_contribution(output_type);
self.supply
.apply_delta(output_type, pre.exposed_contribution, after);
// First-ever pubkey exposure. Non-pk-exposed types fire on first spend.
// Pk-exposed types never fire here (was already exposed at first receive).
if !pre.was_pubkey_exposed {
*self.total.get_mut_unwrap(output_type) += 1;
if !will_be_empty {
*self.funded.get_mut_unwrap(output_type) += 1;
}
}
// Leaving the funded exposed set: was in it iff pubkey was exposed pre-spend.
if will_be_empty && pre.was_pubkey_exposed {
*self.funded.get_mut_unwrap(output_type) -= 1;
}
}
}
impl From<(&ExposedAddrVecs, Height)> for ExposedAddrState {
#[inline]
fn from((vecs, starting_height): (&ExposedAddrVecs, Height)) -> Self {
Self {
funded: AddrTypeToAddrCount::from((&vecs.count.funded, starting_height)),
total: AddrTypeToAddrCount::from((&vecs.count.total, starting_height)),
supply: AddrTypeToSupply::from((&vecs.supply, starting_height)),
}
}
}
@@ -1,12 +0,0 @@
//! Exposed address supply (sats) tracking — running sum of balances held by
//! addresses currently in the funded exposed set, per address type plus an
//! aggregated `all`. See the parent [`super`] module for the definition of
//! "exposed" and how it varies by address type.
mod share;
mod state;
mod vecs;
pub use share::ExposedSupplyShareVecs;
pub use state::AddrTypeToExposedSupply;
pub use vecs::ExposedAddrSupplyVecs;
@@ -1,36 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPoints16, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Rw, StorageMode};
use crate::{
indexes,
internal::{PercentPerBlock, WithAddrTypes},
};
/// Share of exposed supply relative to total supply.
///
/// - `all`: exposed_supply / circulating_supply
/// - Per-type: type's exposed_supply / type's total supply
#[derive(Deref, DerefMut, Traversable)]
pub struct ExposedSupplyShareVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PercentPerBlock<BasisPoints16, M>>,
);
impl ExposedSupplyShareVecs {
pub(crate) fn forced_import(
db: &Database,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(
WithAddrTypes::<PercentPerBlock<BasisPoints16>>::forced_import(
db,
"exposed_supply_share",
version,
indexes,
)?,
))
}
}
@@ -1,42 +0,0 @@
use brk_cohort::ByAddrType;
use brk_types::{Height, Sats};
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use crate::internal::ValuePerBlock;
use super::vecs::ExposedAddrSupplyVecs;
/// Runtime running counter for the total balance (sats) held by funded
/// exposed addresses, per address type.
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToExposedSupply(ByAddrType<Sats>);
impl AddrTypeToExposedSupply {
#[inline]
pub(crate) fn sum(&self) -> Sats {
self.0.values().copied().sum()
}
}
impl From<(&ExposedAddrSupplyVecs, Height)> for AddrTypeToExposedSupply {
#[inline]
fn from((vecs, starting_height): (&ExposedAddrSupplyVecs, Height)) -> Self {
if let Some(prev_height) = starting_height.decremented() {
let read =
|v: &ValuePerBlock| -> Sats { v.sats.height.collect_one(prev_height).unwrap() };
Self(ByAddrType {
p2pk65: read(&vecs.by_addr_type.p2pk65),
p2pk33: read(&vecs.by_addr_type.p2pk33),
p2pkh: read(&vecs.by_addr_type.p2pkh),
p2sh: read(&vecs.by_addr_type.p2sh),
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
p2wsh: read(&vecs.by_addr_type.p2wsh),
p2tr: read(&vecs.by_addr_type.p2tr),
p2a: read(&vecs.by_addr_type.p2a),
})
} else {
Default::default()
}
}
}
@@ -1,21 +1,25 @@
mod activity;
mod addr_count;
mod count;
mod data;
mod delta;
mod exposed;
mod indexes;
mod new_addr_count;
mod reused;
mod state;
mod supply;
mod total_addr_count;
mod type_map;
pub use activity::{AddrActivityVecs, AddrTypeToActivityCounts};
pub use addr_count::{AddrCountsVecs, AddrTypeToAddrCount};
pub use count::{AddrCountsVecs, AddrTypeToAddrCount};
pub use data::AddrsDataVecs;
pub use delta::DeltaVecs;
pub use exposed::{AddrTypeToExposedAddrCount, AddrTypeToExposedSupply, ExposedAddrVecs,};
pub use exposed::{ExposedAddrState, ExposedAddrVecs};
pub use indexes::AnyAddrIndexesVecs;
pub use new_addr_count::NewAddrCountVecs;
pub use reused::{AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, ReusedAddrVecs};
pub use reused::{ReusedAddrState, ReusedAddrVecs};
pub use state::{AddrMetricsState, AddrReceivePreState, AddrSendPreState};
pub use supply::AddrTypeToSupply;
pub use total_addr_count::TotalAddrCountVecs;
pub use type_map::{AddrTypeToTypeIndexMap, AddrTypeToVec, HeightToAddrTypeToVec};
@@ -1,78 +0,0 @@
//! Reused address count tracking — running counters of how many addresses
//! are currently in (or have ever been in) the reused set, per address type
//! plus an aggregated `all`. See the parent [`super`] module for the
//! definition of "reused".
//!
//! Two counters are exposed:
//! - `funded`: addresses currently funded AND with `funded_txo_count > 1`
//! - `total`: addresses that have ever satisfied `funded_txo_count > 1` (monotonic)
mod state;
mod vecs;
pub use state::AddrTypeToReusedAddrCount;
pub use vecs::ReusedAddrCountAllVecs;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Indexes, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
use crate::indexes;
/// Reused address counts: funded (currently with balance) and total (ever reused).
#[derive(Traversable)]
pub struct ReusedAddrCountsVecs<M: StorageMode = Rw> {
pub funded: ReusedAddrCountAllVecs<M>,
pub total: ReusedAddrCountAllVecs<M>,
}
impl ReusedAddrCountsVecs {
pub(crate) fn forced_import(
db: &Database,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
funded: ReusedAddrCountAllVecs::forced_import(
db,
"reused_addr_count",
version,
indexes,
)?,
total: ReusedAddrCountAllVecs::forced_import(
db,
"total_reused_addr_count",
version,
indexes,
)?,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.funded
.min_stateful_len()
.min(self.total.min_stateful_len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.funded
.par_iter_height_mut()
.chain(self.total.par_iter_height_mut())
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.funded.reset_height()?;
self.total.reset_height()?;
Ok(())
}
pub(crate) fn compute_rest(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
self.funded.compute_rest(starting_indexes, exit)?;
self.total.compute_rest(starting_indexes, exit)?;
Ok(())
}
}
@@ -1,42 +0,0 @@
use brk_cohort::ByAddrType;
use brk_types::{Height, StoredU64};
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use crate::internal::PerBlock;
use super::vecs::ReusedAddrCountAllVecs;
/// Runtime counter for reused address counts per address type.
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToReusedAddrCount(ByAddrType<u64>);
impl AddrTypeToReusedAddrCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
}
}
impl From<(&ReusedAddrCountAllVecs, Height)> for AddrTypeToReusedAddrCount {
#[inline]
fn from((vecs, starting_height): (&ReusedAddrCountAllVecs, Height)) -> Self {
if let Some(prev_height) = starting_height.decremented() {
let read = |v: &PerBlock<StoredU64>| -> u64 {
v.height.collect_one(prev_height).unwrap().into()
};
Self(ByAddrType {
p2pk65: read(&vecs.by_addr_type.p2pk65),
p2pk33: read(&vecs.by_addr_type.p2pk33),
p2pkh: read(&vecs.by_addr_type.p2pkh),
p2sh: read(&vecs.by_addr_type.p2sh),
p2wpkh: read(&vecs.by_addr_type.p2wpkh),
p2wsh: read(&vecs.by_addr_type.p2wsh),
p2tr: read(&vecs.by_addr_type.p2tr),
p2a: read(&vecs.by_addr_type.p2a),
})
} else {
Default::default()
}
}
}
@@ -1,11 +1,11 @@
//! Per-block reused-address event tracking. Holds both the output-side
//! Per-block address-reuse 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.
//! Shared between reused (receive-based) and respent (spend-based) flavors.
//! See [`vecs::AddrEventsVecs`] for the full description of each metric.
mod state;
mod vecs;
pub use state::AddrTypeToReusedAddrEventCount;
pub use vecs::ReusedAddrEventsVecs;
pub use state::AddrTypeToAddrEventCount;
pub use vecs::AddrEventsVecs;
@@ -1,19 +1,18 @@
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`).
/// Per-block running counter of address-reuse events, per address type. Shared
/// across reused (receive-based) and respent (spend-based) flavors, and
/// across output-side ("output landed on a previously-used address") and
/// input-side ("input spent from an address in the set") event kinds.
///
/// 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>);
pub struct AddrTypeToAddrEventCount(ByAddrType<u64>);
impl AddrTypeToReusedAddrEventCount {
impl AddrTypeToAddrEventCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
@@ -14,7 +14,7 @@ use crate::{
outputs,
};
use super::state::AddrTypeToReusedAddrEventCount;
use super::state::AddrTypeToAddrEventCount;
/// Per-block reused-address event metrics. Holds three families of
/// signals: output-level (use), input-level (spend), and address-level
@@ -63,7 +63,7 @@ use super::state::AddrTypeToReusedAddrEventCount;
/// distinct-address counts would be misleading because the same
/// address can appear in multiple blocks.
#[derive(Traversable)]
pub struct ReusedAddrEventsVecs<M: StorageMode = Rw> {
pub struct AddrEventsVecs<M: StorageMode = Rw> {
pub output_to_reused_addr_count:
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
pub output_to_reused_addr_share: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
@@ -75,9 +75,10 @@ pub struct ReusedAddrEventsVecs<M: StorageMode = Rw> {
pub active_reused_addr_share: PerBlockRollingAverage<StoredF32, StoredF32, M>,
}
impl ReusedAddrEventsVecs {
impl AddrEventsVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
@@ -107,27 +108,31 @@ impl ReusedAddrEventsVecs {
})
};
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 output_to_reused_addr_count =
import_count(&format!("output_to_{name}_addr_count"))?;
let output_to_reused_addr_share =
import_percent(&format!("output_to_{name}_addr_share"))?;
let spendable_output_to_reused_addr_share = PercentCumulativeRolling::forced_import(
db,
"spendable_output_to_reused_addr_share",
&format!("spendable_output_to_{name}_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 input_from_reused_addr_count =
import_count(&format!("input_from_{name}_addr_count"))?;
let input_from_reused_addr_share =
import_percent(&format!("input_from_{name}_addr_share"))?;
let active_reused_addr_count = PerBlockRollingAverage::forced_import(
db,
"active_reused_addr_count",
&format!("active_{name}_addr_count"),
version,
indexes,
cached_starts,
)?;
let active_reused_addr_share = PerBlockRollingAverage::forced_import(
db,
"active_reused_addr_share",
&format!("active_{name}_addr_share"),
version,
indexes,
cached_starts,
@@ -175,8 +180,8 @@ impl ReusedAddrEventsVecs {
#[inline(always)]
pub(crate) fn push_height(
&mut self,
uses: &AddrTypeToReusedAddrEventCount,
spends: &AddrTypeToReusedAddrEventCount,
uses: &AddrTypeToAddrEventCount,
spends: &AddrTypeToAddrEventCount,
active_addr_count: u32,
active_reused_addr_count: u32,
) {
@@ -16,42 +16,56 @@
//! paired with a percent over the matching block-level output/input
//! total.
mod count;
mod events;
pub use count::{AddrTypeToReusedAddrCount, ReusedAddrCountsVecs};
pub use events::{AddrTypeToReusedAddrEventCount, ReusedAddrEventsVecs};
pub use events::{AddrEventsVecs, AddrTypeToAddrEventCount};
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Indexes, Version};
use brk_types::{Height, Indexes, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use super::{
count::AddrCountFundedTotalVecs,
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
};
use crate::{
indexes, inputs,
internal::{WindowStartVec, Windows},
outputs,
outputs, prices,
};
mod state;
pub use state::ReusedAddrState;
/// Top-level container for all reused address tracking: counts (funded +
/// total) plus per-block reuse events (output-side + input-side).
/// total), per-block reuse events (output-side + input-side), and funded
/// supply + share.
#[derive(Traversable)]
pub struct ReusedAddrVecs<M: StorageMode = Rw> {
pub count: ReusedAddrCountsVecs<M>,
pub events: ReusedAddrEventsVecs<M>,
pub count: AddrCountFundedTotalVecs<M>,
pub events: AddrEventsVecs<M>,
pub supply: AddrSupplyVecs<M>,
#[traversable(wrap = "supply", rename = "share")]
pub supply_share: AddrSupplyShareVecs<M>,
}
impl ReusedAddrVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self {
count: ReusedAddrCountsVecs::forced_import(db, version, indexes)?,
events: ReusedAddrEventsVecs::forced_import(db, version, indexes, cached_starts)?,
count: AddrCountFundedTotalVecs::forced_import(db, name, version, indexes)?,
events: AddrEventsVecs::forced_import(db, name, version, indexes, cached_starts)?,
supply: AddrSupplyVecs::forced_import(db, name, version, indexes)?,
supply_share: AddrSupplyShareVecs::forced_import(db, name, version, indexes)?,
})
}
@@ -59,6 +73,7 @@ impl ReusedAddrVecs {
self.count
.min_stateful_len()
.min(self.events.min_stateful_len())
.min(self.supply.min_stateful_len())
}
pub(crate) fn par_iter_height_mut(
@@ -67,26 +82,50 @@ impl ReusedAddrVecs {
self.count
.par_iter_height_mut()
.chain(self.events.par_iter_height_mut())
.chain(self.supply.par_iter_height_mut())
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.count.reset_height()?;
self.events.reset_height()?;
self.supply.reset_height()?;
self.supply_share.reset_height()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(&mut self, state: &ReusedAddrState, active_addr_count: u32) {
self.count.push_counts(&state.funded, &state.total);
self.supply.push_supply(&state.supply);
self.events.push_height(
&state.output_events,
&state.input_events,
active_addr_count,
u32::try_from(state.active.sum()).unwrap(),
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest(
&mut self,
starting_indexes: &Indexes,
outputs_by_type: &outputs::ByTypeVecs,
inputs_by_type: &inputs::ByTypeVecs,
prices: &prices::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
self.count.compute_rest(starting_indexes, exit)?;
self.events.compute_rest(
starting_indexes,
outputs_by_type,
inputs_by_type,
self.events
.compute_rest(starting_indexes, outputs_by_type, inputs_by_type, exit)?;
self.supply
.compute_rest(starting_indexes.height, prices, exit)?;
self.supply_share.compute_rest(
starting_indexes.height,
&self.supply,
all_supply_sats,
type_supply_sats,
exit,
)?;
Ok(())
@@ -0,0 +1,201 @@
use brk_types::{FundedAddrData, Height, OutputType};
use crate::distribution::addr::{
AddrReceivePreState, AddrSendPreState, AddrTypeToAddrCount, AddrTypeToSupply,
};
use super::{AddrTypeToAddrEventCount, ReusedAddrVecs};
/// Runtime state for receive-based (reused) or spend-based (respent)
/// address tracking. Mirrors the persistent fields of [`ReusedAddrVecs`]
/// (funded + total counts, funded supply) plus per-block event counters
/// that reset every block.
///
/// `output_events`, `input_events`, and `active` are cleared via
/// [`Self::reset_per_block`] at the start of each block. The three running
/// totals (`funded`, `total`, `supply`) are recovered from disk at the start
/// of a run via [`From<(&ReusedAddrVecs, Height)>`].
#[derive(Debug, Default)]
pub struct ReusedAddrState {
pub funded: AddrTypeToAddrCount,
pub total: AddrTypeToAddrCount,
pub supply: AddrTypeToSupply,
pub output_events: AddrTypeToAddrEventCount,
pub input_events: AddrTypeToAddrEventCount,
pub active: AddrTypeToAddrEventCount,
}
impl ReusedAddrState {
#[inline]
pub(crate) fn reset_per_block(&mut self) {
self.output_events.reset();
self.input_events.reset();
self.active.reset();
}
/// Apply reused-flavor (receive-based: `funded_txo_count > 1`) updates
/// for a received output, AFTER the receive has mutated `addr_data`.
#[inline]
pub(crate) fn on_receive_as_reused(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrReceivePreState,
output_count: u32,
) {
let is_now_reused = addr_data.is_reused();
// Threshold crossing: the 2nd lifetime receive lands here. The address
// is always funded post-receive.
if is_now_reused && !pre.was_reused {
*self.total.get_mut_unwrap(output_type) += 1;
*self.funded.get_mut_unwrap(output_type) += 1;
} else if pre.was_reused && !pre.was_funded {
// Reactivation: already-reused address was empty, now funded.
*self.funded.get_mut_unwrap(output_type) += 1;
}
// output-to-reused events: outputs landing on addresses that had
// already received >= 1 prior output, i.e. every output except the
// very first one the address ever sees. With `before =
// prev_funded_txo_count` and `N = output_count`: events = N - max(0, 1 - before).
let skip_first = 1u32.saturating_sub(pre.prev_funded_txo_count.min(1));
let reused_events = output_count.saturating_sub(skip_first);
if reused_events > 0 {
*self.output_events.get_mut_unwrap(output_type) += u64::from(reused_events);
}
if is_now_reused {
*self.active.get_mut_unwrap(output_type) += 1;
}
let after = addr_data.reused_supply_contribution();
self.supply
.apply_delta(output_type, pre.reused_contribution, after);
}
/// Apply respent-flavor (spend-based: `spent_txo_count > 1`) updates for a
/// received output, AFTER the receive has mutated `addr_data`. Receives
/// don't cross the respent threshold. The only transition is an
/// already-respent empty address reactivating into the funded set.
#[inline]
pub(crate) fn on_receive_as_respent(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrReceivePreState,
output_count: u32,
) {
if pre.was_respent && !pre.was_funded {
*self.funded.get_mut_unwrap(output_type) += 1;
}
// Respent status is stable across a receive, so every output lands on
// a respent address iff the address was already respent.
if pre.was_respent {
*self.output_events.get_mut_unwrap(output_type) += u64::from(output_count);
*self.active.get_mut_unwrap(output_type) += 1;
}
let after = addr_data.respent_supply_contribution();
self.supply
.apply_delta(output_type, pre.respent_contribution, after);
}
/// Apply reused-flavor updates for a spent UTXO, AFTER the send has
/// mutated `addr_data`. Sends don't change the reused predicate, so
/// `pre.was_reused == is_reused` post-spend.
#[inline]
pub(crate) fn on_send_as_reused(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrSendPreState,
is_first_encounter: bool,
also_received: bool,
will_be_empty: bool,
) {
if pre.was_reused {
*self.input_events.get_mut_unwrap(output_type) += 1;
}
// Active reused: first-encounter sender, currently reused, and not
// already counted on the receive side.
if is_first_encounter && pre.was_reused && !also_received {
*self.active.get_mut_unwrap(output_type) += 1;
}
if will_be_empty && pre.was_reused {
*self.funded.get_mut_unwrap(output_type) -= 1;
}
let after = addr_data.reused_supply_contribution();
self.supply
.apply_delta(output_type, pre.reused_contribution, after);
}
/// Apply respent-flavor updates for a spent UTXO, AFTER the send has
/// mutated `addr_data`. Sends CAN cross the respent threshold on the
/// 2nd lifetime spend.
#[inline]
pub(crate) fn on_send_as_respent(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrSendPreState,
is_first_encounter: bool,
also_received: bool,
will_be_empty: bool,
) {
if pre.was_respent {
*self.input_events.get_mut_unwrap(output_type) += 1;
}
let is_now_respent = addr_data.is_respent();
// Threshold crossing: the 2nd spend ever on this address. Always
// bumps the monotonic total. Bumps the funded count iff the address
// still has a balance. If the crossing spend also empties the
// address, the `will_be_empty` branch below doesn't decrement
// (was_respent is false), so the funded count stays correct.
if is_now_respent && !pre.was_respent {
*self.total.get_mut_unwrap(output_type) += 1;
if !will_be_empty {
*self.funded.get_mut_unwrap(output_type) += 1;
}
}
// Active respent splits cleanly into two disjoint branches (gated on
// `pre.was_respent`):
// - was already respent + active this block, and not also counted
// on the receive side: pure senders on first spend.
// - crosses the respent threshold this block: fires once per
// address ever, on the exact crossing spend.
if (is_first_encounter && pre.was_respent && !also_received)
|| (is_now_respent && !pre.was_respent)
{
*self.active.get_mut_unwrap(output_type) += 1;
}
// Leaving the funded respent set on empty uses pre-spend state: a
// threshold-crossing spend that also empties the address never
// entered the funded set above (gated on !will_be_empty), so we
// don't double-decrement.
if will_be_empty && pre.was_respent {
*self.funded.get_mut_unwrap(output_type) -= 1;
}
let after = addr_data.respent_supply_contribution();
self.supply
.apply_delta(output_type, pre.respent_contribution, after);
}
}
impl From<(&ReusedAddrVecs, Height)> for ReusedAddrState {
#[inline]
fn from((vecs, starting_height): (&ReusedAddrVecs, Height)) -> Self {
Self {
funded: AddrTypeToAddrCount::from((&vecs.count.funded, starting_height)),
total: AddrTypeToAddrCount::from((&vecs.count.total, starting_height)),
supply: AddrTypeToSupply::from((&vecs.supply, starting_height)),
output_events: AddrTypeToAddrEventCount::default(),
input_events: AddrTypeToAddrEventCount::default(),
active: AddrTypeToAddrEventCount::default(),
}
}
}
@@ -0,0 +1,181 @@
use brk_types::{FundedAddrData, Height, OutputType, Sats};
use crate::distribution::{block::TrackingStatus, vecs::AddrMetricsVecs};
use super::{
AddrTypeToActivityCounts, AddrTypeToAddrCount, ExposedAddrState, ReusedAddrState,
};
/// Bundle of per-block runtime state for the full address-metrics pipeline.
/// Feeds `process_received` / `process_sent` and is pushed to [`AddrMetricsVecs`]
/// once per block.
///
/// Recovery: [`From<(&AddrMetricsVecs, Height)>`] reads the prior block from
/// disk to seed all persistent running totals. Per-block counters (activity,
/// and event counts inside each [`ReusedAddrState`]) default to zero and are
/// cleared at the top of each block via [`Self::reset_per_block`].
#[derive(Debug, Default)]
pub struct AddrMetricsState {
pub funded: AddrTypeToAddrCount,
pub empty: AddrTypeToAddrCount,
pub activity: AddrTypeToActivityCounts,
pub reused: ReusedAddrState,
pub respent: ReusedAddrState,
pub exposed: ExposedAddrState,
}
/// Snapshot of [`FundedAddrData`] taken BEFORE a receive mutates it.
/// Feeds delta-based updates in [`AddrMetricsState::on_receive_applied`].
#[derive(Debug)]
pub struct AddrReceivePreState {
pub was_funded: bool,
pub was_reused: bool,
pub was_respent: bool,
pub was_pubkey_exposed: bool,
pub prev_funded_txo_count: u32,
pub exposed_contribution: Sats,
pub reused_contribution: Sats,
pub respent_contribution: Sats,
}
impl AddrReceivePreState {
#[inline]
pub fn capture(addr_data: &FundedAddrData, output_type: OutputType) -> Self {
Self {
was_funded: addr_data.is_funded(),
was_reused: addr_data.is_reused(),
was_respent: addr_data.is_respent(),
was_pubkey_exposed: addr_data.is_pubkey_exposed(output_type),
prev_funded_txo_count: addr_data.funded_txo_count,
exposed_contribution: addr_data.exposed_supply_contribution(output_type),
reused_contribution: addr_data.reused_supply_contribution(),
respent_contribution: addr_data.respent_supply_contribution(),
}
}
}
/// Snapshot of [`FundedAddrData`] taken BEFORE a spend mutates it.
/// Feeds delta-based updates in [`AddrMetricsState::on_send_applied`].
#[derive(Debug)]
pub struct AddrSendPreState {
pub was_reused: bool,
pub was_respent: bool,
pub was_pubkey_exposed: bool,
pub exposed_contribution: Sats,
pub reused_contribution: Sats,
pub respent_contribution: Sats,
}
impl AddrSendPreState {
#[inline]
pub fn capture(addr_data: &FundedAddrData, output_type: OutputType) -> Self {
Self {
was_reused: addr_data.is_reused(),
was_respent: addr_data.is_respent(),
was_pubkey_exposed: addr_data.is_pubkey_exposed(output_type),
exposed_contribution: addr_data.exposed_supply_contribution(output_type),
reused_contribution: addr_data.reused_supply_contribution(),
respent_contribution: addr_data.respent_supply_contribution(),
}
}
}
impl AddrMetricsState {
#[inline]
pub(crate) fn reset_per_block(&mut self) {
self.activity.reset();
self.reused.reset_per_block();
self.respent.reset_per_block();
}
/// Apply all state updates for a received output, AFTER the cohort and
/// `addr_data` have been mutated. `pre` is the snapshot captured before
/// the mutation, `addr_data` is the post-receive view.
#[inline]
pub(crate) fn on_receive_applied(
&mut self,
output_type: OutputType,
status: TrackingStatus,
addr_data: &FundedAddrData,
pre: &AddrReceivePreState,
output_count: u32,
) {
let activity = self.activity.get_mut_unwrap(output_type);
activity.receiving += 1;
match status {
TrackingStatus::New => {
*self.funded.get_mut_unwrap(output_type) += 1;
}
TrackingStatus::WasEmpty => {
activity.reactivated += 1;
*self.funded.get_mut_unwrap(output_type) += 1;
*self.empty.get_mut_unwrap(output_type) -= 1;
}
TrackingStatus::Tracked => {}
}
self.reused
.on_receive_as_reused(output_type, addr_data, pre, output_count);
self.respent
.on_receive_as_respent(output_type, addr_data, pre, output_count);
self.exposed.on_receive(output_type, addr_data, pre, status);
}
/// Apply all state updates for a spent UTXO, AFTER the cohort and
/// `addr_data` have been mutated. `pre` is the snapshot captured before
/// the mutation. `is_first_encounter` / `also_received` come from the
/// caller's per-block seen/received tracking. `will_be_empty` is from
/// the pre-mutation `addr_data.has_1_utxos()`.
#[inline]
pub(crate) fn on_send_applied(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrSendPreState,
is_first_encounter: bool,
also_received: bool,
will_be_empty: bool,
) {
if is_first_encounter {
let activity = self.activity.get_mut_unwrap(output_type);
activity.sending += 1;
if also_received {
activity.bidirectional += 1;
}
}
if will_be_empty {
*self.funded.get_mut_unwrap(output_type) -= 1;
*self.empty.get_mut_unwrap(output_type) += 1;
}
self.reused.on_send_as_reused(
output_type,
addr_data,
pre,
is_first_encounter,
also_received,
will_be_empty,
);
self.respent.on_send_as_respent(
output_type,
addr_data,
pre,
is_first_encounter,
also_received,
will_be_empty,
);
self.exposed.on_send(output_type, addr_data, pre, will_be_empty);
}
}
impl From<(&AddrMetricsVecs, Height)> for AddrMetricsState {
#[inline]
fn from((vecs, starting_height): (&AddrMetricsVecs, Height)) -> Self {
Self {
funded: AddrTypeToAddrCount::from((&vecs.funded, starting_height)),
empty: AddrTypeToAddrCount::from((&vecs.empty, starting_height)),
activity: AddrTypeToActivityCounts::default(),
reused: ReusedAddrState::from((&vecs.reused, starting_height)),
respent: ReusedAddrState::from((&vecs.respent, starting_height)),
exposed: ExposedAddrState::from((&vecs.exposed, starting_height)),
}
}
}
@@ -0,0 +1,12 @@
//! Generic per-address-type supply tracking, shared across predicate-based
//! supply categories (exposed, reused, respent). A "category supply" is the
//! running sum of balances held by addresses currently in the funded subset
//! defined by some predicate.
mod share;
mod state;
mod vecs;
pub use share::AddrSupplyShareVecs;
pub use state::AddrTypeToSupply;
pub use vecs::AddrSupplyVecs;
@@ -0,0 +1,69 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPoints16, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{PercentPerBlock, RatioSatsBp16, WithAddrTypes},
};
use super::vecs::AddrSupplyVecs;
/// Share of a predicate-based supply category relative to total supply.
///
/// - `all`: category supply / circulating supply
/// - Per-type: type's category supply / type's total supply
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrSupplyShareVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PercentPerBlock<BasisPoints16, M>>,
);
impl AddrSupplyShareVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(
WithAddrTypes::<PercentPerBlock<BasisPoints16>>::forced_import(
db,
&format!("{name}_addr_supply_share"),
version,
indexes,
)?,
))
}
pub(crate) fn compute_rest(
&mut self,
max_from: Height,
supply: &AddrSupplyVecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
self.all.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&supply.all.sats.height,
all_supply_sats,
exit,
)?;
for ((_, share), ((_, cat), (_, denom))) in self
.by_addr_type
.iter_mut()
.zip(supply.by_addr_type.iter().zip(type_supply_sats.iter()))
{
share.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&cat.sats.height,
*denom,
exit,
)?;
}
Ok(())
}
}
@@ -0,0 +1,49 @@
use brk_cohort::ByAddrType;
use brk_types::{Height, OutputType, Sats};
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use super::vecs::AddrSupplyVecs;
/// Per-addr-type running-total of a supply category (sats). Shared across
/// predicate-based supply categories (exposed, reused, respent).
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToSupply(ByAddrType<Sats>);
impl AddrTypeToSupply {
#[inline]
pub(crate) fn sum(&self) -> Sats {
self.0.values().copied().sum()
}
/// Apply a signed `after - before` delta to the slot for `output_type`.
/// Sats is unsigned, so branch on sign.
#[inline]
pub(crate) fn apply_delta(&mut self, output_type: OutputType, before: Sats, after: Sats) {
let slot = self.get_mut_unwrap(output_type);
if after >= before {
*slot += after - before;
} else {
*slot -= before - after;
}
}
}
impl From<ByAddrType<Sats>> for AddrTypeToSupply {
#[inline]
fn from(value: ByAddrType<Sats>) -> Self {
Self(value)
}
}
impl From<(&AddrSupplyVecs, Height)> for AddrTypeToSupply {
#[inline]
fn from((vecs, starting_height): (&AddrSupplyVecs, Height)) -> Self {
let Some(prev_height) = starting_height.decremented() else {
return Self::default();
};
vecs.by_addr_type
.map_with_name(|_, v| v.sats.height.collect_one(prev_height).unwrap())
.into()
}
}
@@ -9,26 +9,34 @@ use crate::{
internal::{ValuePerBlock, WithAddrTypes},
};
/// Exposed address supply (sats/btc/cents/usd) — `all` + per-address-type.
/// Tracks the total balance held by addresses currently in the funded
/// exposed set. Sats are pushed stateful per block; cents/usd are derived
/// post-hoc from sats × spot price.
use super::AddrTypeToSupply;
/// Per-addr-type running supply (sats/btc/cents/usd) with an aggregated `all`.
/// Shared across predicate-based supply categories (exposed, reused, respent).
/// Sats are pushed stateful per block; cents/usd are derived post-hoc from
/// sats × spot price.
#[derive(Deref, DerefMut, Traversable)]
pub struct ExposedAddrSupplyVecs<M: StorageMode = Rw>(
pub struct AddrSupplyVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<ValuePerBlock<M>>,
);
impl ExposedAddrSupplyVecs {
impl AddrSupplyVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(WithAddrTypes::<ValuePerBlock>::forced_import(
db,
"exposed_supply",
&format!("{name}_addr_supply"),
version,
indexes,
)?))
}
#[inline(always)]
pub(crate) fn push_supply(&mut self, supply: &AddrTypeToSupply) {
self.push_height(supply.sum(), supply.values().copied());
}
}
@@ -39,14 +39,14 @@ impl TotalAddrCountVecs {
empty_addr_count: &AddrCountsVecs,
exit: &Exit,
) -> Result<()> {
self.0.all.height.compute_add(
self.all.height.compute_add(
max_from,
&addr_count.all.height,
&empty_addr_count.all.height,
exit,
)?;
for ((_, total), ((_, addr), (_, empty))) in self.0.by_addr_type.iter_mut().zip(
for ((_, total), ((_, addr), (_, empty))) in self.by_addr_type.iter_mut().zip(
addr_count
.by_addr_type
.iter()