diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index ee7411443..af3fc3790 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -1277,6 +1277,20 @@ impl AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90S } } +/// Pattern struct for repeated tree structure. +pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern { + pub all: BtcCentsSatsUsdPattern, + pub p2a: BtcCentsSatsUsdPattern, + pub p2pk33: BtcCentsSatsUsdPattern, + pub p2pk65: BtcCentsSatsUsdPattern, + pub p2pkh: BtcCentsSatsUsdPattern, + pub p2sh: BtcCentsSatsUsdPattern, + pub p2tr: BtcCentsSatsUsdPattern, + pub p2wpkh: BtcCentsSatsUsdPattern, + pub p2wsh: BtcCentsSatsUsdPattern, + pub share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5, +} + /// Pattern struct for repeated tree structure. pub struct IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern { pub index: SeriesPattern1, @@ -1339,6 +1353,36 @@ impl AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 { } } +/// Pattern struct for repeated tree structure. +pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 { + pub all: BpsPercentRatioPattern2, + pub p2a: BpsPercentRatioPattern2, + pub p2pk33: BpsPercentRatioPattern2, + pub p2pk65: BpsPercentRatioPattern2, + pub p2pkh: BpsPercentRatioPattern2, + pub p2sh: BpsPercentRatioPattern2, + pub p2tr: BpsPercentRatioPattern2, + pub p2wpkh: BpsPercentRatioPattern2, + pub p2wsh: BpsPercentRatioPattern2, +} + +impl AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + all: BpsPercentRatioPattern2::new(client.clone(), acc.clone()), + p2a: BpsPercentRatioPattern2::new(client.clone(), _p("p2a", &acc)), + p2pk33: BpsPercentRatioPattern2::new(client.clone(), _p("p2pk33", &acc)), + p2pk65: BpsPercentRatioPattern2::new(client.clone(), _p("p2pk65", &acc)), + p2pkh: BpsPercentRatioPattern2::new(client.clone(), _p("p2pkh", &acc)), + p2sh: BpsPercentRatioPattern2::new(client.clone(), _p("p2sh", &acc)), + p2tr: BpsPercentRatioPattern2::new(client.clone(), _p("p2tr", &acc)), + p2wpkh: BpsPercentRatioPattern2::new(client.clone(), _p("p2wpkh", &acc)), + p2wsh: BpsPercentRatioPattern2::new(client.clone(), _p("p2wsh", &acc)), + } + } +} + /// Pattern struct for repeated tree structure. pub struct AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4 { pub all: SeriesPattern1, @@ -1551,6 +1595,17 @@ impl _1m1w1y24hBpsPercentRatioPattern { } } +/// Pattern struct for repeated tree structure. +pub struct ActiveInputOutputSpendablePattern { + pub active_reused_addr_count: _1m1w1y24hBlockPattern, + pub active_reused_addr_share: _1m1w1y24hBlockPattern2, + pub input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6, + pub input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7, + pub output_to_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6, + pub output_to_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7, + pub spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern, +} + /// Pattern struct for repeated tree structure. pub struct CapLossMvrvNetPriceProfitSoprPattern { pub cap: CentsDeltaUsdPattern, @@ -1823,6 +1878,28 @@ impl DeltaDominanceHalfInTotalPattern { } } +/// Pattern struct for repeated tree structure. +pub struct _1m1w1y24hBlockPattern2 { + pub _1m: SeriesPattern1, + pub _1w: SeriesPattern1, + pub _1y: SeriesPattern1, + pub _24h: SeriesPattern1, + pub block: SeriesPattern18, +} + +impl _1m1w1y24hBlockPattern2 { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + _1m: SeriesPattern1::new(client.clone(), _m(&acc, "average_1m")), + _1w: SeriesPattern1::new(client.clone(), _m(&acc, "average_1w")), + _1y: SeriesPattern1::new(client.clone(), _m(&acc, "average_1y")), + _24h: SeriesPattern1::new(client.clone(), _m(&acc, "average_24h")), + block: SeriesPattern18::new(client.clone(), acc.clone()), + } + } +} + /// Pattern struct for repeated tree structure. pub struct _1m1w1y24hBlockPattern { pub _1m: SeriesPattern1, @@ -2760,6 +2837,13 @@ impl CentsSatsUsdPattern { } } +/// Pattern struct for repeated tree structure. +pub struct CountEventsSupplyPattern { + pub count: FundedTotalPattern, + pub events: ActiveInputOutputSpendablePattern, + pub supply: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern, +} + /// Pattern struct for repeated tree structure. pub struct CumulativeRollingSumPattern { pub cumulative: SeriesPattern1, @@ -4235,6 +4319,7 @@ pub struct SeriesTree_Addrs { pub total: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4, pub new: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6, pub reused: SeriesTree_Addrs_Reused, + pub respent: SeriesTree_Addrs_Respent, pub exposed: SeriesTree_Addrs_Exposed, pub delta: SeriesTree_Addrs_Delta, pub avg_amount: SeriesTree_Addrs_AvgAmount, @@ -4252,6 +4337,7 @@ impl SeriesTree_Addrs { total: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4::new(client.clone(), "total_addr_count".to_string()), new: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6::new(client.clone(), "new_addr_count".to_string()), reused: SeriesTree_Addrs_Reused::new(client.clone(), format!("{base_path}_reused")), + respent: SeriesTree_Addrs_Respent::new(client.clone(), format!("{base_path}_respent")), exposed: SeriesTree_Addrs_Exposed::new(client.clone(), format!("{base_path}_exposed")), delta: SeriesTree_Addrs_Delta::new(client.clone(), format!("{base_path}_delta")), avg_amount: SeriesTree_Addrs_AvgAmount::new(client.clone(), format!("{base_path}_avg_amount")), @@ -4506,6 +4592,7 @@ impl SeriesTree_Addrs_Activity_All { pub struct SeriesTree_Addrs_Reused { pub count: FundedTotalPattern, pub events: SeriesTree_Addrs_Reused_Events, + pub supply: SeriesTree_Addrs_Reused_Supply, } impl SeriesTree_Addrs_Reused { @@ -4513,6 +4600,7 @@ impl SeriesTree_Addrs_Reused { Self { count: FundedTotalPattern::new(client.clone(), "reused_addr_count".to_string()), events: SeriesTree_Addrs_Reused_Events::new(client.clone(), format!("{base_path}_events")), + supply: SeriesTree_Addrs_Reused_Supply::new(client.clone(), format!("{base_path}_supply")), } } } @@ -4525,7 +4613,7 @@ pub struct SeriesTree_Addrs_Reused_Events { pub input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6, pub input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7, pub active_reused_addr_count: _1m1w1y24hBlockPattern, - pub active_reused_addr_share: SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare, + pub active_reused_addr_share: _1m1w1y24hBlockPattern2, } impl SeriesTree_Addrs_Reused_Events { @@ -4537,28 +4625,111 @@ impl SeriesTree_Addrs_Reused_Events { input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6::new(client.clone(), "input_from_reused_addr_count".to_string()), input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7::new(client.clone(), "input_from_reused_addr_share".to_string()), active_reused_addr_count: _1m1w1y24hBlockPattern::new(client.clone(), "active_reused_addr_count".to_string()), - active_reused_addr_share: SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare::new(client.clone(), format!("{base_path}_active_reused_addr_share")), + active_reused_addr_share: _1m1w1y24hBlockPattern2::new(client.clone(), "active_reused_addr_share".to_string()), } } } /// Series tree node. -pub struct SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare { - pub block: SeriesPattern18, - pub _24h: SeriesPattern1, - pub _1w: SeriesPattern1, - pub _1m: SeriesPattern1, - pub _1y: SeriesPattern1, +pub struct SeriesTree_Addrs_Reused_Supply { + pub all: BtcCentsSatsUsdPattern, + pub p2pk65: BtcCentsSatsUsdPattern, + pub p2pk33: BtcCentsSatsUsdPattern, + pub p2pkh: BtcCentsSatsUsdPattern, + pub p2sh: BtcCentsSatsUsdPattern, + pub p2wpkh: BtcCentsSatsUsdPattern, + pub p2wsh: BtcCentsSatsUsdPattern, + pub p2tr: BtcCentsSatsUsdPattern, + pub p2a: BtcCentsSatsUsdPattern, + pub share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5, } -impl SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare { +impl SeriesTree_Addrs_Reused_Supply { pub fn new(client: Arc, base_path: String) -> Self { Self { - block: SeriesPattern18::new(client.clone(), "active_reused_addr_share".to_string()), - _24h: SeriesPattern1::new(client.clone(), "active_reused_addr_share_average_24h".to_string()), - _1w: SeriesPattern1::new(client.clone(), "active_reused_addr_share_average_1w".to_string()), - _1m: SeriesPattern1::new(client.clone(), "active_reused_addr_share_average_1m".to_string()), - _1y: SeriesPattern1::new(client.clone(), "active_reused_addr_share_average_1y".to_string()), + all: BtcCentsSatsUsdPattern::new(client.clone(), "reused_addr_supply".to_string()), + p2pk65: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk65_reused_addr_supply".to_string()), + p2pk33: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk33_reused_addr_supply".to_string()), + p2pkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2pkh_reused_addr_supply".to_string()), + p2sh: BtcCentsSatsUsdPattern::new(client.clone(), "p2sh_reused_addr_supply".to_string()), + p2wpkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wpkh_reused_addr_supply".to_string()), + p2wsh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wsh_reused_addr_supply".to_string()), + p2tr: BtcCentsSatsUsdPattern::new(client.clone(), "p2tr_reused_addr_supply".to_string()), + p2a: BtcCentsSatsUsdPattern::new(client.clone(), "p2a_reused_addr_supply".to_string()), + share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5::new(client.clone(), "reused_addr_supply_share".to_string()), + } + } +} + +/// Series tree node. +pub struct SeriesTree_Addrs_Respent { + pub count: FundedTotalPattern, + pub events: SeriesTree_Addrs_Respent_Events, + pub supply: SeriesTree_Addrs_Respent_Supply, +} + +impl SeriesTree_Addrs_Respent { + pub fn new(client: Arc, base_path: String) -> Self { + Self { + count: FundedTotalPattern::new(client.clone(), "respent_addr_count".to_string()), + events: SeriesTree_Addrs_Respent_Events::new(client.clone(), format!("{base_path}_events")), + supply: SeriesTree_Addrs_Respent_Supply::new(client.clone(), format!("{base_path}_supply")), + } + } +} + +/// Series tree node. +pub struct SeriesTree_Addrs_Respent_Events { + pub output_to_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6, + pub output_to_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7, + pub spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern, + pub input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6, + pub input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7, + pub active_reused_addr_count: _1m1w1y24hBlockPattern, + pub active_reused_addr_share: _1m1w1y24hBlockPattern2, +} + +impl SeriesTree_Addrs_Respent_Events { + pub fn new(client: Arc, base_path: String) -> Self { + Self { + output_to_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6::new(client.clone(), "output_to_respent_addr_count".to_string()), + output_to_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7::new(client.clone(), "output_to_respent_addr_share".to_string()), + spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern::new(client.clone(), "spendable_output_to_respent_addr_share".to_string()), + input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6::new(client.clone(), "input_from_respent_addr_count".to_string()), + input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7::new(client.clone(), "input_from_respent_addr_share".to_string()), + active_reused_addr_count: _1m1w1y24hBlockPattern::new(client.clone(), "active_respent_addr_count".to_string()), + active_reused_addr_share: _1m1w1y24hBlockPattern2::new(client.clone(), "active_respent_addr_share".to_string()), + } + } +} + +/// Series tree node. +pub struct SeriesTree_Addrs_Respent_Supply { + pub all: BtcCentsSatsUsdPattern, + pub p2pk65: BtcCentsSatsUsdPattern, + pub p2pk33: BtcCentsSatsUsdPattern, + pub p2pkh: BtcCentsSatsUsdPattern, + pub p2sh: BtcCentsSatsUsdPattern, + pub p2wpkh: BtcCentsSatsUsdPattern, + pub p2wsh: BtcCentsSatsUsdPattern, + pub p2tr: BtcCentsSatsUsdPattern, + pub p2a: BtcCentsSatsUsdPattern, + pub share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5, +} + +impl SeriesTree_Addrs_Respent_Supply { + pub fn new(client: Arc, base_path: String) -> Self { + Self { + all: BtcCentsSatsUsdPattern::new(client.clone(), "respent_addr_supply".to_string()), + p2pk65: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk65_respent_addr_supply".to_string()), + p2pk33: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk33_respent_addr_supply".to_string()), + p2pkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2pkh_respent_addr_supply".to_string()), + p2sh: BtcCentsSatsUsdPattern::new(client.clone(), "p2sh_respent_addr_supply".to_string()), + p2wpkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wpkh_respent_addr_supply".to_string()), + p2wsh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wsh_respent_addr_supply".to_string()), + p2tr: BtcCentsSatsUsdPattern::new(client.clone(), "p2tr_respent_addr_supply".to_string()), + p2a: BtcCentsSatsUsdPattern::new(client.clone(), "p2a_respent_addr_supply".to_string()), + share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5::new(client.clone(), "respent_addr_supply_share".to_string()), } } } @@ -4589,51 +4760,22 @@ pub struct SeriesTree_Addrs_Exposed_Supply { pub p2wsh: BtcCentsSatsUsdPattern, pub p2tr: BtcCentsSatsUsdPattern, pub p2a: BtcCentsSatsUsdPattern, - pub share: SeriesTree_Addrs_Exposed_Supply_Share, + pub share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5, } impl SeriesTree_Addrs_Exposed_Supply { pub fn new(client: Arc, base_path: String) -> Self { Self { - all: BtcCentsSatsUsdPattern::new(client.clone(), "exposed_supply".to_string()), - p2pk65: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk65_exposed_supply".to_string()), - p2pk33: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk33_exposed_supply".to_string()), - p2pkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2pkh_exposed_supply".to_string()), - p2sh: BtcCentsSatsUsdPattern::new(client.clone(), "p2sh_exposed_supply".to_string()), - p2wpkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wpkh_exposed_supply".to_string()), - p2wsh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wsh_exposed_supply".to_string()), - p2tr: BtcCentsSatsUsdPattern::new(client.clone(), "p2tr_exposed_supply".to_string()), - p2a: BtcCentsSatsUsdPattern::new(client.clone(), "p2a_exposed_supply".to_string()), - share: SeriesTree_Addrs_Exposed_Supply_Share::new(client.clone(), format!("{base_path}_share")), - } - } -} - -/// Series tree node. -pub struct SeriesTree_Addrs_Exposed_Supply_Share { - pub all: BpsPercentRatioPattern2, - pub p2pk65: BpsPercentRatioPattern2, - pub p2pk33: BpsPercentRatioPattern2, - pub p2pkh: BpsPercentRatioPattern2, - pub p2sh: BpsPercentRatioPattern2, - pub p2wpkh: BpsPercentRatioPattern2, - pub p2wsh: BpsPercentRatioPattern2, - pub p2tr: BpsPercentRatioPattern2, - pub p2a: BpsPercentRatioPattern2, -} - -impl SeriesTree_Addrs_Exposed_Supply_Share { - pub fn new(client: Arc, base_path: String) -> Self { - Self { - all: BpsPercentRatioPattern2::new(client.clone(), "exposed_supply_share".to_string()), - p2pk65: BpsPercentRatioPattern2::new(client.clone(), "p2pk65_exposed_supply_share".to_string()), - p2pk33: BpsPercentRatioPattern2::new(client.clone(), "p2pk33_exposed_supply_share".to_string()), - p2pkh: BpsPercentRatioPattern2::new(client.clone(), "p2pkh_exposed_supply_share".to_string()), - p2sh: BpsPercentRatioPattern2::new(client.clone(), "p2sh_exposed_supply_share".to_string()), - p2wpkh: BpsPercentRatioPattern2::new(client.clone(), "p2wpkh_exposed_supply_share".to_string()), - p2wsh: BpsPercentRatioPattern2::new(client.clone(), "p2wsh_exposed_supply_share".to_string()), - p2tr: BpsPercentRatioPattern2::new(client.clone(), "p2tr_exposed_supply_share".to_string()), - p2a: BpsPercentRatioPattern2::new(client.clone(), "p2a_exposed_supply_share".to_string()), + all: BtcCentsSatsUsdPattern::new(client.clone(), "exposed_addr_supply".to_string()), + p2pk65: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk65_exposed_addr_supply".to_string()), + p2pk33: BtcCentsSatsUsdPattern::new(client.clone(), "p2pk33_exposed_addr_supply".to_string()), + p2pkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2pkh_exposed_addr_supply".to_string()), + p2sh: BtcCentsSatsUsdPattern::new(client.clone(), "p2sh_exposed_addr_supply".to_string()), + p2wpkh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wpkh_exposed_addr_supply".to_string()), + p2wsh: BtcCentsSatsUsdPattern::new(client.clone(), "p2wsh_exposed_addr_supply".to_string()), + p2tr: BtcCentsSatsUsdPattern::new(client.clone(), "p2tr_exposed_addr_supply".to_string()), + p2a: BtcCentsSatsUsdPattern::new(client.clone(), "p2a_exposed_addr_supply".to_string()), + share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5::new(client.clone(), "exposed_addr_supply_share".to_string()), } } } @@ -8755,7 +8897,7 @@ pub struct BrkClient { impl BrkClient { /// Client version. - pub const VERSION: &'static str = "v0.3.0-beta.3"; + pub const VERSION: &'static str = "v0.3.0-beta.4"; /// Create a new client with the given base URL. pub fn new(base_url: impl Into) -> Self { diff --git a/crates/brk_computer/src/distribution/addr/addr_count.rs b/crates/brk_computer/src/distribution/addr/addr_count.rs deleted file mode 100644 index 8d8271344..000000000 --- a/crates/brk_computer/src/distribution/addr/addr_count.rs +++ /dev/null @@ -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(#[traversable(flatten)] pub PerBlock); - -impl AddrCountVecs { - pub(crate) fn forced_import( - db: &Database, - name: &str, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { - 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); - -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(ByAddrType>); - -impl From> for AddrTypeToAddrCountVecs { - #[inline] - fn from(value: ByAddrType) -> Self { - Self(value) - } -} - -impl AddrTypeToAddrCountVecs { - pub(crate) fn forced_import( - db: &Database, - name: &str, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { - Ok(Self::from(ByAddrType::::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 { - 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>> { - self.0.values().map(|v| &v.height).collect() - } -} - -#[derive(Traversable)] -pub struct AddrCountsVecs { - pub all: AddrCountVecs, - #[traversable(flatten)] - pub by_addr_type: AddrTypeToAddrCountVecs, -} - -impl AddrCountsVecs { - pub(crate) fn forced_import( - db: &Database, - name: &str, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { - 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 { - 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(()) - } -} diff --git a/crates/brk_computer/src/distribution/addr/reused/count/vecs.rs b/crates/brk_computer/src/distribution/addr/count/all_vecs.rs similarity index 56% rename from crates/brk_computer/src/distribution/addr/reused/count/vecs.rs rename to crates/brk_computer/src/distribution/addr/count/all_vecs.rs index 7922ddffb..1cb4eda6f 100644 --- a/crates/brk_computer/src/distribution/addr/reused/count/vecs.rs +++ b/crates/brk_computer/src/distribution/addr/count/all_vecs.rs @@ -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( +pub struct AddrCountsVecs( #[traversable(flatten)] pub WithAddrTypes>, ); -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()); + } } diff --git a/crates/brk_computer/src/distribution/addr/exposed/count/mod.rs b/crates/brk_computer/src/distribution/addr/count/funded_total_vecs.rs similarity index 61% rename from crates/brk_computer/src/distribution/addr/exposed/count/mod.rs rename to crates/brk_computer/src/distribution/addr/count/funded_total_vecs.rs index f28bed55e..64af73ed9 100644 --- a/crates/brk_computer/src/distribution/addr/exposed/count/mod.rs +++ b/crates/brk_computer/src/distribution/addr/count/funded_total_vecs.rs @@ -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 { - pub funded: ExposedAddrCountAllVecs, - pub total: ExposedAddrCountAllVecs, +pub struct AddrCountFundedTotalVecs { + pub funded: AddrCountsVecs, + pub total: AddrCountsVecs, } -impl ExposedAddrCountsVecs { +impl AddrCountFundedTotalVecs { pub(crate) fn forced_import( db: &Database, + name: &str, version: Version, indexes: &indexes::Vecs, ) -> Result { 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)?; diff --git a/crates/brk_computer/src/distribution/addr/count/mod.rs b/crates/brk_computer/src/distribution/addr/count/mod.rs new file mode 100644 index 000000000..49aa00cb7 --- /dev/null +++ b/crates/brk_computer/src/distribution/addr/count/mod.rs @@ -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; diff --git a/crates/brk_computer/src/distribution/addr/count/state.rs b/crates/brk_computer/src/distribution/addr/count/state.rs new file mode 100644 index 000000000..f1f73c2e9 --- /dev/null +++ b/crates/brk_computer/src/distribution/addr/count/state.rs @@ -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); + +impl AddrTypeToAddrCount { + #[inline] + pub(crate) fn sum(&self) -> u64 { + self.0.values().sum() + } +} + +impl From> for AddrTypeToAddrCount { + #[inline] + fn from(value: ByAddrType) -> 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() + } +} diff --git a/crates/brk_computer/src/distribution/addr/delta.rs b/crates/brk_computer/src/distribution/addr/delta.rs index bad052a6a..e5a92f203 100644 --- a/crates/brk_computer/src/distribution/addr/delta.rs +++ b/crates/brk_computer/src/distribution/addr/delta.rs @@ -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, ) diff --git a/crates/brk_computer/src/distribution/addr/exposed/count/state.rs b/crates/brk_computer/src/distribution/addr/exposed/count/state.rs deleted file mode 100644 index 2b902d9e1..000000000 --- a/crates/brk_computer/src/distribution/addr/exposed/count/state.rs +++ /dev/null @@ -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); - -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| -> 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() - } - } -} diff --git a/crates/brk_computer/src/distribution/addr/exposed/count/vecs.rs b/crates/brk_computer/src/distribution/addr/exposed/count/vecs.rs deleted file mode 100644 index 49da53253..000000000 --- a/crates/brk_computer/src/distribution/addr/exposed/count/vecs.rs +++ /dev/null @@ -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( - #[traversable(flatten)] pub WithAddrTypes>, -); - -impl ExposedAddrCountAllVecs { - pub(crate) fn forced_import( - db: &Database, - name: &str, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { - Ok(Self(WithAddrTypes::>::forced_import( - db, name, version, indexes, - )?)) - } -} diff --git a/crates/brk_computer/src/distribution/addr/exposed/mod.rs b/crates/brk_computer/src/distribution/addr/exposed/mod.rs index 04af499ca..9e4b7b96d 100644 --- a/crates/brk_computer/src/distribution/addr/exposed/mod.rs +++ b/crates/brk_computer/src/distribution/addr/exposed/mod.rs @@ -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 { - pub count: ExposedAddrCountsVecs, - pub supply: ExposedAddrSupplyVecs, + pub count: AddrCountFundedTotalVecs, + pub supply: AddrSupplyVecs, #[traversable(wrap = "supply", rename = "share")] - pub supply_share: ExposedSupplyShareVecs, + pub supply_share: AddrSupplyShareVecs, } impl ExposedAddrVecs { @@ -65,9 +67,9 @@ impl ExposedAddrVecs { indexes: &indexes::Vecs, ) -> Result { 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::( - 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::( - 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(()) } } diff --git a/crates/brk_computer/src/distribution/addr/exposed/state.rs b/crates/brk_computer/src/distribution/addr/exposed/state.rs new file mode 100644 index 000000000..2e917fabd --- /dev/null +++ b/crates/brk_computer/src/distribution/addr/exposed/state.rs @@ -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)), + } + } +} diff --git a/crates/brk_computer/src/distribution/addr/exposed/supply/mod.rs b/crates/brk_computer/src/distribution/addr/exposed/supply/mod.rs deleted file mode 100644 index 4fd7fe472..000000000 --- a/crates/brk_computer/src/distribution/addr/exposed/supply/mod.rs +++ /dev/null @@ -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; diff --git a/crates/brk_computer/src/distribution/addr/exposed/supply/share.rs b/crates/brk_computer/src/distribution/addr/exposed/supply/share.rs deleted file mode 100644 index 47fad33c8..000000000 --- a/crates/brk_computer/src/distribution/addr/exposed/supply/share.rs +++ /dev/null @@ -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( - #[traversable(flatten)] pub WithAddrTypes>, -); - -impl ExposedSupplyShareVecs { - pub(crate) fn forced_import( - db: &Database, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { - Ok(Self( - WithAddrTypes::>::forced_import( - db, - "exposed_supply_share", - version, - indexes, - )?, - )) - } -} diff --git a/crates/brk_computer/src/distribution/addr/exposed/supply/state.rs b/crates/brk_computer/src/distribution/addr/exposed/supply/state.rs deleted file mode 100644 index cf0bc3514..000000000 --- a/crates/brk_computer/src/distribution/addr/exposed/supply/state.rs +++ /dev/null @@ -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); - -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() - } - } -} diff --git a/crates/brk_computer/src/distribution/addr/mod.rs b/crates/brk_computer/src/distribution/addr/mod.rs index 433af7bdb..1cc61cd0d 100644 --- a/crates/brk_computer/src/distribution/addr/mod.rs +++ b/crates/brk_computer/src/distribution/addr/mod.rs @@ -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}; diff --git a/crates/brk_computer/src/distribution/addr/reused/count/mod.rs b/crates/brk_computer/src/distribution/addr/reused/count/mod.rs deleted file mode 100644 index 399b3acc4..000000000 --- a/crates/brk_computer/src/distribution/addr/reused/count/mod.rs +++ /dev/null @@ -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 { - pub funded: ReusedAddrCountAllVecs, - pub total: ReusedAddrCountAllVecs, -} - -impl ReusedAddrCountsVecs { - pub(crate) fn forced_import( - db: &Database, - version: Version, - indexes: &indexes::Vecs, - ) -> Result { - 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 { - 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(()) - } -} diff --git a/crates/brk_computer/src/distribution/addr/reused/count/state.rs b/crates/brk_computer/src/distribution/addr/reused/count/state.rs deleted file mode 100644 index 54598f261..000000000 --- a/crates/brk_computer/src/distribution/addr/reused/count/state.rs +++ /dev/null @@ -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); - -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| -> 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() - } - } -} diff --git a/crates/brk_computer/src/distribution/addr/reused/events/mod.rs b/crates/brk_computer/src/distribution/addr/reused/events/mod.rs index 224eb0ebf..1c11ff1f7 100644 --- a/crates/brk_computer/src/distribution/addr/reused/events/mod.rs +++ b/crates/brk_computer/src/distribution/addr/reused/events/mod.rs @@ -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; diff --git a/crates/brk_computer/src/distribution/addr/reused/events/state.rs b/crates/brk_computer/src/distribution/addr/reused/events/state.rs index d74804267..2aaef4d50 100644 --- a/crates/brk_computer/src/distribution/addr/reused/events/state.rs +++ b/crates/brk_computer/src/distribution/addr/reused/events/state.rs @@ -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); +pub struct AddrTypeToAddrEventCount(ByAddrType); -impl AddrTypeToReusedAddrEventCount { +impl AddrTypeToAddrEventCount { #[inline] pub(crate) fn sum(&self) -> u64 { self.0.values().sum() diff --git a/crates/brk_computer/src/distribution/addr/reused/events/vecs.rs b/crates/brk_computer/src/distribution/addr/reused/events/vecs.rs index 48d386363..e49418913 100644 --- a/crates/brk_computer/src/distribution/addr/reused/events/vecs.rs +++ b/crates/brk_computer/src/distribution/addr/reused/events/vecs.rs @@ -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 { +pub struct AddrEventsVecs { pub output_to_reused_addr_count: WithAddrTypes>, pub output_to_reused_addr_share: WithAddrTypes>, @@ -75,9 +75,10 @@ pub struct ReusedAddrEventsVecs { pub active_reused_addr_share: PerBlockRollingAverage, } -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, ) { diff --git a/crates/brk_computer/src/distribution/addr/reused/mod.rs b/crates/brk_computer/src/distribution/addr/reused/mod.rs index c7d4778e8..f0867933d 100644 --- a/crates/brk_computer/src/distribution/addr/reused/mod.rs +++ b/crates/brk_computer/src/distribution/addr/reused/mod.rs @@ -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 { - pub count: ReusedAddrCountsVecs, - pub events: ReusedAddrEventsVecs, + pub count: AddrCountFundedTotalVecs, + pub events: AddrEventsVecs, + pub supply: AddrSupplyVecs, + #[traversable(wrap = "supply", rename = "share")] + pub supply_share: AddrSupplyShareVecs, } impl ReusedAddrVecs { pub(crate) fn forced_import( db: &Database, + name: &str, version: Version, indexes: &indexes::Vecs, cached_starts: &Windows<&WindowStartVec>, ) -> Result { 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, + type_supply_sats: &ByAddrType<&impl ReadableVec>, 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(()) diff --git a/crates/brk_computer/src/distribution/addr/reused/state.rs b/crates/brk_computer/src/distribution/addr/reused/state.rs new file mode 100644 index 000000000..560ce416d --- /dev/null +++ b/crates/brk_computer/src/distribution/addr/reused/state.rs @@ -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(), + } + } +} diff --git a/crates/brk_computer/src/distribution/addr/state.rs b/crates/brk_computer/src/distribution/addr/state.rs new file mode 100644 index 000000000..fd0f87817 --- /dev/null +++ b/crates/brk_computer/src/distribution/addr/state.rs @@ -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)), + } + } +} diff --git a/crates/brk_computer/src/distribution/addr/supply/mod.rs b/crates/brk_computer/src/distribution/addr/supply/mod.rs new file mode 100644 index 000000000..f00858a42 --- /dev/null +++ b/crates/brk_computer/src/distribution/addr/supply/mod.rs @@ -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; diff --git a/crates/brk_computer/src/distribution/addr/supply/share.rs b/crates/brk_computer/src/distribution/addr/supply/share.rs new file mode 100644 index 000000000..d7175294d --- /dev/null +++ b/crates/brk_computer/src/distribution/addr/supply/share.rs @@ -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( + #[traversable(flatten)] pub WithAddrTypes>, +); + +impl AddrSupplyShareVecs { + pub(crate) fn forced_import( + db: &Database, + name: &str, + version: Version, + indexes: &indexes::Vecs, + ) -> Result { + Ok(Self( + WithAddrTypes::>::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, + type_supply_sats: &ByAddrType<&impl ReadableVec>, + exit: &Exit, + ) -> Result<()> { + self.all.compute_binary::( + 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::( + max_from, + &cat.sats.height, + *denom, + exit, + )?; + } + Ok(()) + } +} diff --git a/crates/brk_computer/src/distribution/addr/supply/state.rs b/crates/brk_computer/src/distribution/addr/supply/state.rs new file mode 100644 index 000000000..f3b36c8bf --- /dev/null +++ b/crates/brk_computer/src/distribution/addr/supply/state.rs @@ -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); + +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> for AddrTypeToSupply { + #[inline] + fn from(value: ByAddrType) -> 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() + } +} diff --git a/crates/brk_computer/src/distribution/addr/exposed/supply/vecs.rs b/crates/brk_computer/src/distribution/addr/supply/vecs.rs similarity index 51% rename from crates/brk_computer/src/distribution/addr/exposed/supply/vecs.rs rename to crates/brk_computer/src/distribution/addr/supply/vecs.rs index 54889b7d7..70d3758dc 100644 --- a/crates/brk_computer/src/distribution/addr/exposed/supply/vecs.rs +++ b/crates/brk_computer/src/distribution/addr/supply/vecs.rs @@ -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( +pub struct AddrSupplyVecs( #[traversable(flatten)] pub WithAddrTypes>, ); -impl ExposedAddrSupplyVecs { +impl AddrSupplyVecs { pub(crate) fn forced_import( db: &Database, + name: &str, version: Version, indexes: &indexes::Vecs, ) -> Result { Ok(Self(WithAddrTypes::::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()); + } } diff --git a/crates/brk_computer/src/distribution/addr/total_addr_count.rs b/crates/brk_computer/src/distribution/addr/total_addr_count.rs index 53a3982f4..55a1de047 100644 --- a/crates/brk_computer/src/distribution/addr/total_addr_count.rs +++ b/crates/brk_computer/src/distribution/addr/total_addr_count.rs @@ -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() diff --git a/crates/brk_computer/src/distribution/block/cohort/received.rs b/crates/brk_computer/src/distribution/block/cohort/received.rs index e43ce1ace..3def7aca4 100644 --- a/crates/brk_computer/src/distribution/block/cohort/received.rs +++ b/crates/brk_computer/src/distribution/block/cohort/received.rs @@ -1,12 +1,9 @@ -use brk_cohort::{AmountBucket, ByAddrType}; +use brk_cohort::AmountBucket; use brk_types::{Cents, Sats, TypeIndex}; use rustc_hash::FxHashMap; use crate::distribution::{ - addr::{ - AddrTypeToActivityCounts, AddrTypeToExposedAddrCount, AddrTypeToExposedSupply, - AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, AddrTypeToVec, - }, + addr::{AddrMetricsState, AddrReceivePreState, AddrTypeToVec}, cohorts::AddrCohorts, }; @@ -19,22 +16,12 @@ struct AggregatedReceive { output_count: u32, } -#[allow(clippy::too_many_arguments)] pub(crate) fn process_received( received_data: AddrTypeToVec<(TypeIndex, Sats)>, cohorts: &mut AddrCohorts, lookup: &mut AddrLookup<'_>, price: Cents, - addr_count: &mut ByAddrType, - empty_addr_count: &mut ByAddrType, - activity_counts: &mut AddrTypeToActivityCounts, - reused_addr_count: &mut AddrTypeToReusedAddrCount, - total_reused_addr_count: &mut AddrTypeToReusedAddrCount, - output_to_reused_addr_count: &mut AddrTypeToReusedAddrEventCount, - active_reused_addr_count: &mut AddrTypeToReusedAddrEventCount, - exposed_addr_count: &mut AddrTypeToExposedAddrCount, - total_exposed_addr_count: &mut AddrTypeToExposedAddrCount, - exposed_supply: &mut AddrTypeToExposedSupply, + state: &mut AddrMetricsState, ) { let max_type_len = received_data .iter() @@ -49,19 +36,7 @@ pub(crate) fn process_received( continue; } - // Cache mutable refs for this address type - let type_addr_count = addr_count.get_mut(output_type).unwrap(); - let type_empty_count = empty_addr_count.get_mut(output_type).unwrap(); - let type_activity = activity_counts.get_mut_unwrap(output_type); - let type_reused_count = reused_addr_count.get_mut(output_type).unwrap(); - let type_total_reused_count = total_reused_addr_count.get_mut(output_type).unwrap(); - let type_output_to_reused_count = output_to_reused_addr_count.get_mut(output_type).unwrap(); - let type_active_reused_count = active_reused_addr_count.get_mut(output_type).unwrap(); - let type_exposed_count = exposed_addr_count.get_mut(output_type).unwrap(); - let type_total_exposed_count = total_exposed_addr_count.get_mut(output_type).unwrap(); - let type_exposed_supply = exposed_supply.get_mut(output_type).unwrap(); - - // Aggregate receives by address - each address processed exactly once + // Aggregate per address so each address is processed exactly once. for (type_index, value) in vec { let entry = aggregated.entry(type_index).or_default(); entry.total_value += value; @@ -70,39 +45,13 @@ pub(crate) fn process_received( for (type_index, recv) in aggregated.drain() { let (addr_data, status) = lookup.get_or_create_for_receive(output_type, type_index); + let pre = AddrReceivePreState::capture(addr_data, output_type); - // Track receiving activity - each address in receive aggregation - type_activity.receiving += 1; - - // Capture state BEFORE the receive mutates funded_txo_count - let was_funded = addr_data.is_funded(); - let was_reused = addr_data.is_reused(); - let funded_txo_count_before = addr_data.funded_txo_count; - let was_pubkey_exposed = addr_data.is_pubkey_exposed(output_type); - let exposed_contribution_before = addr_data.exposed_supply_contribution(output_type); - - match status { - TrackingStatus::New => { - *type_addr_count += 1; - } - TrackingStatus::WasEmpty => { - *type_addr_count += 1; - *type_empty_count -= 1; - // Reactivated - was empty, now has funds - type_activity.reactivated += 1; - } - TrackingStatus::Tracked => {} - } - - let is_new_entry = matches!(status, TrackingStatus::New | TrackingStatus::WasEmpty); - - if is_new_entry { - // New/was-empty address - just add to cohort + if matches!(status, TrackingStatus::New | TrackingStatus::WasEmpty) { addr_data.receive_outputs(recv.total_value, price, recv.output_count); - let new_bucket = AmountBucket::from(recv.total_value); cohorts .amount_range - .get_mut_by_bucket(new_bucket) + .get_mut_by_bucket(AmountBucket::from(recv.total_value)) .state .as_mut() .unwrap() @@ -114,7 +63,6 @@ pub(crate) fn process_received( let new_bucket = AmountBucket::from(new_balance); if let Some((old_bucket, new_bucket)) = prev_bucket.transition_to(new_bucket) { - // Crossing cohort boundary - subtract from old, add to new let cohort_state = cohorts .amount_range .get_mut_by_bucket(old_bucket) @@ -122,7 +70,6 @@ pub(crate) fn process_received( .as_mut() .unwrap(); - // Debug info for tracking down underflow issues if cohort_state.inner.supply.utxo_count < addr_data.utxo_count() as u64 { panic!( "process_received: cohort underflow detected!\n\ @@ -148,7 +95,6 @@ pub(crate) fn process_received( .unwrap() .add(addr_data); } else { - // Staying in same cohort - just receive cohorts .amount_range .get_mut_by_bucket(new_bucket) @@ -159,61 +105,7 @@ pub(crate) fn process_received( } } - // Update reused counts based on the post-receive state - let is_now_reused = addr_data.is_reused(); - if is_now_reused && !was_reused { - // Newly crossed the reuse threshold this block - *type_reused_count += 1; - *type_total_reused_count += 1; - } else if is_now_reused && !was_funded { - // Already-reused address reactivating into the funded set - *type_reused_count += 1; - } - - // Block-level "active reused address" count: each address - // is processed exactly once here (via aggregation), so we - // count it once iff it is reused after the block's receives. - // The sender-side counterpart in process_sent dedupes - // against `received_addrs` so addresses that did both - // aren't double-counted. - if is_now_reused { - *type_active_reused_count += 1; - } - - // Per-block reused-use count: every individual output to this - // address counts iff, at the moment the output arrives, the - // address had already received at least one prior output - // (i.e. it is an output-level "address reuse event"). With - // aggregation, that means we skip the very first output the - // address ever sees and count every subsequent one, so - // `skipped` is `max(0, 1 - before)`. - let skipped = 1u32.saturating_sub(funded_txo_count_before); - let counted = recv.output_count.saturating_sub(skipped); - *type_output_to_reused_count += u64::from(counted); - - // Update exposed counts. The address's pubkey-exposure state - // is unchanged by a receive (spent_txo_count unchanged), so we - // can use the captured `was_pubkey_exposed` for both pre and post. - // After the receive the address is always funded, so it's in the - // funded exposed set iff its pubkey is exposed. - // - // Funded exposed enters when the address wasn't funded before but - // is now AND its pubkey is exposed. - // Total exposed (pk_exposed_at_funding types only) increments on - // first-ever receive (status == TrackingStatus::New); for other - // types it's incremented in process_sent on the first spend. - if !was_funded && was_pubkey_exposed { - *type_exposed_count += 1; - } - if output_type.pubkey_exposed_at_funding() && matches!(status, TrackingStatus::New) { - *type_total_exposed_count += 1; - } - - // Update exposed supply via post-receive contribution delta. - let exposed_contribution_after = addr_data.exposed_supply_contribution(output_type); - // Receives can only add to balance and membership, so the delta - // is always non-negative. - *type_exposed_supply += exposed_contribution_after - exposed_contribution_before; + state.on_receive_applied(output_type, status, addr_data, &pre, recv.output_count); } } } diff --git a/crates/brk_computer/src/distribution/block/cohort/sent.rs b/crates/brk_computer/src/distribution/block/cohort/sent.rs index cf7c05d02..69fcc0410 100644 --- a/crates/brk_computer/src/distribution/block/cohort/sent.rs +++ b/crates/brk_computer/src/distribution/block/cohort/sent.rs @@ -5,29 +5,20 @@ use rustc_hash::FxHashSet; use vecdb::VecIndex; use crate::distribution::{ - addr::{ - AddrTypeToActivityCounts, AddrTypeToExposedAddrCount, AddrTypeToExposedSupply, - AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, HeightToAddrTypeToVec, - }, + addr::{AddrMetricsState, AddrSendPreState, HeightToAddrTypeToVec}, cohorts::AddrCohorts, compute::PriceRangeMax, }; use super::super::cache::AddrLookup; -/// Process sent outputs for address cohorts. +/// Process sent UTXOs for address cohorts: age metrics, cohort membership, +/// and empty-address transitions. /// -/// For each spent UTXO: -/// 1. Look up address data -/// 2. Calculate age metrics -/// 3. Update address balance and cohort membership -/// 4. Handle addresses becoming empty -/// -/// Note: Takes separate price/timestamp slices instead of chain_state to allow -/// parallel execution with UTXO cohort processing (which mutates chain_state). -/// -/// `price_range_max` is used to compute the peak price during each UTXO's holding period -/// for accurate peak regret calculation. +/// Takes separate price/timestamp slices rather than `chain_state` so it can +/// run in parallel with UTXO cohort processing (which mutates `chain_state`). +/// `price_range_max` feeds peak-regret computation via max price during +/// each UTXO's holding period. #[allow(clippy::too_many_arguments)] pub(crate) fn process_sent( sent_data: HeightToAddrTypeToVec<(TypeIndex, Sats)>, @@ -35,15 +26,7 @@ pub(crate) fn process_sent( lookup: &mut AddrLookup<'_>, current_price: Cents, price_range_max: &PriceRangeMax, - addr_count: &mut ByAddrType, - empty_addr_count: &mut ByAddrType, - activity_counts: &mut AddrTypeToActivityCounts, - reused_addr_count: &mut AddrTypeToReusedAddrCount, - input_from_reused_addr_count: &mut AddrTypeToReusedAddrEventCount, - active_reused_addr_count: &mut AddrTypeToReusedAddrEventCount, - exposed_addr_count: &mut AddrTypeToExposedAddrCount, - total_exposed_addr_count: &mut AddrTypeToExposedAddrCount, - exposed_supply: &mut AddrTypeToExposedSupply, + state: &mut AddrMetricsState, received_addrs: &ByAddrType>, height_to_price: &[Cents], height_to_timestamp: &[Timestamp], @@ -57,68 +40,22 @@ pub(crate) fn process_sent( let prev_price = height_to_price[receive_height.to_usize()]; let prev_timestamp = height_to_timestamp[receive_height.to_usize()]; let age = Age::new(current_timestamp, prev_timestamp); - - // Compute peak spot price during holding period for peak regret let peak_price = price_range_max.max_between(receive_height, current_height); for (output_type, vec) in by_type.unwrap().into_iter() { - // Cache mutable refs for this address type - let type_addr_count = addr_count.get_mut(output_type).unwrap(); - let type_empty_count = empty_addr_count.get_mut(output_type).unwrap(); - let type_activity = activity_counts.get_mut_unwrap(output_type); - let type_reused_count = reused_addr_count.get_mut(output_type).unwrap(); - let type_input_from_reused_count = - input_from_reused_addr_count.get_mut(output_type).unwrap(); - let type_active_reused_count = active_reused_addr_count.get_mut(output_type).unwrap(); - let type_exposed_count = exposed_addr_count.get_mut(output_type).unwrap(); - let type_total_exposed_count = total_exposed_addr_count.get_mut(output_type).unwrap(); - let type_exposed_supply = exposed_supply.get_mut(output_type).unwrap(); let type_received = received_addrs.get(output_type); let type_seen = seen_senders.get_mut_unwrap(output_type); for (type_index, value) in vec { let addr_data = lookup.get_for_send(output_type, type_index); - - // "Input from a reused address" event: the sending - // address is in the reused set (lifetime - // funded_txo_count > 1). Checked once per input. The - // spend itself doesn't touch funded_txo_count so the - // predicate is stable before/after `cohort_state.send`. - if addr_data.is_reused() { - *type_input_from_reused_count += 1; - } + let pre = AddrSendPreState::capture(addr_data, output_type); let prev_balance = addr_data.balance(); let new_balance = prev_balance.checked_sub(value).unwrap(); - - // On first encounter of this address this block, track activity - if type_seen.insert(type_index) { - type_activity.sending += 1; - - let also_received = type_received.is_some_and(|s| s.contains(&type_index)); - // Track "bidirectional": addresses that sent AND - // received this block. - if also_received { - type_activity.bidirectional += 1; - } - - // Block-level "active reused address" count: count - // every distinct sender that's reused, but skip - // those that also received this block (already - // counted in process_received). - if !also_received && addr_data.is_reused() { - *type_active_reused_count += 1; - } - } - + let is_first_encounter = type_seen.insert(type_index); + let also_received = type_received.is_some_and(|s| s.contains(&type_index)); let will_be_empty = addr_data.has_1_utxos(); - // Capture exposed state BEFORE the spend mutates spent_txo_count. - let was_pubkey_exposed = addr_data.is_pubkey_exposed(output_type); - let exposed_contribution_before = - addr_data.exposed_supply_contribution(output_type); - - // Compute buckets once let prev_bucket = AmountBucket::from(prev_balance); let new_bucket = AmountBucket::from(new_balance); let crossing_boundary = prev_bucket != new_bucket; @@ -130,50 +67,21 @@ pub(crate) fn process_sent( .as_mut() .unwrap(); + // Mutates addr_data.spent_txo_count (+= 1). on_send_applied reads the post-spend view. cohort_state.send(addr_data, value, current_price, prev_price, peak_price, age)?; - // addr_data.spent_txo_count is now incremented by 1. + state.on_send_applied( + output_type, + addr_data, + &pre, + is_first_encounter, + also_received, + will_be_empty, + ); - // Update exposed supply via post-spend contribution delta. - let exposed_contribution_after = addr_data.exposed_supply_contribution(output_type); - if exposed_contribution_after >= exposed_contribution_before { - *type_exposed_supply += - exposed_contribution_after - exposed_contribution_before; - } else { - *type_exposed_supply -= - exposed_contribution_before - exposed_contribution_after; - } - - // Update exposed counts on first-ever pubkey exposure. - // For non-pk-exposed types this fires on the first spend; for - // pk-exposed types it never fires here (was_pubkey_exposed was - // already true at first receive in process_received). - if !was_pubkey_exposed { - *type_total_exposed_count += 1; - if !will_be_empty { - *type_exposed_count += 1; - } - } - - // If crossing a bucket boundary, remove the (now-updated) address from old bucket if will_be_empty || crossing_boundary { cohort_state.subtract(addr_data); } - - // Migrate address to new bucket or mark as empty if will_be_empty { - *type_addr_count -= 1; - *type_empty_count += 1; - // Reused addr leaving the funded reused set - if addr_data.is_reused() { - *type_reused_count -= 1; - } - // Exposed addr leaving the funded exposed set: was in set - // iff its pubkey was exposed pre-spend (since it was funded - // to be in process_sent in the first place), and now leaves - // because it's empty. - if was_pubkey_exposed { - *type_exposed_count -= 1; - } lookup.move_to_empty(output_type, type_index); } else if crossing_boundary { cohorts diff --git a/crates/brk_computer/src/distribution/compute/block_loop.rs b/crates/brk_computer/src/distribution/compute/block_loop.rs index e28770730..7c9f881ae 100644 --- a/crates/brk_computer/src/distribution/compute/block_loop.rs +++ b/crates/brk_computer/src/distribution/compute/block_loop.rs @@ -11,10 +11,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec, unli use crate::{ distribution::{ - addr::{ - AddrTypeToActivityCounts, AddrTypeToAddrCount, AddrTypeToExposedAddrCount, - AddrTypeToExposedSupply, AddrTypeToReusedAddrCount, AddrTypeToReusedAddrEventCount, - }, + addr::AddrMetricsState, block::{ AddrCache, InputsResult, process_inputs, process_outputs, process_received, process_sent, @@ -193,51 +190,9 @@ pub(crate) fn process_blocks( .first_index .collect_range_at(start_usize, end_usize); - // Track running totals - recover from previous height if resuming - debug!("recovering addr_counts from height {}", starting_height); - let ( - mut addr_counts, - mut empty_addr_counts, - mut reused_addr_counts, - mut total_reused_addr_counts, - mut exposed_addr_counts, - mut total_exposed_addr_counts, - mut exposed_supply, - ) = if starting_height > Height::ZERO { - ( - AddrTypeToAddrCount::from((&vecs.addrs.funded.by_addr_type, starting_height)), - AddrTypeToAddrCount::from((&vecs.addrs.empty.by_addr_type, starting_height)), - AddrTypeToReusedAddrCount::from((&vecs.addrs.reused.count.funded, starting_height)), - AddrTypeToReusedAddrCount::from((&vecs.addrs.reused.count.total, starting_height)), - AddrTypeToExposedAddrCount::from((&vecs.addrs.exposed.count.funded, starting_height)), - AddrTypeToExposedAddrCount::from((&vecs.addrs.exposed.count.total, starting_height)), - AddrTypeToExposedSupply::from((&vecs.addrs.exposed.supply, starting_height)), - ) - } else { - ( - AddrTypeToAddrCount::default(), - AddrTypeToAddrCount::default(), - AddrTypeToReusedAddrCount::default(), - AddrTypeToReusedAddrCount::default(), - AddrTypeToExposedAddrCount::default(), - AddrTypeToExposedAddrCount::default(), - AddrTypeToExposedSupply::default(), - ) - }; - debug!("addr_counts recovered"); - - // Track activity counts - reset each block - let mut activity_counts = AddrTypeToActivityCounts::default(); - // Reused-addr event counts (receive + spend side). Per-block - // flow, reset each block. - let mut output_to_reused_addr_counts = AddrTypeToReusedAddrEventCount::default(); - let mut input_from_reused_addr_counts = AddrTypeToReusedAddrEventCount::default(); - // Distinct addresses active this block whose lifetime - // funded_txo_count > 1 after this block's events. Incremented in - // process_received for every receiver that ends up reused, and in - // process_sent for every sender that's reused AND didn't also - // receive this block (deduped via `received_addrs`). - let mut active_reused_addr_counts = AddrTypeToReusedAddrEventCount::default(); + debug!("recovering addr metrics state from height {}", starting_height); + let mut state = AddrMetricsState::from((&vecs.addrs, starting_height)); + debug!("addr metrics state recovered"); debug!("creating AddrCache"); let mut cache = AddrCache::new(); @@ -253,12 +208,7 @@ pub(crate) fn process_blocks( vecs.utxo_cohorts .par_iter_vecs_mut() .chain(vecs.addr_cohorts.par_iter_vecs_mut()) - .chain(vecs.addrs.funded.par_iter_height_mut()) - .chain(vecs.addrs.empty.par_iter_height_mut()) - .chain(vecs.addrs.activity.par_iter_height_mut()) - .chain(vecs.addrs.reused.par_iter_height_mut()) - .chain(vecs.addrs.exposed.par_iter_height_mut()) - .chain(vecs.addrs.avg_amount.par_iter_height_mut()) + .chain(vecs.addrs.par_iter_height_mut()) .chain(rayon::iter::once( &mut vecs.coinblocks_destroyed.block as &mut dyn AnyStoredVec, )) @@ -309,11 +259,7 @@ pub(crate) fn process_blocks( p2wsh: TypeIndex::from(first_p2wsh_vec[offset].to_usize()), }; - // Reset per-block activity counts - activity_counts.reset(); - output_to_reused_addr_counts.reset(); - input_from_reused_addr_counts.reset(); - active_reused_addr_counts.reset(); + state.reset_per_block(); // Process outputs, inputs, and tick-tock in parallel via rayon::join. // Collection (build tx_index mappings + bulk mmap reads) is merged into the @@ -474,40 +420,21 @@ pub(crate) fn process_blocks( || -> Result<()> { let mut lookup = cache.as_lookup(); - // Process received outputs (addresses receiving funds) process_received( outputs_result.received_data, &mut vecs.addr_cohorts, &mut lookup, block_price, - &mut addr_counts, - &mut empty_addr_counts, - &mut activity_counts, - &mut reused_addr_counts, - &mut total_reused_addr_counts, - &mut output_to_reused_addr_counts, - &mut active_reused_addr_counts, - &mut exposed_addr_counts, - &mut total_exposed_addr_counts, - &mut exposed_supply, + &mut state, ); - // Process sent inputs (addresses sending funds) process_sent( inputs_result.sent_data, &mut vecs.addr_cohorts, &mut lookup, block_price, ctx.price_range_max, - &mut addr_counts, - &mut empty_addr_counts, - &mut activity_counts, - &mut reused_addr_counts, - &mut input_from_reused_addr_counts, - &mut active_reused_addr_counts, - &mut exposed_addr_counts, - &mut total_exposed_addr_counts, - &mut exposed_supply, + &mut state, &received_addrs, height_to_price_vec, height_to_timestamp_vec, @@ -522,44 +449,10 @@ pub(crate) fn process_blocks( // Update Fenwick tree from pending deltas (must happen before push_cohort_states drains pending) vecs.utxo_cohorts.update_fenwick_from_pending(); - // Push to height-indexed vectors - vecs.addrs - .funded - .push_height(addr_counts.sum(), &addr_counts); - vecs.addrs - .empty - .push_height(empty_addr_counts.sum(), &empty_addr_counts); - vecs.addrs.activity.push_height(&activity_counts); - vecs.addrs.reused.count.funded.push_height( - reused_addr_counts.sum(), - reused_addr_counts.values().copied(), - ); - vecs.addrs.reused.count.total.push_height( - total_reused_addr_counts.sum(), - total_reused_addr_counts.values().copied(), - ); - let activity_totals = activity_counts.totals(); + let activity_totals = state.activity.totals(); let active_addr_count = activity_totals.sending + activity_totals.receiving - activity_totals.bidirectional; - let active_reused = u32::try_from(active_reused_addr_counts.sum()).unwrap(); - vecs.addrs.reused.events.push_height( - &output_to_reused_addr_counts, - &input_from_reused_addr_counts, - active_addr_count, - active_reused, - ); - vecs.addrs.exposed.count.funded.push_height( - exposed_addr_counts.sum(), - exposed_addr_counts.values().copied(), - ); - vecs.addrs.exposed.count.total.push_height( - total_exposed_addr_counts.sum(), - total_exposed_addr_counts.values().copied(), - ); - vecs.addrs - .exposed - .supply - .push_height(exposed_supply.sum(), exposed_supply.values().copied()); + vecs.addrs.push_height(&state, active_addr_count); let is_last_of_day = is_last_of_day[offset]; let date_opt = is_last_of_day.then(|| Date::from(timestamp)); @@ -632,7 +525,7 @@ fn push_cohort_states( height: Height, height_price: Cents, ) { - // Phase 1: push + unrealized (no reset yet; states still needed for aggregation) + // Phase 1: push + unrealized (no reset yet, states still needed for aggregation) rayon::join( || { utxo_cohorts.par_iter_separate_mut().for_each(|v| { diff --git a/crates/brk_computer/src/distribution/compute/write.rs b/crates/brk_computer/src/distribution/compute/write.rs index e50457c3d..39d76072a 100644 --- a/crates/brk_computer/src/distribution/compute/write.rs +++ b/crates/brk_computer/src/distribution/compute/write.rs @@ -76,11 +76,7 @@ pub(crate) fn write( vecs.any_addr_indexes .par_iter_mut() .chain(vecs.addrs_data.par_iter_mut()) - .chain(vecs.addrs.funded.par_iter_height_mut()) - .chain(vecs.addrs.empty.par_iter_height_mut()) - .chain(vecs.addrs.activity.par_iter_height_mut()) - .chain(vecs.addrs.reused.par_iter_height_mut()) - .chain(vecs.addrs.exposed.par_iter_height_mut()) + .chain(vecs.addrs.par_iter_stateful_height_mut()) .chain( [ &mut vecs.supply_state as &mut dyn AnyStoredVec, diff --git a/crates/brk_computer/src/distribution/vecs.rs b/crates/brk_computer/src/distribution/vecs.rs index a9ad42fca..9d5797c8c 100644 --- a/crates/brk_computer/src/distribution/vecs.rs +++ b/crates/brk_computer/src/distribution/vecs.rs @@ -8,9 +8,10 @@ use brk_types::{ Cents, EmptyAddrData, EmptyAddrIndex, FundedAddrData, FundedAddrIndex, Height, Indexes, StoredF64, SupplyState, Timestamp, TxIndex, Version, }; +use rayon::prelude::*; use tracing::{debug, info}; use vecdb::{ - AnyVec, BytesVec, Database, Exit, ImportableVec, LazyVecFrom1, ReadOnlyClone, + AnyStoredVec, AnyVec, BytesVec, Database, Exit, ImportableVec, LazyVecFrom1, ReadOnlyClone, ReadableCloneableVec, ReadableVec, Rw, Stamp, StorageMode, WritableVec, }; @@ -34,8 +35,8 @@ use crate::{ use super::{ AddrCohorts, AddrsDataVecs, AnyAddrIndexesVecs, RangeMap, UTXOCohorts, addr::{ - AddrActivityVecs, AddrCountsVecs, DeltaVecs, ExposedAddrVecs, NewAddrCountVecs, - ReusedAddrVecs, TotalAddrCountVecs, + AddrActivityVecs, AddrCountsVecs, AddrMetricsState, DeltaVecs, ExposedAddrVecs, + NewAddrCountVecs, ReusedAddrVecs, TotalAddrCountVecs, }, metrics::AvgAmountMetrics, }; @@ -50,6 +51,7 @@ pub struct AddrMetricsVecs { pub total: TotalAddrCountVecs, pub new: NewAddrCountVecs, pub reused: ReusedAddrVecs, + pub respent: ReusedAddrVecs, pub exposed: ExposedAddrVecs, pub delta: DeltaVecs, pub avg_amount: WithAddrTypes>, @@ -60,6 +62,71 @@ pub struct AddrMetricsVecs { pub empty_index: LazyVecFrom1, } +impl AddrMetricsVecs { + pub(crate) fn reset_height(&mut self) -> Result<()> { + self.funded.reset_height()?; + self.empty.reset_height()?; + self.activity.reset_height()?; + self.reused.reset_height()?; + self.respent.reset_height()?; + self.exposed.reset_height()?; + self.avg_amount.reset_height()?; + Ok(()) + } + + pub(crate) fn min_stateful_len(&self) -> usize { + self.funded + .min_stateful_len() + .min(self.empty.min_stateful_len()) + .min(self.activity.min_stateful_len()) + .min(self.reused.min_stateful_len()) + .min(self.respent.min_stateful_len()) + .min(self.exposed.min_stateful_len()) + } + + /// Stateful vecs pushed per block. Mirrors [`Self::push_height`] and + /// [`Self::min_stateful_len`]. Used by the stamped write path. + pub(crate) fn par_iter_stateful_height_mut( + &mut self, + ) -> impl ParallelIterator { + self.funded + .par_iter_height_mut() + .chain(self.empty.par_iter_height_mut()) + .chain(self.activity.par_iter_height_mut()) + .chain(self.reused.par_iter_height_mut()) + .chain(self.respent.par_iter_height_mut()) + .chain(self.exposed.par_iter_height_mut()) + } + + /// All height-indexed vecs including derived (`avg_amount`). Used for + /// bulk truncation, where derived vecs must follow the stateful ones. + pub(crate) fn par_iter_height_mut( + &mut self, + ) -> impl ParallelIterator { + self.funded + .par_iter_height_mut() + .chain(self.empty.par_iter_height_mut()) + .chain(self.activity.par_iter_height_mut()) + .chain(self.reused.par_iter_height_mut()) + .chain(self.respent.par_iter_height_mut()) + .chain(self.exposed.par_iter_height_mut()) + .chain(self.avg_amount.par_iter_height_mut()) + } + + /// Push one block's worth of per-addr-type running totals to all + /// height-indexed vecs. `active_addr_count` is the block-level total + /// of active addresses (sending + receiving - bidirectional). + #[inline(always)] + pub(crate) fn push_height(&mut self, state: &AddrMetricsState, active_addr_count: u32) { + self.funded.push_counts(&state.funded); + self.empty.push_counts(&state.empty); + self.activity.push_height(&state.activity); + self.exposed.push_height(&state.exposed); + self.reused.push_height(&state.reused, active_addr_count); + self.respent.push_height(&state.respent, active_addr_count); + } +} + #[derive(Traversable)] pub struct Vecs { #[traversable(skip)] @@ -162,9 +229,14 @@ impl Vecs { // Per-block delta of total (global + per-type) let new_addr_count = NewAddrCountVecs::forced_import(&db, version, indexes, cached_starts)?; - // Reused address tracking (counts + per-block uses + percent) + // Reused address tracking (counts + per-block uses + percent). + // `reused_*` uses the receive-side predicate (funded_txo_count > 1, + // industry standard). `respent_*` uses the spend-side counterpart + // (spent_txo_count > 1, strictly more restrictive). let reused_addr_count = - ReusedAddrVecs::forced_import(&db, version, indexes, cached_starts)?; + ReusedAddrVecs::forced_import(&db, "reused", version, indexes, cached_starts)?; + let respent_addr_count = + ReusedAddrVecs::forced_import(&db, "respent", version, indexes, cached_starts)?; // Exposed address tracking (counts + supply) - quantum / pubkey-exposure sense let exposed_addr_vecs = ExposedAddrVecs::forced_import(&db, version, indexes)?; @@ -188,6 +260,7 @@ impl Vecs { total: total_addr_count, new: new_addr_count, reused: reused_addr_count, + respent: respent_addr_count, exposed: exposed_addr_vecs, delta, avg_amount, @@ -303,12 +376,7 @@ impl Vecs { if needs_fresh_start { self.supply_state.reset()?; - self.addrs.funded.reset_height()?; - self.addrs.empty.reset_height()?; - self.addrs.activity.reset_height()?; - self.addrs.reused.reset_height()?; - self.addrs.exposed.reset_height()?; - self.addrs.avg_amount.reset_height()?; + self.addrs.reset_height()?; reset_state( &mut self.any_addr_indexes, &mut self.addrs_data, @@ -478,21 +546,34 @@ impl Vecs { // 6b. Compute address count sum (by addr_type -> all) self.addrs.funded.compute_rest(starting_indexes, exit)?; self.addrs.empty.compute_rest(starting_indexes, exit)?; - self.addrs.reused.compute_rest( - starting_indexes, - &outputs.by_type, - &inputs.by_type, - exit, - )?; let t = &self.utxo_cohorts.type_; let type_supply_sats = ByAddrType::new(|filter| { let Filter::Type(ot) = filter else { unreachable!() }; &t.get(ot).metrics.supply.total.sats.height }); + let all_supply_sats = &self.utxo_cohorts.all.metrics.supply.total.sats.height; + self.addrs.reused.compute_rest( + starting_indexes, + &outputs.by_type, + &inputs.by_type, + prices, + all_supply_sats, + &type_supply_sats, + exit, + )?; + self.addrs.respent.compute_rest( + starting_indexes, + &outputs.by_type, + &inputs.by_type, + prices, + all_supply_sats, + &type_supply_sats, + exit, + )?; self.addrs.exposed.compute_rest( starting_indexes, prices, - &self.utxo_cohorts.all.metrics.supply.total.sats.height, + all_supply_sats, &type_supply_sats, exit, )?; @@ -605,11 +686,7 @@ impl Vecs { .min(Height::from(self.supply_state.len())) .min(self.any_addr_indexes.min_stamped_len()) .min(self.addrs_data.min_stamped_len()) - .min(Height::from(self.addrs.funded.min_stateful_len())) - .min(Height::from(self.addrs.empty.min_stateful_len())) - .min(Height::from(self.addrs.activity.min_stateful_len())) - .min(Height::from(self.addrs.reused.min_stateful_len())) - .min(Height::from(self.addrs.exposed.min_stateful_len())) + .min(Height::from(self.addrs.min_stateful_len())) .min(Height::from(self.coinblocks_destroyed.block.len())) } } diff --git a/crates/brk_computer/src/lib.rs b/crates/brk_computer/src/lib.rs index 46e132da4..db7ee5bd1 100644 --- a/crates/brk_computer/src/lib.rs +++ b/crates/brk_computer/src/lib.rs @@ -266,10 +266,14 @@ impl Computer { } if let Some(name) = entry.file_name().to_str() + && !name.starts_with('_') && !EXPECTED_DBS.contains(&name) { info!("Removing obsolete database folder: {}", name); - fs::remove_dir_all(entry.path())?; + let path = entry.path(); + fs::remove_dir_all(&path).map_err(|e| { + std::io::Error::other(format!("remove_dir_all {path:?}: {e}")) + })?; } } diff --git a/crates/brk_mempool/src/block_builder/linearize/mod.rs b/crates/brk_mempool/src/block_builder/linearize/mod.rs index 410fb8d20..160b3d3db 100644 --- a/crates/brk_mempool/src/block_builder/linearize/mod.rs +++ b/crates/brk_mempool/src/block_builder/linearize/mod.rs @@ -45,13 +45,14 @@ pub fn linearize_clusters(graph: &Graph) -> Vec { let clusters = find_components(graph); let mut packages: Vec = Vec::with_capacity(clusters.len()); - for cluster in clusters { + for (cluster_id, cluster) in clusters.into_iter().enumerate() { + let cluster_id = cluster_id as u32; if cluster.nodes.len() == 1 { - packages.push(singleton_package(&cluster)); + packages.push(singleton_package(&cluster, cluster_id)); continue; } - for chunk in sfl::linearize(&cluster) { - packages.push(chunk_to_package(&cluster, &chunk)); + for (chunk_order, chunk) in sfl::linearize(&cluster).iter().enumerate() { + packages.push(chunk_to_package(&cluster, chunk, cluster_id, chunk_order as u32)); } } @@ -168,19 +169,24 @@ fn kahn_topo_rank(nodes: &[ClusterNode]) -> Vec { } /// Build a one-tx `Package` for a cluster of size 1. -fn singleton_package(cluster: &Cluster) -> Package { +fn singleton_package(cluster: &Cluster, cluster_id: u32) -> Package { let node = &cluster.nodes[0]; let fee_rate = FeeRate::from((node.fee, node.vsize)); - let mut package = Package::new(fee_rate); + let mut package = Package::new(fee_rate, cluster_id, 0); package.add_tx(node.tx_index, u64::from(node.vsize)); package } /// Convert an SFL-emitted chunk (set of local indices) into a `Package`. /// Txs inside the package are ordered parents-first by `topo_rank`. -fn chunk_to_package(cluster: &Cluster, chunk: &sfl::Chunk) -> Package { +fn chunk_to_package( + cluster: &Cluster, + chunk: &sfl::Chunk, + cluster_id: u32, + chunk_order: u32, +) -> Package { let fee_rate = FeeRate::from((Sats::from(chunk.fee), VSize::from(chunk.vsize))); - let mut package = Package::new(fee_rate); + let mut package = Package::new(fee_rate, cluster_id, chunk_order); let mut ordered: SmallVec<[LocalIdx; 8]> = chunk.nodes.iter().copied().collect(); ordered.sort_by_key(|&local| cluster.topo_rank[local as usize]); diff --git a/crates/brk_mempool/src/block_builder/linearize/sfl.rs b/crates/brk_mempool/src/block_builder/linearize/sfl.rs index 82ff0ea52..5e04eff54 100644 --- a/crates/brk_mempool/src/block_builder/linearize/sfl.rs +++ b/crates/brk_mempool/src/block_builder/linearize/sfl.rs @@ -35,7 +35,11 @@ pub fn linearize(cluster: &Cluster) -> Vec { if n == 0 { return Vec::new(); } - assert!(n <= BITMASK_LIMIT, "cluster size {} exceeds u128 capacity", n); + assert!( + n <= BITMASK_LIMIT, + "cluster size {} exceeds u128 capacity", + n + ); let mut parents_mask: Vec = vec![0; n]; let mut ancestor_incl: Vec = vec![0; n]; @@ -97,6 +101,7 @@ fn best_subset( best } +#[allow(clippy::too_many_arguments)] fn recurse( idx: usize, topo_order: &[LocalIdx], @@ -120,18 +125,34 @@ fn recurse( // Not in remaining, or a parent (within remaining) is excluded: // this node is forced-excluded, no branching. - if (bit & remaining) == 0 - || (parents_mask[node as usize] & remaining & !included) != 0 - { + if (bit & remaining) == 0 || (parents_mask[node as usize] & remaining & !included) != 0 { recurse( - idx + 1, topo_order, parents_mask, remaining, included, f, v, fee_of, vsize_of, best, + idx + 1, + topo_order, + parents_mask, + remaining, + included, + f, + v, + fee_of, + vsize_of, + best, ); return; } // Exclude recurse( - idx + 1, topo_order, parents_mask, remaining, included, f, v, fee_of, vsize_of, best, + idx + 1, + topo_order, + parents_mask, + remaining, + included, + f, + v, + fee_of, + vsize_of, + best, ); // Include recurse( diff --git a/crates/brk_mempool/src/block_builder/mod.rs b/crates/brk_mempool/src/block_builder/mod.rs index c2e4a09d8..ede7da011 100644 --- a/crates/brk_mempool/src/block_builder/mod.rs +++ b/crates/brk_mempool/src/block_builder/mod.rs @@ -9,7 +9,7 @@ pub use package::Package; use crate::entry::Entry; /// Target vsize per block (~1MB, derived from 4MW weight limit). -const BLOCK_VSIZE: u64 = 1_000_000; +pub(crate) const BLOCK_VSIZE: u64 = 1_000_000; /// Number of projected blocks to build (last one is a catch-all overflow). const NUM_BLOCKS: usize = 8; diff --git a/crates/brk_mempool/src/block_builder/package.rs b/crates/brk_mempool/src/block_builder/package.rs index 33714309a..9aeb1d0e2 100644 --- a/crates/brk_mempool/src/block_builder/package.rs +++ b/crates/brk_mempool/src/block_builder/package.rs @@ -9,19 +9,27 @@ use crate::types::TxIndex; /// i.e. what a miner collects per vsize when the package is mined. /// Packages are produced by SFL in descending-`fee_rate` order within a /// cluster and are atomic (all-or-nothing) at mining time. +/// +/// `cluster_id` + `chunk_order` let the partitioner enforce intra-cluster +/// ordering when its look-ahead would otherwise pull a child chunk into +/// an earlier block than its parent chunk. pub struct Package { /// Transactions in topological order (parents before children). pub txs: Vec, pub vsize: u64, pub fee_rate: FeeRate, + pub cluster_id: u32, + pub chunk_order: u32, } impl Package { - pub fn new(fee_rate: FeeRate) -> Self { + pub fn new(fee_rate: FeeRate, cluster_id: u32, chunk_order: u32) -> Self { Self { txs: Vec::new(), vsize: 0, fee_rate, + cluster_id, + chunk_order, } } diff --git a/crates/brk_mempool/src/block_builder/partitioner.rs b/crates/brk_mempool/src/block_builder/partitioner.rs index 0828fec00..3e37d17e4 100644 --- a/crates/brk_mempool/src/block_builder/partitioner.rs +++ b/crates/brk_mempool/src/block_builder/partitioner.rs @@ -11,21 +11,30 @@ const LOOK_AHEAD_COUNT: usize = 100; /// chunks. The final block is a catch-all containing every remaining /// package, so no low-rate tx is silently dropped from the projection /// (matches mempool.space's last-block behavior). +/// +/// Look-ahead respects intra-cluster order: a chunk is only taken once +/// every earlier-rate chunk of the same cluster has been placed, so a +/// child chunk never lands in an earlier block than its parent chunk. pub fn partition_into_blocks( mut packages: Vec, num_blocks: usize, ) -> Vec> { - // Stable sort for deterministic output across equal fee rates. SFL - // guarantees chunks within a cluster come in non-increasing rate - // order, so stable sorting by fee_rate preserves intra-cluster - // topology automatically. + // Stable sort preserves SFL's per-cluster non-increasing-rate emission + // order in the global list, which is what `cluster_next` relies on. packages.sort_by_key(|p| Reverse(p.fee_rate)); + let num_clusters = packages + .iter() + .map(|p| p.cluster_id as usize + 1) + .max() + .unwrap_or(0); + let mut cluster_next: Vec = vec![0; num_clusters]; + let mut slots: Vec> = packages.into_iter().map(Some).collect(); let mut blocks: Vec> = Vec::with_capacity(num_blocks); let normal_blocks = num_blocks.saturating_sub(1); - let mut idx = fill_normal_blocks(&mut slots, &mut blocks, normal_blocks); + let mut idx = fill_normal_blocks(&mut slots, &mut blocks, normal_blocks, &mut cluster_next); if blocks.len() < num_blocks { let mut overflow: Vec = Vec::new(); @@ -49,6 +58,7 @@ fn fill_normal_blocks( slots: &mut [Option], blocks: &mut Vec>, target_blocks: usize, + cluster_next: &mut [u32], ) -> usize { let mut current_block: Vec = Vec::new(); let mut current_vsize: u64 = 0; @@ -63,9 +73,7 @@ fn fill_normal_blocks( let remaining_space = BLOCK_VSIZE.saturating_sub(current_vsize); if pkg.vsize <= remaining_space { - let pkg = slots[idx].take().unwrap(); - current_vsize += pkg.vsize; - current_block.push(pkg); + take(slots, idx, &mut current_block, &mut current_vsize, cluster_next); idx += 1; continue; } @@ -73,9 +81,7 @@ fn fill_normal_blocks( if current_block.is_empty() { // Oversized package with no partial block to preserve; take it // anyway so we don't stall on a package larger than BLOCK_VSIZE. - let pkg = slots[idx].take().unwrap(); - current_vsize += pkg.vsize; - current_block.push(pkg); + take(slots, idx, &mut current_block, &mut current_vsize, cluster_next); idx += 1; continue; } @@ -86,6 +92,7 @@ fn fill_normal_blocks( remaining_space, &mut current_block, &mut current_vsize, + cluster_next, ) { continue; } @@ -102,23 +109,44 @@ fn fill_normal_blocks( } /// Scan the look-ahead window for a package small enough to fit in the -/// remaining space and move it into the current block. +/// remaining space, skipping any candidate whose cluster has an earlier +/// unplaced chunk (that chunk's parents would land after its children). fn try_fill_with_smaller( slots: &mut [Option], start: usize, remaining_space: u64, block: &mut Vec, block_vsize: &mut u64, + cluster_next: &mut [u32], ) -> bool { let end = (start + LOOK_AHEAD_COUNT).min(slots.len()); for idx in (start + 1)..end { let Some(pkg) = &slots[idx] else { continue }; - if pkg.vsize <= remaining_space { - let pkg = slots[idx].take().unwrap(); - *block_vsize += pkg.vsize; - block.push(pkg); - return true; + if pkg.vsize > remaining_space { + continue; } + if pkg.chunk_order != cluster_next[pkg.cluster_id as usize] { + continue; + } + take(slots, idx, block, block_vsize, cluster_next); + return true; } false } + +fn take( + slots: &mut [Option], + idx: usize, + block: &mut Vec, + block_vsize: &mut u64, + cluster_next: &mut [u32], +) { + let pkg = slots[idx].take().unwrap(); + debug_assert_eq!( + pkg.chunk_order, cluster_next[pkg.cluster_id as usize], + "partitioner took a chunk out of cluster order" + ); + cluster_next[pkg.cluster_id as usize] = pkg.chunk_order + 1; + *block_vsize += pkg.vsize; + block.push(pkg); +} diff --git a/crates/brk_mempool/src/projected_blocks/mod.rs b/crates/brk_mempool/src/projected_blocks/mod.rs index cd8e349ff..fa2b04ec9 100644 --- a/crates/brk_mempool/src/projected_blocks/mod.rs +++ b/crates/brk_mempool/src/projected_blocks/mod.rs @@ -1,6 +1,8 @@ mod fees; mod snapshot; mod stats; +#[cfg(debug_assertions)] +pub(crate) mod verify; pub use brk_types::RecommendedFees; pub use snapshot::Snapshot; diff --git a/crates/brk_mempool/src/projected_blocks/verify.rs b/crates/brk_mempool/src/projected_blocks/verify.rs new file mode 100644 index 000000000..008b9b4af --- /dev/null +++ b/crates/brk_mempool/src/projected_blocks/verify.rs @@ -0,0 +1,149 @@ +use brk_rpc::Client; +use brk_types::{Sats, SatsSigned, TxidPrefix}; +use rustc_hash::{FxHashMap, FxHashSet}; +use tracing::{debug, warn}; + +use crate::{ + block_builder::{BLOCK_VSIZE, Package}, + entry::Entry, + types::TxIndex, +}; + +type PrefixSet = FxHashSet; +type FeeByPrefix = FxHashMap; + +pub struct Verifier; + +impl Verifier { + pub fn check(client: &Client, blocks: &[Vec], entries: &[Option]) { + Self::check_structure(blocks, entries); + Self::compare_to_core(client, blocks, entries); + } + + fn check_structure(blocks: &[Vec], entries: &[Option]) { + let in_pool: PrefixSet = entries + .iter() + .filter_map(|e| e.as_ref().map(Entry::txid_prefix)) + .collect(); + let mut placed = PrefixSet::default(); + + for (b, block) in blocks.iter().enumerate() { + for (p, pkg) in block.iter().enumerate() { + let mut summed_vsize = 0u64; + for &tx_index in &pkg.txs { + let entry = Self::live_entry(entries, tx_index, b, p); + Self::assert_parents_placed_first(entry, &in_pool, &placed, b, p); + Self::place(entry, &mut placed, b, p); + summed_vsize += u64::from(entry.vsize); + } + assert_eq!( + pkg.vsize, summed_vsize, + "block {b} pkg {p}: pkg.vsize {} != sum {summed_vsize}", + pkg.vsize + ); + } + if b + 1 < blocks.len() { + Self::assert_block_fits_budget(block, b); + } + } + } + + fn live_entry<'e>( + entries: &'e [Option], + tx_index: TxIndex, + b: usize, + p: usize, + ) -> &'e Entry { + entries[tx_index.as_usize()] + .as_ref() + .unwrap_or_else(|| panic!("block {b} pkg {p}: dead tx_index {tx_index:?}")) + } + + fn assert_parents_placed_first( + entry: &Entry, + in_pool: &PrefixSet, + placed: &PrefixSet, + b: usize, + p: usize, + ) { + for parent in &entry.depends { + if in_pool.contains(parent) && !placed.contains(parent) { + panic!( + "block {b} pkg {p}: {} placed before its parent", + entry.txid + ); + } + } + } + + fn place(entry: &Entry, placed: &mut PrefixSet, b: usize, p: usize) { + assert!( + placed.insert(entry.txid_prefix()), + "block {b} pkg {p}: duplicate txid {}", + entry.txid + ); + } + + fn assert_block_fits_budget(block: &[Package], b: usize) { + let total: u64 = block.iter().map(|pkg| pkg.vsize).sum(); + let is_oversized_singleton = block.len() == 1 && total > BLOCK_VSIZE; + if is_oversized_singleton { + return; + } + assert!( + total <= BLOCK_VSIZE, + "block {b}: vsize {total} exceeds {BLOCK_VSIZE}" + ); + } + + fn compare_to_core(client: &Client, blocks: &[Vec], entries: &[Option]) { + let Some(next_block) = blocks.first() else { + return; + }; + let core: FeeByPrefix = match client.get_block_template_txs() { + Ok(txs) => txs + .into_iter() + .map(|t| (TxidPrefix::from(&t.txid), t.fee)) + .collect(), + Err(e) => { + warn!("verify: getblocktemplate failed: {e}"); + return; + } + }; + let ours: FeeByPrefix = next_block + .iter() + .flat_map(|pkg| &pkg.txs) + .filter_map(|&i| entries[i.as_usize()].as_ref()) + .map(|e| (e.txid_prefix(), e.fee)) + .collect(); + + let overlap = ours.keys().filter(|k| core.contains_key(k)).count(); + let union = ours.len() + core.len() - overlap; + let jaccard = if union == 0 { + 1.0 + } else { + overlap as f64 / union as f64 + }; + + let ours_fee: Sats = ours.values().copied().sum(); + let core_fee: Sats = core.values().copied().sum(); + let delta = SatsSigned::from(ours_fee) - SatsSigned::from(core_fee); + let delta_bps = if core_fee == Sats::ZERO { + 0.0 + } else { + f64::from(delta) / f64::from(core_fee) * 10_000.0 + }; + + debug!( + "verify block 0: txs {}/{} (overlap {}, jaccard {:.3}) | fee {}/{} (delta {:+}, {:+.1} bps)", + ours.len(), + core.len(), + overlap, + jaccard, + ours_fee, + core_fee, + delta.inner(), + delta_bps, + ); + } +} diff --git a/crates/brk_mempool/src/sync.rs b/crates/brk_mempool/src/sync.rs index 85c02e56c..cac795264 100644 --- a/crates/brk_mempool/src/sync.rs +++ b/crates/brk_mempool/src/sync.rs @@ -343,6 +343,10 @@ impl MempoolInner { let entries_slice = entries.entries(); let blocks = build_projected_blocks(entries_slice); + + #[cfg(debug_assertions)] + crate::projected_blocks::verify::Verifier::check(&self.client, &blocks, entries_slice); + let snapshot = Snapshot::build(blocks, entries_slice); *self.snapshot.write() = snapshot; diff --git a/crates/brk_query/src/impl/urpd.rs b/crates/brk_query/src/impl/urpd.rs index 32c3625de..f4afbd3f4 100644 --- a/crates/brk_query/src/impl/urpd.rs +++ b/crates/brk_query/src/impl/urpd.rs @@ -22,7 +22,8 @@ impl Query { }) .collect(); - cohorts.sort_by(|a, b| a.to_string().cmp(&b.to_string())); + cohorts.sort_by_key(|a| a.to_string()); + Ok(cohorts) } @@ -76,12 +77,7 @@ impl Query { } /// URPD for a cohort on a specific date. - pub fn urpd_at( - &self, - cohort: &Cohort, - date: Date, - agg: UrpdAggregation, - ) -> Result { + pub fn urpd_at(&self, cohort: &Cohort, date: Date, agg: UrpdAggregation) -> Result { let raw = self.urpd_raw(cohort, date)?; let day1 = Day1::try_from(date).map_err(|e| Error::Parse(e.to_string()))?; let close = self @@ -99,9 +95,9 @@ impl Query { /// URPD for the most recently available date in a cohort. pub fn urpd_latest(&self, cohort: &Cohort, agg: UrpdAggregation) -> Result { let dates = self.urpd_dates(cohort)?; - let date = *dates.last().ok_or_else(|| { - Error::NotFound(format!("No URPD available for cohort '{cohort}'")) - })?; + let date = *dates + .last() + .ok_or_else(|| Error::NotFound(format!("No URPD available for cohort '{cohort}'")))?; self.urpd_at(cohort, date, agg) } } diff --git a/crates/brk_rpc/Cargo.toml b/crates/brk_rpc/Cargo.toml index e054c05e1..ab7929d89 100644 --- a/crates/brk_rpc/Cargo.toml +++ b/crates/brk_rpc/Cargo.toml @@ -10,7 +10,7 @@ exclude = ["examples/"] [features] default = ["corepc"] -bitcoincore-rpc = ["dep:bitcoincore-rpc", "brk_error/bitcoincore-rpc"] +bitcoincore-rpc = ["dep:bitcoincore-rpc", "dep:serde_json", "brk_error/bitcoincore-rpc"] corepc = ["dep:corepc-client", "dep:corepc-jsonrpc", "dep:serde_json", "dep:serde", "brk_error/corepc"] [dependencies] diff --git a/crates/brk_rpc/src/backend/bitcoincore.rs b/crates/brk_rpc/src/backend/bitcoincore.rs index 0a3784b32..1c9c3ce8f 100644 --- a/crates/brk_rpc/src/backend/bitcoincore.rs +++ b/crates/brk_rpc/src/backend/bitcoincore.rs @@ -1,13 +1,19 @@ use std::{thread::sleep, time::Duration}; -use bitcoincore_rpc::{Client as CoreClient, Error as RpcError, RpcApi, jsonrpc}; +use bitcoincore_rpc::{ + Client as CoreClient, Error as RpcError, RpcApi, + json::{GetBlockTemplateCapabilities, GetBlockTemplateModes, GetBlockTemplateRules}, + jsonrpc, +}; use brk_error::{Error, Result}; -use brk_types::Sats; +use brk_types::{Sats, Txid}; use parking_lot::RwLock; use serde_json::value::RawValue; use tracing::info; -use super::{Auth, BlockHeaderInfo, BlockInfo, BlockchainInfo, RawMempoolEntry, TxOutInfo}; +use super::{ + Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, RawMempoolEntry, TxOutInfo, +}; /// Per-batch request count for `get_block_hashes_range`. Sized so the /// JSON request body stays well under a megabyte and bitcoind doesn't @@ -310,4 +316,23 @@ impl ClientInner { pub fn send_raw_transaction(&self, hex: &str) -> Result { Ok(self.call_once(|c| c.send_raw_transaction(hex))?) } + + /// Transactions Bitcoin Core would include in the next block it would + /// mine. Core requires the `segwit` rule to be declared. + pub fn get_block_template_txs(&self) -> Result> { + let r = self.call_with_retry(|c| { + c.get_block_template( + GetBlockTemplateModes::Template, + &[GetBlockTemplateRules::SegWit], + &[] as &[GetBlockTemplateCapabilities], + ) + })?; + Ok(r.transactions + .into_iter() + .map(|t| BlockTemplateTx { + txid: Txid::from(t.txid), + fee: Sats::from(t.fee.to_sat()), + }) + .collect()) + } } diff --git a/crates/brk_rpc/src/backend/corepc.rs b/crates/brk_rpc/src/backend/corepc.rs index 399dedf65..50edf59f9 100644 --- a/crates/brk_rpc/src/backend/corepc.rs +++ b/crates/brk_rpc/src/backend/corepc.rs @@ -1,13 +1,15 @@ use std::{thread::sleep, time::Duration}; use brk_error::{Error, Result}; -use brk_types::Sats; +use brk_types::{Sats, Txid}; use corepc_client::client_sync::Auth as CorepcAuth; use parking_lot::RwLock; use serde_json::value::RawValue; use tracing::info; -use super::{Auth, BlockHeaderInfo, BlockInfo, BlockchainInfo, RawMempoolEntry, TxOutInfo}; +use super::{ + Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, RawMempoolEntry, TxOutInfo, +}; type CoreClient = corepc_client::client_sync::v30::Client; type CoreError = corepc_client::client_sync::Error; @@ -186,11 +188,7 @@ impl ClientInner { /// a 50 MB request body or hold every response in memory at once. /// /// Returns hashes in canonical order (`start`, `start+1`, …, `end`). - pub fn get_block_hashes_range( - &self, - start: u64, - end: u64, - ) -> Result> { + pub fn get_block_hashes_range(&self, start: u64, end: u64) -> Result> { if end < start { return Ok(Vec::new()); } @@ -370,6 +368,22 @@ impl ClientInner { c.call("sendrawtransaction", &args) })?) } + + /// Transactions Bitcoin Core would include in the next block it would + /// mine. Core requires the `segwit` rule to be declared. + pub fn get_block_template_txs(&self) -> Result> { + let args = [serde_json::json!({ "rules": ["segwit"] })]; + let r: GetBlockTemplateResponse = + self.call_with_retry(|c| c.call("getblocktemplate", &args))?; + + Ok(r.transactions + .into_iter() + .map(|t| BlockTemplateTx { + txid: Txid::from(t.txid), + fee: Sats::from(t.fee), + }) + .collect()) + } } // Local deserialization structs for raw RPC responses @@ -386,3 +400,14 @@ struct TxOutResponse { struct TxOutScriptPubKey { hex: String, } + +#[derive(serde::Deserialize)] +struct GetBlockTemplateResponse { + transactions: Vec, +} + +#[derive(serde::Deserialize)] +struct GetBlockTemplateTx { + txid: bitcoin::Txid, + fee: u64, +} diff --git a/crates/brk_rpc/src/backend/mod.rs b/crates/brk_rpc/src/backend/mod.rs index 71eac244e..80939ae87 100644 --- a/crates/brk_rpc/src/backend/mod.rs +++ b/crates/brk_rpc/src/backend/mod.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use bitcoin::ScriptBuf; -use brk_types::Sats; +use brk_types::{Sats, Txid}; #[derive(Debug, Clone)] pub struct BlockchainInfo { @@ -29,6 +29,12 @@ pub struct TxOutInfo { pub script_pub_key: ScriptBuf, } +#[derive(Debug, Clone)] +pub struct BlockTemplateTx { + pub txid: Txid, + pub fee: Sats, +} + #[derive(Debug, Clone)] pub struct RawMempoolEntry { pub vsize: u64, diff --git a/crates/brk_rpc/src/lib.rs b/crates/brk_rpc/src/lib.rs index 59eae9da5..c12052a85 100644 --- a/crates/brk_rpc/src/lib.rs +++ b/crates/brk_rpc/src/lib.rs @@ -12,7 +12,7 @@ use brk_types::{BlockHash, Height, MempoolEntryInfo, Sats, Txid, Vout}; pub mod backend; -pub use backend::{Auth, BlockHeaderInfo, BlockInfo, BlockchainInfo, TxOutInfo}; +pub use backend::{Auth, BlockHeaderInfo, BlockInfo, BlockTemplateTx, BlockchainInfo, TxOutInfo}; use backend::ClientInner; use tracing::{debug, info}; @@ -201,6 +201,12 @@ impl Client { self.0.send_raw_transaction(hex).map(Txid::from) } + /// Transactions (txid + fee) Bitcoin Core would include in the next + /// block it would mine, via `getblocktemplate`. + pub fn get_block_template_txs(&self) -> Result> { + self.0.get_block_template_txs() + } + /// Checks if a block is in the main chain (has positive confirmations) pub fn is_in_main_chain(&self, hash: &BlockHash) -> Result { let block_info = self.get_block_info(hash)?; diff --git a/crates/brk_server/src/api/scalar.html b/crates/brk_server/src/api/scalar.html index 73a1a2034..d4cb11125 100644 --- a/crates/brk_server/src/api/scalar.html +++ b/crates/brk_server/src/api/scalar.html @@ -17,7 +17,7 @@ @@ -29,7 +29,7 @@ /> diff --git a/crates/brk_types/src/funded_addr_data.rs b/crates/brk_types/src/funded_addr_data.rs index 914f2da84..08a5102da 100644 --- a/crates/brk_types/src/funded_addr_data.rs +++ b/crates/brk_types/src/funded_addr_data.rs @@ -111,14 +111,25 @@ impl FundedAddrData { } /// Whether this address has received more than one output over its - /// lifetime — the simplest proxy for address reuse (close to but not - /// exactly "received in 2+ distinct transactions"; over-counts the rare - /// case of multi-output funding to the same address in one tx). + /// lifetime: the receive-side proxy for address reuse (close to but + /// not exactly "received in 2+ distinct transactions"; over-counts + /// the rare case of multi-output funding to the same address in one + /// tx). Matches the industry-standard "address reuse" signal. #[inline] pub fn is_reused(&self) -> bool { self.funded_txo_count > 1 } + /// Whether this address has spent more than one output over its + /// lifetime: the spend-side counterpart to `is_reused`. Captures + /// "demonstrated reuse via actual spending" and excludes addresses + /// that received multiple outputs but have not yet been drawn from + /// more than once. + #[inline] + pub fn is_respent(&self) -> bool { + self.spent_txo_count > 1 + } + /// Whether this address's public key has been revealed in the chain. /// For P2PK33/P2PK65/P2TR the pubkey is in the locking script of any /// funding output; for other types it's only revealed when spending. @@ -145,6 +156,28 @@ impl FundedAddrData { } } + /// This address's contribution (in sats) to the funded-reused supply: + /// its balance if currently funded AND reused (received ≥ 2), else 0. + #[inline] + pub fn reused_supply_contribution(&self) -> Sats { + if self.is_funded() && self.is_reused() { + self.balance() + } else { + Sats::ZERO + } + } + + /// This address's contribution (in sats) to the funded-respent supply: + /// its balance if currently funded AND respent (spent ≥ 2), else 0. + #[inline] + pub fn respent_supply_contribution(&self) -> Sats { + if self.is_funded() && self.is_respent() { + self.balance() + } else { + Sats::ZERO + } + } + pub fn receive(&mut self, amount: Sats, price: Cents) { self.receive_outputs(amount, price, 1); } diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 4df95faea..cfbca20d0 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -2303,6 +2303,20 @@ function createAverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern(c }; } +/** + * @typedef {Object} AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern + * @property {BtcCentsSatsUsdPattern} all + * @property {BtcCentsSatsUsdPattern} p2a + * @property {BtcCentsSatsUsdPattern} p2pk33 + * @property {BtcCentsSatsUsdPattern} p2pk65 + * @property {BtcCentsSatsUsdPattern} p2pkh + * @property {BtcCentsSatsUsdPattern} p2sh + * @property {BtcCentsSatsUsdPattern} p2tr + * @property {BtcCentsSatsUsdPattern} p2wpkh + * @property {BtcCentsSatsUsdPattern} p2wsh + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} share + */ + /** * @typedef {Object} IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern * @property {SeriesPattern1} index @@ -2371,6 +2385,39 @@ function createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, acc) { }; } +/** + * @typedef {Object} AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 + * @property {BpsPercentRatioPattern2} all + * @property {BpsPercentRatioPattern2} p2a + * @property {BpsPercentRatioPattern2} p2pk33 + * @property {BpsPercentRatioPattern2} p2pk65 + * @property {BpsPercentRatioPattern2} p2pkh + * @property {BpsPercentRatioPattern2} p2sh + * @property {BpsPercentRatioPattern2} p2tr + * @property {BpsPercentRatioPattern2} p2wpkh + * @property {BpsPercentRatioPattern2} p2wsh + */ + +/** + * Create a AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated series name + * @returns {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} + */ +function createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(client, acc) { + return { + all: createBpsPercentRatioPattern2(client, acc), + p2a: createBpsPercentRatioPattern2(client, _p('p2a', acc)), + p2pk33: createBpsPercentRatioPattern2(client, _p('p2pk33', acc)), + p2pk65: createBpsPercentRatioPattern2(client, _p('p2pk65', acc)), + p2pkh: createBpsPercentRatioPattern2(client, _p('p2pkh', acc)), + p2sh: createBpsPercentRatioPattern2(client, _p('p2sh', acc)), + p2tr: createBpsPercentRatioPattern2(client, _p('p2tr', acc)), + p2wpkh: createBpsPercentRatioPattern2(client, _p('p2wpkh', acc)), + p2wsh: createBpsPercentRatioPattern2(client, _p('p2wsh', acc)), + }; +} + /** * @typedef {Object} AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4 * @property {SeriesPattern1} all @@ -2604,6 +2651,17 @@ function create_1m1w1y24hBpsPercentRatioPattern(client, acc) { }; } +/** + * @typedef {Object} ActiveInputOutputSpendablePattern + * @property {_1m1w1y24hBlockPattern} activeReusedAddrCount + * @property {_1m1w1y24hBlockPattern2} activeReusedAddrShare + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} inputFromReusedAddrCount + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} inputFromReusedAddrShare + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} outputToReusedAddrCount + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} outputToReusedAddrShare + * @property {_1m1w1y24hBpsPercentRatioPattern} spendableOutputToReusedAddrShare + */ + /** * @typedef {Object} CapLossMvrvNetPriceProfitSoprPattern * @property {CentsDeltaUsdPattern} cap @@ -2911,6 +2969,31 @@ function createDeltaDominanceHalfInTotalPattern(client, acc) { }; } +/** + * @typedef {Object} _1m1w1y24hBlockPattern2 + * @property {SeriesPattern1} _1m + * @property {SeriesPattern1} _1w + * @property {SeriesPattern1} _1y + * @property {SeriesPattern1} _24h + * @property {SeriesPattern18} block + */ + +/** + * Create a _1m1w1y24hBlockPattern2 pattern node + * @param {BrkClientBase} client + * @param {string} acc - Accumulated series name + * @returns {_1m1w1y24hBlockPattern2} + */ +function create_1m1w1y24hBlockPattern2(client, acc) { + return { + _1m: createSeriesPattern1(client, _m(acc, 'average_1m')), + _1w: createSeriesPattern1(client, _m(acc, 'average_1w')), + _1y: createSeriesPattern1(client, _m(acc, 'average_1y')), + _24h: createSeriesPattern1(client, _m(acc, 'average_24h')), + block: createSeriesPattern18(client, acc), + }; +} + /** * @typedef {Object} _1m1w1y24hBlockPattern * @property {SeriesPattern1} _1m @@ -3991,6 +4074,13 @@ function createCentsSatsUsdPattern(client, acc) { }; } +/** + * @typedef {Object} CountEventsSupplyPattern + * @property {FundedTotalPattern} count + * @property {ActiveInputOutputSpendablePattern} events + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern} supply + */ + /** * @typedef {Object} CumulativeRollingSumPattern * @property {SeriesPattern1} cumulative @@ -5113,6 +5203,7 @@ function createTransferPattern(client, acc) { * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4} total * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} new * @property {SeriesTree_Addrs_Reused} reused + * @property {SeriesTree_Addrs_Respent} respent * @property {SeriesTree_Addrs_Exposed} exposed * @property {SeriesTree_Addrs_Delta} delta * @property {SeriesTree_Addrs_AvgAmount} avgAmount @@ -5224,6 +5315,7 @@ function createTransferPattern(client, acc) { * @typedef {Object} SeriesTree_Addrs_Reused * @property {FundedTotalPattern} count * @property {SeriesTree_Addrs_Reused_Events} events + * @property {SeriesTree_Addrs_Reused_Supply} supply */ /** @@ -5234,16 +5326,53 @@ function createTransferPattern(client, acc) { * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} inputFromReusedAddrCount * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} inputFromReusedAddrShare * @property {_1m1w1y24hBlockPattern} activeReusedAddrCount - * @property {SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare} activeReusedAddrShare + * @property {_1m1w1y24hBlockPattern2} activeReusedAddrShare */ /** - * @typedef {Object} SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare - * @property {SeriesPattern18} block - * @property {SeriesPattern1} _24h - * @property {SeriesPattern1} _1w - * @property {SeriesPattern1} _1m - * @property {SeriesPattern1} _1y + * @typedef {Object} SeriesTree_Addrs_Reused_Supply + * @property {BtcCentsSatsUsdPattern} all + * @property {BtcCentsSatsUsdPattern} p2pk65 + * @property {BtcCentsSatsUsdPattern} p2pk33 + * @property {BtcCentsSatsUsdPattern} p2pkh + * @property {BtcCentsSatsUsdPattern} p2sh + * @property {BtcCentsSatsUsdPattern} p2wpkh + * @property {BtcCentsSatsUsdPattern} p2wsh + * @property {BtcCentsSatsUsdPattern} p2tr + * @property {BtcCentsSatsUsdPattern} p2a + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} share + */ + +/** + * @typedef {Object} SeriesTree_Addrs_Respent + * @property {FundedTotalPattern} count + * @property {SeriesTree_Addrs_Respent_Events} events + * @property {SeriesTree_Addrs_Respent_Supply} supply + */ + +/** + * @typedef {Object} SeriesTree_Addrs_Respent_Events + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} outputToReusedAddrCount + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} outputToReusedAddrShare + * @property {_1m1w1y24hBpsPercentRatioPattern} spendableOutputToReusedAddrShare + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6} inputFromReusedAddrCount + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7} inputFromReusedAddrShare + * @property {_1m1w1y24hBlockPattern} activeReusedAddrCount + * @property {_1m1w1y24hBlockPattern2} activeReusedAddrShare + */ + +/** + * @typedef {Object} SeriesTree_Addrs_Respent_Supply + * @property {BtcCentsSatsUsdPattern} all + * @property {BtcCentsSatsUsdPattern} p2pk65 + * @property {BtcCentsSatsUsdPattern} p2pk33 + * @property {BtcCentsSatsUsdPattern} p2pkh + * @property {BtcCentsSatsUsdPattern} p2sh + * @property {BtcCentsSatsUsdPattern} p2wpkh + * @property {BtcCentsSatsUsdPattern} p2wsh + * @property {BtcCentsSatsUsdPattern} p2tr + * @property {BtcCentsSatsUsdPattern} p2a + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} share */ /** @@ -5263,20 +5392,7 @@ function createTransferPattern(client, acc) { * @property {BtcCentsSatsUsdPattern} p2wsh * @property {BtcCentsSatsUsdPattern} p2tr * @property {BtcCentsSatsUsdPattern} p2a - * @property {SeriesTree_Addrs_Exposed_Supply_Share} share - */ - -/** - * @typedef {Object} SeriesTree_Addrs_Exposed_Supply_Share - * @property {BpsPercentRatioPattern2} all - * @property {BpsPercentRatioPattern2} p2pk65 - * @property {BpsPercentRatioPattern2} p2pk33 - * @property {BpsPercentRatioPattern2} p2pkh - * @property {BpsPercentRatioPattern2} p2sh - * @property {BpsPercentRatioPattern2} p2wpkh - * @property {BpsPercentRatioPattern2} p2wsh - * @property {BpsPercentRatioPattern2} p2tr - * @property {BpsPercentRatioPattern2} p2a + * @property {AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5} share */ /** @@ -8631,38 +8747,58 @@ class BrkClient extends BrkClientBase { inputFromReusedAddrCount: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(this, 'input_from_reused_addr_count'), inputFromReusedAddrShare: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(this, 'input_from_reused_addr_share'), activeReusedAddrCount: create_1m1w1y24hBlockPattern(this, 'active_reused_addr_count'), - activeReusedAddrShare: { - block: createSeriesPattern18(this, 'active_reused_addr_share'), - _24h: createSeriesPattern1(this, 'active_reused_addr_share_average_24h'), - _1w: createSeriesPattern1(this, 'active_reused_addr_share_average_1w'), - _1m: createSeriesPattern1(this, 'active_reused_addr_share_average_1m'), - _1y: createSeriesPattern1(this, 'active_reused_addr_share_average_1y'), - }, + activeReusedAddrShare: create_1m1w1y24hBlockPattern2(this, 'active_reused_addr_share'), + }, + supply: { + all: createBtcCentsSatsUsdPattern(this, 'reused_addr_supply'), + p2pk65: createBtcCentsSatsUsdPattern(this, 'p2pk65_reused_addr_supply'), + p2pk33: createBtcCentsSatsUsdPattern(this, 'p2pk33_reused_addr_supply'), + p2pkh: createBtcCentsSatsUsdPattern(this, 'p2pkh_reused_addr_supply'), + p2sh: createBtcCentsSatsUsdPattern(this, 'p2sh_reused_addr_supply'), + p2wpkh: createBtcCentsSatsUsdPattern(this, 'p2wpkh_reused_addr_supply'), + p2wsh: createBtcCentsSatsUsdPattern(this, 'p2wsh_reused_addr_supply'), + p2tr: createBtcCentsSatsUsdPattern(this, 'p2tr_reused_addr_supply'), + p2a: createBtcCentsSatsUsdPattern(this, 'p2a_reused_addr_supply'), + share: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(this, 'reused_addr_supply_share'), + }, + }, + respent: { + count: createFundedTotalPattern(this, 'respent_addr_count'), + events: { + outputToReusedAddrCount: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(this, 'output_to_respent_addr_count'), + outputToReusedAddrShare: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(this, 'output_to_respent_addr_share'), + spendableOutputToReusedAddrShare: create_1m1w1y24hBpsPercentRatioPattern(this, 'spendable_output_to_respent_addr_share'), + inputFromReusedAddrCount: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(this, 'input_from_respent_addr_count'), + inputFromReusedAddrShare: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(this, 'input_from_respent_addr_share'), + activeReusedAddrCount: create_1m1w1y24hBlockPattern(this, 'active_respent_addr_count'), + activeReusedAddrShare: create_1m1w1y24hBlockPattern2(this, 'active_respent_addr_share'), + }, + supply: { + all: createBtcCentsSatsUsdPattern(this, 'respent_addr_supply'), + p2pk65: createBtcCentsSatsUsdPattern(this, 'p2pk65_respent_addr_supply'), + p2pk33: createBtcCentsSatsUsdPattern(this, 'p2pk33_respent_addr_supply'), + p2pkh: createBtcCentsSatsUsdPattern(this, 'p2pkh_respent_addr_supply'), + p2sh: createBtcCentsSatsUsdPattern(this, 'p2sh_respent_addr_supply'), + p2wpkh: createBtcCentsSatsUsdPattern(this, 'p2wpkh_respent_addr_supply'), + p2wsh: createBtcCentsSatsUsdPattern(this, 'p2wsh_respent_addr_supply'), + p2tr: createBtcCentsSatsUsdPattern(this, 'p2tr_respent_addr_supply'), + p2a: createBtcCentsSatsUsdPattern(this, 'p2a_respent_addr_supply'), + share: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(this, 'respent_addr_supply_share'), }, }, exposed: { count: createFundedTotalPattern(this, 'exposed_addr_count'), supply: { - all: createBtcCentsSatsUsdPattern(this, 'exposed_supply'), - p2pk65: createBtcCentsSatsUsdPattern(this, 'p2pk65_exposed_supply'), - p2pk33: createBtcCentsSatsUsdPattern(this, 'p2pk33_exposed_supply'), - p2pkh: createBtcCentsSatsUsdPattern(this, 'p2pkh_exposed_supply'), - p2sh: createBtcCentsSatsUsdPattern(this, 'p2sh_exposed_supply'), - p2wpkh: createBtcCentsSatsUsdPattern(this, 'p2wpkh_exposed_supply'), - p2wsh: createBtcCentsSatsUsdPattern(this, 'p2wsh_exposed_supply'), - p2tr: createBtcCentsSatsUsdPattern(this, 'p2tr_exposed_supply'), - p2a: createBtcCentsSatsUsdPattern(this, 'p2a_exposed_supply'), - share: { - all: createBpsPercentRatioPattern2(this, 'exposed_supply_share'), - p2pk65: createBpsPercentRatioPattern2(this, 'p2pk65_exposed_supply_share'), - p2pk33: createBpsPercentRatioPattern2(this, 'p2pk33_exposed_supply_share'), - p2pkh: createBpsPercentRatioPattern2(this, 'p2pkh_exposed_supply_share'), - p2sh: createBpsPercentRatioPattern2(this, 'p2sh_exposed_supply_share'), - p2wpkh: createBpsPercentRatioPattern2(this, 'p2wpkh_exposed_supply_share'), - p2wsh: createBpsPercentRatioPattern2(this, 'p2wsh_exposed_supply_share'), - p2tr: createBpsPercentRatioPattern2(this, 'p2tr_exposed_supply_share'), - p2a: createBpsPercentRatioPattern2(this, 'p2a_exposed_supply_share'), - }, + all: createBtcCentsSatsUsdPattern(this, 'exposed_addr_supply'), + p2pk65: createBtcCentsSatsUsdPattern(this, 'p2pk65_exposed_addr_supply'), + p2pk33: createBtcCentsSatsUsdPattern(this, 'p2pk33_exposed_addr_supply'), + p2pkh: createBtcCentsSatsUsdPattern(this, 'p2pkh_exposed_addr_supply'), + p2sh: createBtcCentsSatsUsdPattern(this, 'p2sh_exposed_addr_supply'), + p2wpkh: createBtcCentsSatsUsdPattern(this, 'p2wpkh_exposed_addr_supply'), + p2wsh: createBtcCentsSatsUsdPattern(this, 'p2wsh_exposed_addr_supply'), + p2tr: createBtcCentsSatsUsdPattern(this, 'p2tr_exposed_addr_supply'), + p2a: createBtcCentsSatsUsdPattern(this, 'p2a_exposed_addr_supply'), + share: createAllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(this, 'exposed_addr_supply_share'), }, }, delta: { diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index caa97d673..0e6d2a12f 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -2767,6 +2767,10 @@ class AverageBaseCumulativeMaxMedianMinPct10Pct25Pct75Pct90SumPattern(Generic[T] self.pct90: _1m1w1y24hPattern[T] = _1m1w1y24hPattern(client, _m(acc, 'pct90')) self.sum: _1m1w1y24hPattern[T] = _1m1w1y24hPattern(client, _m(acc, 'sum')) +class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshSharePattern: + """Pattern struct for repeated tree structure.""" + pass + class IndexPct0Pct1Pct2Pct5Pct95Pct98Pct99ScorePattern: """Pattern struct for repeated tree structure.""" @@ -2798,6 +2802,21 @@ class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6: self.p2wpkh: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _p('p2wpkh', acc)) self.p2wsh: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _p('p2wsh', acc)) +class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated series name.""" + self.all: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, acc) + self.p2a: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2a', acc)) + self.p2pk33: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2pk33', acc)) + self.p2pk65: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2pk65', acc)) + self.p2pkh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2pkh', acc)) + self.p2sh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2sh', acc)) + self.p2tr: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2tr', acc)) + self.p2wpkh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2wpkh', acc)) + self.p2wsh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _p('p2wsh', acc)) + class AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4: """Pattern struct for repeated tree structure.""" @@ -2902,6 +2921,10 @@ class _1m1w1y24hBpsPercentRatioPattern: self.percent: SeriesPattern1[StoredF32] = SeriesPattern1(client, acc) self.ratio: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'ratio')) +class ActiveInputOutputSpendablePattern: + """Pattern struct for repeated tree structure.""" + pass + class CapLossMvrvNetPriceProfitSoprPattern: """Pattern struct for repeated tree structure.""" @@ -3038,6 +3061,17 @@ class DeltaDominanceHalfInTotalPattern: self.in_profit: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, _m(acc, 'in_profit')) self.total: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, acc) +class _1m1w1y24hBlockPattern2: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClientBase, acc: str): + """Create pattern node with accumulated series name.""" + self._1m: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'average_1m')) + self._1w: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'average_1w')) + self._1y: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'average_1y')) + self._24h: SeriesPattern1[StoredF32] = SeriesPattern1(client, _m(acc, 'average_24h')) + self.block: SeriesPattern18[StoredF32] = SeriesPattern18(client, acc) + class _1m1w1y24hBlockPattern: """Pattern struct for repeated tree structure.""" @@ -3506,6 +3540,10 @@ class CentsSatsUsdPattern: self.sats: SeriesPattern1[SatsFract] = SeriesPattern1(client, _m(acc, 'sats')) self.usd: SeriesPattern1[Dollars] = SeriesPattern1(client, acc) +class CountEventsSupplyPattern: + """Pattern struct for repeated tree structure.""" + pass + class CumulativeRollingSumPattern: """Pattern struct for repeated tree structure.""" @@ -4316,16 +4354,6 @@ class SeriesTree_Addrs_Activity: self.p2tr: ActiveBidirectionalReactivatedReceivingSendingPattern = ActiveBidirectionalReactivatedReceivingSendingPattern(client, 'p2tr') self.p2a: ActiveBidirectionalReactivatedReceivingSendingPattern = ActiveBidirectionalReactivatedReceivingSendingPattern(client, 'p2a') -class SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare: - """Series tree node.""" - - def __init__(self, client: BrkClientBase, base_path: str = ''): - self.block: SeriesPattern18[StoredF32] = SeriesPattern18(client, 'active_reused_addr_share') - self._24h: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'active_reused_addr_share_average_24h') - self._1w: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'active_reused_addr_share_average_1w') - self._1m: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'active_reused_addr_share_average_1m') - self._1y: SeriesPattern1[StoredF32] = SeriesPattern1(client, 'active_reused_addr_share_average_1y') - class SeriesTree_Addrs_Reused_Events: """Series tree node.""" @@ -4336,7 +4364,22 @@ class SeriesTree_Addrs_Reused_Events: self.input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, 'input_from_reused_addr_count') self.input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(client, 'input_from_reused_addr_share') self.active_reused_addr_count: _1m1w1y24hBlockPattern = _1m1w1y24hBlockPattern(client, 'active_reused_addr_count') - self.active_reused_addr_share: SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare = SeriesTree_Addrs_Reused_Events_ActiveReusedAddrShare(client) + self.active_reused_addr_share: _1m1w1y24hBlockPattern2 = _1m1w1y24hBlockPattern2(client, 'active_reused_addr_share') + +class SeriesTree_Addrs_Reused_Supply: + """Series tree node.""" + + def __init__(self, client: BrkClientBase, base_path: str = ''): + self.all: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'reused_addr_supply') + self.p2pk65: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk65_reused_addr_supply') + self.p2pk33: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk33_reused_addr_supply') + self.p2pkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pkh_reused_addr_supply') + self.p2sh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2sh_reused_addr_supply') + self.p2wpkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wpkh_reused_addr_supply') + self.p2wsh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wsh_reused_addr_supply') + self.p2tr: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2tr_reused_addr_supply') + self.p2a: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2a_reused_addr_supply') + self.share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(client, 'reused_addr_supply_share') class SeriesTree_Addrs_Reused: """Series tree node.""" @@ -4344,35 +4387,57 @@ class SeriesTree_Addrs_Reused: def __init__(self, client: BrkClientBase, base_path: str = ''): self.count: FundedTotalPattern = FundedTotalPattern(client, 'reused_addr_count') self.events: SeriesTree_Addrs_Reused_Events = SeriesTree_Addrs_Reused_Events(client) + self.supply: SeriesTree_Addrs_Reused_Supply = SeriesTree_Addrs_Reused_Supply(client) -class SeriesTree_Addrs_Exposed_Supply_Share: +class SeriesTree_Addrs_Respent_Events: """Series tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.all: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'exposed_supply_share') - self.p2pk65: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2pk65_exposed_supply_share') - self.p2pk33: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2pk33_exposed_supply_share') - self.p2pkh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2pkh_exposed_supply_share') - self.p2sh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2sh_exposed_supply_share') - self.p2wpkh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2wpkh_exposed_supply_share') - self.p2wsh: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2wsh_exposed_supply_share') - self.p2tr: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2tr_exposed_supply_share') - self.p2a: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'p2a_exposed_supply_share') + self.output_to_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, 'output_to_respent_addr_count') + self.output_to_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(client, 'output_to_respent_addr_share') + self.spendable_output_to_reused_addr_share: _1m1w1y24hBpsPercentRatioPattern = _1m1w1y24hBpsPercentRatioPattern(client, 'spendable_output_to_respent_addr_share') + self.input_from_reused_addr_count: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, 'input_from_respent_addr_count') + self.input_from_reused_addr_share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern7(client, 'input_from_respent_addr_share') + self.active_reused_addr_count: _1m1w1y24hBlockPattern = _1m1w1y24hBlockPattern(client, 'active_respent_addr_count') + self.active_reused_addr_share: _1m1w1y24hBlockPattern2 = _1m1w1y24hBlockPattern2(client, 'active_respent_addr_share') + +class SeriesTree_Addrs_Respent_Supply: + """Series tree node.""" + + def __init__(self, client: BrkClientBase, base_path: str = ''): + self.all: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'respent_addr_supply') + self.p2pk65: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk65_respent_addr_supply') + self.p2pk33: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk33_respent_addr_supply') + self.p2pkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pkh_respent_addr_supply') + self.p2sh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2sh_respent_addr_supply') + self.p2wpkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wpkh_respent_addr_supply') + self.p2wsh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wsh_respent_addr_supply') + self.p2tr: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2tr_respent_addr_supply') + self.p2a: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2a_respent_addr_supply') + self.share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(client, 'respent_addr_supply_share') + +class SeriesTree_Addrs_Respent: + """Series tree node.""" + + def __init__(self, client: BrkClientBase, base_path: str = ''): + self.count: FundedTotalPattern = FundedTotalPattern(client, 'respent_addr_count') + self.events: SeriesTree_Addrs_Respent_Events = SeriesTree_Addrs_Respent_Events(client) + self.supply: SeriesTree_Addrs_Respent_Supply = SeriesTree_Addrs_Respent_Supply(client) class SeriesTree_Addrs_Exposed_Supply: """Series tree node.""" def __init__(self, client: BrkClientBase, base_path: str = ''): - self.all: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'exposed_supply') - self.p2pk65: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk65_exposed_supply') - self.p2pk33: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk33_exposed_supply') - self.p2pkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pkh_exposed_supply') - self.p2sh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2sh_exposed_supply') - self.p2wpkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wpkh_exposed_supply') - self.p2wsh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wsh_exposed_supply') - self.p2tr: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2tr_exposed_supply') - self.p2a: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2a_exposed_supply') - self.share: SeriesTree_Addrs_Exposed_Supply_Share = SeriesTree_Addrs_Exposed_Supply_Share(client) + self.all: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'exposed_addr_supply') + self.p2pk65: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk65_exposed_addr_supply') + self.p2pk33: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pk33_exposed_addr_supply') + self.p2pkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2pkh_exposed_addr_supply') + self.p2sh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2sh_exposed_addr_supply') + self.p2wpkh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wpkh_exposed_addr_supply') + self.p2wsh: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2wsh_exposed_addr_supply') + self.p2tr: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2tr_exposed_addr_supply') + self.p2a: BtcCentsSatsUsdPattern = BtcCentsSatsUsdPattern(client, 'p2a_exposed_addr_supply') + self.share: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern5(client, 'exposed_addr_supply_share') class SeriesTree_Addrs_Exposed: """Series tree node.""" @@ -4422,6 +4487,7 @@ class SeriesTree_Addrs: self.total: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern4(client, 'total_addr_count') self.new: AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6 = AllP2aP2pk33P2pk65P2pkhP2shP2trP2wpkhP2wshPattern6(client, 'new_addr_count') self.reused: SeriesTree_Addrs_Reused = SeriesTree_Addrs_Reused(client) + self.respent: SeriesTree_Addrs_Respent = SeriesTree_Addrs_Respent(client) self.exposed: SeriesTree_Addrs_Exposed = SeriesTree_Addrs_Exposed(client) self.delta: SeriesTree_Addrs_Delta = SeriesTree_Addrs_Delta(client) self.avg_amount: SeriesTree_Addrs_AvgAmount = SeriesTree_Addrs_AvgAmount(client) diff --git a/website/.well-known/ai-plugin.json b/website/.well-known/ai-plugin.json index 1588d4a91..f6d2ced1f 100644 --- a/website/.well-known/ai-plugin.json +++ b/website/.well-known/ai-plugin.json @@ -9,7 +9,7 @@ "type": "openapi", "url": "https://bitview.space/openapi.json" }, - "logo_url": "https://bitview.space/assets/favicon-196.png", + "logo_url": "https://bitview.space/assets/favicon/web-app-manifest-512x512.png", "contact_email": "hello@bitcoinresearchkit.org", "legal_info_url": "https://github.com/bitcoinresearchkit/brk/blob/main/docs/LICENSE.md" } diff --git a/website/assets/apple-icon-180.png b/website/assets/apple-icon-180.png deleted file mode 100644 index 78f520286..000000000 Binary files a/website/assets/apple-icon-180.png and /dev/null differ diff --git a/website/assets/favicon-196.png b/website/assets/favicon-196.png deleted file mode 100644 index 807be193c..000000000 Binary files a/website/assets/favicon-196.png and /dev/null differ diff --git a/website/assets/favicon/apple-touch-icon.png b/website/assets/favicon/apple-touch-icon.png new file mode 100644 index 000000000..1740fc962 Binary files /dev/null and b/website/assets/favicon/apple-touch-icon.png differ diff --git a/website/assets/favicon/favicon-96x96.png b/website/assets/favicon/favicon-96x96.png new file mode 100644 index 000000000..9eaa409b0 Binary files /dev/null and b/website/assets/favicon/favicon-96x96.png differ diff --git a/website/assets/favicon/favicon.ico b/website/assets/favicon/favicon.ico new file mode 100644 index 000000000..a9ac06559 Binary files /dev/null and b/website/assets/favicon/favicon.ico differ diff --git a/website/assets/favicon/favicon.svg b/website/assets/favicon/favicon.svg new file mode 100644 index 000000000..36d14c301 --- /dev/null +++ b/website/assets/favicon/favicon.svg @@ -0,0 +1,43 @@ +RealFaviconGeneratorhttps://realfavicongenerator.netbitview + + + + + + + + + + \ No newline at end of file diff --git a/website/assets/favicon/site.webmanifest b/website/assets/favicon/site.webmanifest new file mode 100644 index 000000000..916c4f61d --- /dev/null +++ b/website/assets/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "bitview", + "short_name": "bitview", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#EEE", + "background_color": "#EEE", + "display": "standalone" +} \ No newline at end of file diff --git a/website/assets/favicon/web-app-manifest-192x192.png b/website/assets/favicon/web-app-manifest-192x192.png new file mode 100644 index 000000000..291226c10 Binary files /dev/null and b/website/assets/favicon/web-app-manifest-192x192.png differ diff --git a/website/assets/favicon/web-app-manifest-512x512.png b/website/assets/favicon/web-app-manifest-512x512.png new file mode 100644 index 000000000..500ab9250 Binary files /dev/null and b/website/assets/favicon/web-app-manifest-512x512.png differ diff --git a/website/assets/logo/demo-svg.html b/website/assets/logo/demo-svg.html index 5ee7a2b7b..bc9f7fcb0 100644 --- a/website/assets/logo/demo-svg.html +++ b/website/assets/logo/demo-svg.html @@ -26,16 +26,9 @@ --cube: calc(var(--cube-rem) * 1rem); --cube-px: calc(var(--cube-rem) * 16); --face-scale: calc(100 / var(--cube-px)); - --orange: oklch(67.64% 0.191 44.41); --white: oklch(95% 0 0); --black: oklch(15% 0 0); - --light-gray: oklch(90% 0 0); - --dark-gray: oklch(20% 0 0); - --border-color: light-dark(var(--light-gray), var(--dark-gray)); --background-color: light-dark(var(--white), var(--black)); - --fill: 0.5; - --empty-alpha: 0.4; - --face-step: 0.033; --iso-scale: 0.866; --font-size-xs: 0.75rem; --font-size-sm: 0.875rem; @@ -113,6 +106,8 @@ font-weight: 500; pointer-events: none; } + .face-text { color: var(--black); } + svg[data-theme="dark"] .face-text { color: var(--white); } .face-text.top, .face-text.right { display: flex; @@ -165,51 +160,65 @@ .face-text .pool img.marapool { content: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgdmlld0JveD0iMCAwIDI1NiAyNTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0wIDMuNjAwOEMwIDIuMzQzMTMgMCAxLjcxNDMgMC4yNDQ3NTcgMS4yMzM5NEMwLjQ2MDA1MiAwLjgxMTQgMC44MDM1ODcgMC40Njc4NjQgMS4yMjYxMyAwLjI1MjU3QzEuNzA2NDkgMC4wMDc4MTI1IDIuMzM1MzIgMC4wMDc4MTI1IDMuNTkyOTggMC4wMDc4MTI1SDI1Mi40MDdDMjUzLjY2NSAwLjAwNzgxMjUgMjU0LjI5NCAwLjAwNzgxMjUgMjU0Ljc3NCAwLjI1MjU3QzI1NS4xOTYgMC40Njc4NjQgMjU1LjU0IDAuODExNCAyNTUuNzU1IDEuMjMzOTRDMjU2IDEuNzE0MyAyNTYgMi4zNDMxMyAyNTYgMy42MDA4VjI1Mi40MTVDMjU2IDI1My42NzIgMjU2IDI1NC4zMDEgMjU1Ljc1NSAyNTQuNzgyQzI1NS41NCAyNTUuMjA0IDI1NS4xOTYgMjU1LjU0OCAyNTQuNzc0IDI1NS43NjNDMjU0LjI5NCAyNTYuMDA4IDI1My42NjUgMjU2LjAwOCAyNTIuNDA3IDI1Ni4wMDhIMy41OTI5OUMyLjMzNTMyIDI1Ni4wMDggMS43MDY0OSAyNTYuMDA4IDEuMjI2MTMgMjU1Ljc2M0MwLjgwMzU4NyAyNTUuNTQ4IDAuNDYwMDUyIDI1NS4yMDQgMC4yNDQ3NTcgMjU0Ljc4MkMwIDI1NC4zMDEgMCAyNTMuNjcyIDAgMjUyLjQxNVYzLjYwMDhaIiBmaWxsPSIjMjcyNTI1Ii8+CjxwYXRoIGQ9Ik01OS4yODUgNDguMDYyNUM1OC43ODg5IDQ4LjA2MjUgNTguMzg2NyA0OC40NjQ3IDU4LjM4NjcgNDguOTYwN1YxNTkuNDQ1QzU4LjM4NjcgMTU5Ljk0MSA1OC43ODg5IDE2MC4zNDMgNTkuMjg1IDE2MC4zNDNIODAuMDc0OUM4MC41NzA5IDE2MC4zNDMgODAuOTczMSAxNTkuOTQxIDgwLjk3MzEgMTU5LjQ0NVY4Mi4zODlIODQuNjMzNUwxMTcuNjQ5IDE1OS43OTdDMTE3Ljc5IDE2MC4xMjggMTE4LjExNiAxNjAuMzQzIDExOC40NzUgMTYwLjM0M0gxMzcuODA5QzEzOC4xNjkgMTYwLjM0MyAxMzguNDk0IDE2MC4xMjggMTM4LjYzNSAxNTkuNzk3TDE3MS42NTEgODIuMzg5SDE3NS4zMTFWMTU5LjQ0NUMxNzUuMzExIDE1OS45NDEgMTc1LjcxNCAxNjAuMzQzIDE3Ni4yMSAxNjAuMzQzSDE5N0MxOTcuNDk2IDE2MC4zNDMgMTk3Ljg5OCAxNTkuOTQxIDE5Ny44OTggMTU5LjQ0NVY0OC45NjA3QzE5Ny44OTggNDguNDY0NyAxOTcuNDk2IDQ4LjA2MjUgMTk3IDQ4LjA2MjVIMTY0LjI5QzE2My45MzEgNDguMDYyNSAxNjMuNjA1IDQ4LjI3NzMgMTYzLjQ2NCA0OC42MDg0TDEyOS45NzIgMTI3LjE0SDEyNi4zMTJMOTIuODIwMiA0OC42MDg0QzkyLjY3OSA0OC4yNzczIDkyLjM1MzkgNDguMDYyNSA5MS45OTQgNDguMDYyNUg1OS4yODVaIiBmaWxsPSIjRUVFQ0U4Ii8+CjxwYXRoIGQ9Ik01OC4zODY3IDE5NC45MjZDNTguMzg2NyAxOTQuNDMgNTguNzg4OSAxOTQuMDI3IDU5LjI4NSAxOTQuMDI3SDE5Ni45OTlDMTk3LjQ5NiAxOTQuMDI3IDE5Ny44OTggMTk0LjQzIDE5Ny44OTggMTk0LjkyNlYyMTUuNTg1QzE5Ny44OTggMjE2LjA4MSAxOTcuNDk2IDIxNi40ODQgMTk2Ljk5OSAyMTYuNDg0SDU5LjI4NUM1OC43ODg5IDIxNi40ODQgNTguMzg2NyAyMTYuMDgxIDU4LjM4NjcgMjE1LjU4NVYxOTQuOTI2WiIgZmlsbD0iI0YyQTkwMCIvPgo8L3N2Zz4K"); } .face-text .pool img.unknown { content: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjUuMjcgMyAxMy40OCAxOCI+IDxwYXRoIGQ9Ik01LjY0MTcgMTguMzY5NkM1LjE1ODM0IDE3Ljg4NzEgNS4xNTgzMyAxNy4xMTMgNS42NDE2OSAxNi42MzA0QzcuMjY5OTIgMTUuMDA1MSA5LjUxNzYxIDE0IDEyIDE0QzE0LjQ4MjUgMTQgMTYuNzMwMSAxNS4wMDUgMTguMzU4MyAxNi42MzA0QzE4Ljg0MTcgMTcuMTEyOSAxOC44NDE3IDE3Ljg4NzEgMTguMzU4NCAxOC4zNjk2QzE2LjczMDEgMTkuOTk0OSAxNC40ODI0IDIxIDEyIDIxQzkuNTE3NTkgMjEgNy4yNjk5MyAxOS45OTUgNS42NDE3IDE4LjM2OTZaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNOC41MDAwMyA3LjVIMTUuNVY4SDE2QzE2IDEwLjYzNzIgMTQuMzE4OCAxMyAxMiAxM0M5LjY4MTIzIDEzIDguMDAwMDMgMTAuNjM3MiA4LjAwMDAzIDhIOC41MDAwM1Y3LjVaTTkuMDIyNzMgOC41QzkuMjEyNjYgMTAuNTY3OCAxMC41NjU4IDEyIDEyIDEyQzEzLjQzNDMgMTIgMTQuNzg3NCAxMC41Njc4IDE0Ljk3NzMgOC41SDkuMDIyNzNaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTIgM0MxNC40ODUzIDMgMTYuNSA1LjIzODU4IDE2LjUgOEg3LjUwMDAzQzcuNTAwMDMgNS4yMzg1OCA5LjUxNDc1IDMgMTIgM1pNMTIgNy41QzEyLjgyODUgNy41IDEzLjUgNi44Mjg0MyAxMy41IDZDMTMuNSA1LjE3MTU3IDEyLjgyODUgNC41IDEyIDQuNUMxMS4xNzE2IDQuNSAxMC41IDUuMTcxNTcgMTAuNSA2QzEwLjUgNi44Mjg0MyAxMS4xNzE2IDcuNSAxMiA3LjVaIiBmaWxsPSIjYjRiNGI0Ij48L3BhdGg+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNi41MDAwMyA4LjVDNi41MDAwMyA4LjIyMzg2IDYuNzIzODkgOCA3LjAwMDAzIDhIMTdDMTcuMjc2MiA4IDE3LjUgOC4yMjM4NiAxNy41IDguNUMxNy41IDguNzc2MTQgMTcuMjc2MiA5IDE3IDlINy4wMDAwM0M2LjcyMzg5IDkgNi41MDAwMyA4Ljc3NjE0IDYuNTAwMDMgOC41WiIgZmlsbD0iI2I0YjRiNCI+PC9wYXRoPiA8L3N2Zz4="); } - /* ---- cube face colors (derived from --face-color, same logic - as website/styles/panes/explorer.css) ---- */ - svg.cube { - --face-color: var(--orange); - --face-right: light-dark( - oklch(from var(--face-color) calc(l - var(--face-step) * 2) c h), - var(--face-color) - ); - --face-left: light-dark( - oklch(from var(--face-color) calc(l - var(--face-step)) c h), - oklch(from var(--face-color) calc(l + var(--face-step)) c h) - ); - --face-top: light-dark( - var(--face-color), - oklch(from var(--face-color) calc(l + var(--face-step) * 2) c h) - ); - --face-bottom: oklch(from var(--face-color) calc(l - var(--face-step) * 3) c h); - color: light-dark(var(--black), var(--white)); - } - - /* Glass polygons: translucent. Liquid polygons: opaque. Rear-face - colors mirror demo.html (bottom → face-bottom, rear-left → - face-top, rear-right → face-left). */ - svg.cube .glass { fill-opacity: var(--empty-alpha); } - - svg.cube .glass-top, svg.cube .liquid-top { fill: var(--face-top); } - svg.cube .glass-right, svg.cube .liquid-right { fill: var(--face-right); } - svg.cube .glass-left, svg.cube .liquid-left { fill: var(--face-left); } - svg.cube .glass-bottom { fill: var(--face-bottom); } - svg.cube .glass-rear-left { fill: var(--face-top); } - svg.cube .glass-rear-right { fill: var(--face-left); } -
- - + +
@@ -224,61 +233,18 @@
- diff --git a/website/assets/logo/logo-dark.svg b/website/assets/logo/logo-dark.svg new file mode 100644 index 000000000..e589cdb4b --- /dev/null +++ b/website/assets/logo/logo-dark.svg @@ -0,0 +1,37 @@ + + bitview + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/assets/logo/logo-light.svg b/website/assets/logo/logo-light.svg new file mode 100644 index 000000000..0d8a3217b --- /dev/null +++ b/website/assets/logo/logo-light.svg @@ -0,0 +1,37 @@ + + bitview + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/assets/logo/logo-orange.svg b/website/assets/logo/logo-orange.svg new file mode 100644 index 000000000..6414607d7 --- /dev/null +++ b/website/assets/logo/logo-orange.svg @@ -0,0 +1,37 @@ + + bitview + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/assets/logo/logo.svg b/website/assets/logo/logo.svg new file mode 100644 index 000000000..b43af3f90 --- /dev/null +++ b/website/assets/logo/logo.svg @@ -0,0 +1,57 @@ + + bitview + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/assets/manifest-icon-192.maskable.png b/website/assets/manifest-icon-192.maskable.png deleted file mode 100644 index cac1e3284..000000000 Binary files a/website/assets/manifest-icon-192.maskable.png and /dev/null differ diff --git a/website/assets/manifest-icon-512.maskable.png b/website/assets/manifest-icon-512.maskable.png deleted file mode 100644 index 1808e99fa..000000000 Binary files a/website/assets/manifest-icon-512.maskable.png and /dev/null differ diff --git a/website/index.html b/website/index.html index 3f934ab50..b54a86bb3 100644 --- a/website/index.html +++ b/website/index.html @@ -96,13 +96,11 @@ - - + + + + + diff --git a/website/manifest.webmanifest b/website/manifest.webmanifest index 1522eb28b..e4b2ee912 100644 --- a/website/manifest.webmanifest +++ b/website/manifest.webmanifest @@ -16,25 +16,25 @@ "lang": "en", "icons": [ { - "src": "/assets/manifest-icon-192.maskable.png", + "src": "/assets/favicon/web-app-manifest-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/manifest-icon-192.maskable.png", + "src": "/assets/favicon/web-app-manifest-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/manifest-icon-512.maskable.png", + "src": "/assets/favicon/web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/manifest-icon-512.maskable.png", + "src": "/assets/favicon/web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/website/scripts/_types.js b/website/scripts/_types.js index 87bf6dff5..9b48ff02c 100644 --- a/website/scripts/_types.js +++ b/website/scripts/_types.js @@ -131,6 +131,7 @@ * @typedef {Brk.AddrUtxoPattern} AvgAmountPattern * @typedef {Brk.SeriesTree_Addrs_Exposed} ExposedTree * @typedef {Brk.SeriesTree_Addrs_Reused} ReusedTree + * @typedef {Brk.SeriesTree_Addrs_Respent} RespentTree */ /** diff --git a/website/scripts/options/distribution/data.js b/website/scripts/options/distribution/data.js index 392c7b6cb..781ef08b7 100644 --- a/website/scripts/options/distribution/data.js +++ b/website/scripts/options/distribution/data.js @@ -178,6 +178,7 @@ export function buildCohortData() { avgAmount: addrs.avgAmount[key], exposed: addrs.exposed, reused: addrs.reused, + respent: addrs.respent, }; }); diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index 1b5ff4694..b092bbe3f 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -264,7 +264,7 @@ export function createCohortFolderAddress(cohort) { createProfitabilitySectionWithProfitLoss({ cohort, title }), createActivitySectionMinimal({ cohort, title }), avgHoldingsSubtree(cohort.avgAmount, title), - reusedSubtree(cohort.reused, cohort.key, title), + reusedSubtree(cohort.reused, cohort.respent, cohort.key, title), exposedSubtree(cohort.exposed, cohort.key, title), ], }; diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index c3d14fc16..b752366d1 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -103,154 +103,254 @@ export function createNetworkSection() { { key: "total", name: "Total", color: colors.default }, ]); - const reusedSetEntries = - /** - * @param {AddressableType | "all"} key - * @param {(name: string) => string} title - */ - (key, title) => [ - { - name: "Compare", - title: title("Reused Address Count"), - bottom: [ - line({ - series: addrs.reused.count.funded[key], - name: "Funded", - unit: Unit.count, - }), - line({ - series: addrs.reused.count.total[key], - name: "Total", - color: colors.gray, - unit: Unit.count, - }), - ], - }, - { - name: "Funded", - title: title("Funded Reused Addresses"), - bottom: [ - line({ - series: addrs.reused.count.funded[key], - name: "Funded Reused", - unit: Unit.count, - }), - ], - }, - { - name: "Total", - title: title("Total Reused Addresses"), - bottom: [ - line({ - series: addrs.reused.count.total[key], - name: "Total Reused", - color: colors.gray, - unit: Unit.count, - }), - ], - }, - ]; - - const reusedInputsSubtree = - /** - * @param {AddressableType | "all"} key - * @param {(name: string) => string} title - */ - (key, title) => ({ - name: "Inputs", - tree: [ - { - name: "Count", - tree: chartsFromCount({ - pattern: addrs.reused.events.inputFromReusedAddrCount[key], - title, - metric: "Transaction Inputs from Reused Addresses", - unit: Unit.count, - }), - }, - { - name: "Share", - tree: chartsFromPercentCumulative({ - pattern: addrs.reused.events.inputFromReusedAddrShare[key], - title, - metric: "Share of Transaction Inputs from Reused Addresses", - }), - }, - ], - }); - - const reusedOutputsSubtreeForAll = - /** @param {(name: string) => string} title */ - (title) => ({ - name: "Outputs", - tree: [ - { - name: "Count", - tree: chartsFromCount({ - pattern: addrs.reused.events.outputToReusedAddrCount.all, - title, - metric: "Transaction Outputs to Reused Addresses", - unit: Unit.count, - }), - }, - { - name: "Share", - tree: chartsFromPercentCumulativeEntries({ - entries: [ - { - name: "All", - pattern: addrs.reused.events.outputToReusedAddrShare.all, - }, - { - name: "Spendable", - pattern: addrs.reused.events.spendableOutputToReusedAddrShare, - color: colors.gray, - }, - ], - title, - metric: "Share of Transaction Outputs to Reused Addresses", - }), - }, - ], - }); - - const reusedActiveSubtreeForAll = - /** @param {(name: string) => string} title */ - (title) => ({ - name: "Active", - tree: [ - { - name: "Count", - tree: averagesArray({ - windows: addrs.reused.events.activeReusedAddrCount, - title, - metric: "Active Reused Addresses", - unit: Unit.count, - }), - }, - { - name: "Share", - tree: averagesArray({ - windows: addrs.reused.events.activeReusedAddrShare, - title, - metric: "Active Reused Address Share", - unit: Unit.percentage, - }), - }, - ], - }); - const reusedSubtreeForAll = /** @param {(name: string) => string} title */ - (title) => ({ - name: "Reused", - tree: [ - ...reusedSetEntries("all", title), - reusedActiveSubtreeForAll(title), - reusedOutputsSubtreeForAll(title), - reusedInputsSubtree("all", title), - ], - }); + (title) => { + const reused = addrs.reused; + const respent = addrs.respent; + const key = /** @type {const} */ ("all"); + + /** + * Windowed sums + cumulative, overlaying reused (primary) and respent (gray). + * @param {CountPattern} reusedPattern + * @param {CountPattern} respentPattern + * @param {string} metric + * @returns {PartialOptionsTree} + */ + const countPair = (reusedPattern, respentPattern, metric) => [ + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`${w.title} ${metric}`), + bottom: [ + line({ + series: reusedPattern.sum[w.key], + name: "2+ Funded", + unit: Unit.count, + }), + line({ + series: respentPattern.sum[w.key], + name: "2+ Spent", + color: colors.gray, + unit: Unit.count, + }), + ], + })), + { + name: "Cumulative", + title: title(`Cumulative ${metric}`), + bottom: [ + line({ + series: reusedPattern.cumulative, + name: "2+ Funded", + unit: Unit.count, + }), + line({ + series: respentPattern.cumulative, + name: "2+ Spent", + color: colors.gray, + unit: Unit.count, + }), + ], + }, + ]; + + return { + name: "Reused", + tree: [ + { + name: "Funded", + title: title("Funded Reused Addresses"), + bottom: [ + line({ + series: reused.count.funded[key], + name: "2+ Funded", + unit: Unit.count, + }), + line({ + series: respent.count.funded[key], + name: "2+ Spent", + color: colors.gray, + unit: Unit.count, + }), + ], + }, + { + name: "Total", + title: title("Total Reused Addresses"), + bottom: [ + line({ + series: reused.count.total[key], + name: "2+ Funded", + unit: Unit.count, + }), + line({ + series: respent.count.total[key], + name: "2+ Spent", + color: colors.gray, + unit: Unit.count, + }), + ], + }, + { + name: "Active", + tree: [ + { + name: "Count", + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`${w.title} Active Reused Addresses`), + bottom: [ + line({ + series: reused.events.activeReusedAddrCount[w.key], + name: "2+ Funded", + unit: Unit.count, + }), + line({ + series: respent.events.activeReusedAddrCount[w.key], + name: "2+ Spent", + color: colors.gray, + unit: Unit.count, + }), + ], + })), + }, + { + name: "Share", + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`${w.title} Active Reused Address Share`), + bottom: [ + line({ + series: reused.events.activeReusedAddrShare[w.key], + name: "2+ Funded", + unit: Unit.percentage, + }), + line({ + series: respent.events.activeReusedAddrShare[w.key], + name: "2+ Spent", + color: colors.gray, + unit: Unit.percentage, + }), + ], + })), + }, + ], + }, + { + name: "Outputs", + tree: [ + { + name: "Count", + tree: countPair( + reused.events.outputToReusedAddrCount[key], + respent.events.outputToReusedAddrCount[key], + "Transaction Outputs to Reused Addresses", + ), + }, + { + name: "Share", + tree: [ + { + name: "All", + tree: chartsFromPercentCumulativeEntries({ + entries: [ + { + name: "2+ Funded", + pattern: reused.events.outputToReusedAddrShare[key], + }, + { + name: "2+ Spent", + pattern: respent.events.outputToReusedAddrShare[key], + color: colors.gray, + }, + ], + title, + metric: "Share of Transaction Outputs to Reused Addresses", + }), + }, + { + name: "Spendable", + tree: chartsFromPercentCumulativeEntries({ + entries: [ + { + name: "2+ Funded", + pattern: reused.events.spendableOutputToReusedAddrShare, + }, + { + name: "2+ Spent", + pattern: respent.events.spendableOutputToReusedAddrShare, + color: colors.gray, + }, + ], + title, + metric: "Share of Spendable Transaction Outputs to Reused Addresses", + }), + }, + ], + }, + ], + }, + { + name: "Inputs", + tree: [ + { + name: "Count", + tree: countPair( + reused.events.inputFromReusedAddrCount[key], + respent.events.inputFromReusedAddrCount[key], + "Transaction Inputs from Reused Addresses", + ), + }, + { + name: "Share", + tree: chartsFromPercentCumulativeEntries({ + entries: [ + { + name: "2+ Funded", + pattern: reused.events.inputFromReusedAddrShare[key], + }, + { + name: "2+ Spent", + pattern: respent.events.inputFromReusedAddrShare[key], + color: colors.gray, + }, + ], + title, + metric: "Share of Transaction Inputs from Reused Addresses", + }), + }, + ], + }, + { + name: "Supply", + title: title("Supply in Reused Addresses"), + bottom: [ + ...satsBtcUsd({ pattern: reused.supply[key], name: "2+ Funded" }), + ...satsBtcUsd({ + pattern: respent.supply[key], + name: "2+ Spent", + color: colors.gray, + }), + ], + }, + { + name: "Share", + title: title("Share of Supply in Reused Addresses"), + bottom: [ + ...percentRatio({ + pattern: reused.supply.share[key], + name: "2+ Funded", + }), + ...percentRatio({ + pattern: respent.supply.share[key], + name: "2+ Spent", + color: colors.gray, + }), + ], + }, + ], + }; + }; const countSubtree = @@ -328,6 +428,12 @@ export function createNetworkSection() { name: "Reused Share", unit: Unit.percentage, }), + line({ + series: addrs.respent.events.activeReusedAddrShare[w.key], + name: "Respent Share", + color: colors.gray, + unit: Unit.percentage, + }), ], })), }, @@ -413,7 +519,7 @@ export function createNetworkSection() { unit: Unit.count, }), activitySubtreeForType(addrType, title), - reusedSubtree(addrs.reused, addrType, title), + reusedSubtree(addrs.reused, addrs.respent, addrType, title), exposedSubtree(addrs.exposed, addrType, title), avgHoldingsSubtree(addrs.avgAmount[addrType], title), ]; @@ -458,6 +564,113 @@ export function createNetworkSection() { }), ); + const reuseCompareByTypeFolder = + /** + * @param {string} label - "Reused" or "Respent" + * @param {ReusedTree | RespentTree} tree + * @returns {PartialOptionsGroup} + */ + (label, tree) => ({ + name: label, + tree: [ + { + name: "Funded", + title: `Funded ${label} Address Count by Type`, + bottom: typeLines((t) => tree.count.funded[t.key]), + }, + { + name: "Total", + title: `Total ${label} Address Count by Type`, + bottom: typeLines((t) => tree.count.total[t.key]), + }, + { + name: "Outputs", + tree: [ + { + name: "Count", + tree: groupedWindowsCumulative({ + list: addressTypes, + title: (s) => s, + metricTitle: `Transaction Outputs to ${label} Addresses by Type`, + getWindowSeries: (t, key) => + tree.events.outputToReusedAddrCount[t.key].sum[key], + getCumulativeSeries: (t) => + tree.events.outputToReusedAddrCount[t.key].cumulative, + seriesFn: line, + unit: Unit.count, + }), + }, + { + name: "Share", + tree: [ + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${w.title} Share of Transaction Outputs to ${label} Addresses by Type`, + bottom: typeLines( + (t) => + tree.events.outputToReusedAddrShare[t.key][w.key] + .percent, + Unit.percentage, + ), + })), + { + name: "Cumulative", + title: `Cumulative Share of Transaction Outputs to ${label} Addresses by Type`, + bottom: typeLines( + (t) => + tree.events.outputToReusedAddrShare[t.key].percent, + Unit.percentage, + ), + }, + ], + }, + ], + }, + { + name: "Inputs", + tree: [ + { + name: "Count", + tree: groupedWindowsCumulative({ + list: addressTypes, + title: (s) => s, + metricTitle: `Transaction Inputs from ${label} Addresses by Type`, + getWindowSeries: (t, key) => + tree.events.inputFromReusedAddrCount[t.key].sum[key], + getCumulativeSeries: (t) => + tree.events.inputFromReusedAddrCount[t.key].cumulative, + seriesFn: line, + unit: Unit.count, + }), + }, + { + name: "Share", + tree: [ + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: `${w.title} Share of Transaction Inputs from ${label} Addresses by Type`, + bottom: typeLines( + (t) => + tree.events.inputFromReusedAddrShare[t.key][w.key] + .percent, + Unit.percentage, + ), + })), + { + name: "Cumulative", + title: `Cumulative Share of Transaction Inputs from ${label} Addresses by Type`, + bottom: typeLines( + (t) => tree.events.inputFromReusedAddrShare[t.key].percent, + Unit.percentage, + ), + }, + ], + }, + ], + }, + ], + }); + return { name: "Compare", tree: [ @@ -525,120 +738,12 @@ export function createNetworkSection() { })), }, - // Reused + // Address Reuse: receive-side (Reused) + spend-side (Respent) { - name: "Reused", + name: "Address Reuse", tree: [ - { - name: "Funded", - title: "Funded Reused Address Count by Type", - bottom: typeLines((t) => addrs.reused.count.funded[t.key]), - }, - { - name: "Total", - title: "Total Reused Address Count by Type", - bottom: typeLines((t) => addrs.reused.count.total[t.key]), - }, - { - name: "Outputs", - tree: [ - { - name: "Count", - tree: groupedWindowsCumulative({ - list: addressTypes, - title: (s) => s, - metricTitle: - "Transaction Outputs to Reused Addresses by Type", - getWindowSeries: (t, key) => - addrs.reused.events.outputToReusedAddrCount[t.key].sum[ - key - ], - getCumulativeSeries: (t) => - addrs.reused.events.outputToReusedAddrCount[t.key] - .cumulative, - seriesFn: line, - unit: Unit.count, - }), - }, - { - name: "Share", - tree: [ - ...ROLLING_WINDOWS.map((w) => ({ - name: w.name, - title: `${w.title} Share of Transaction Outputs to Reused Addresses by Type`, - bottom: typeLines( - (t) => - addrs.reused.events.outputToReusedAddrShare[t.key][ - w.key - ].percent, - Unit.percentage, - ), - })), - { - name: "Cumulative", - title: - "Cumulative Share of Transaction Outputs to Reused Addresses by Type", - bottom: typeLines( - (t) => - addrs.reused.events.outputToReusedAddrShare[t.key] - .percent, - Unit.percentage, - ), - }, - ], - }, - ], - }, - { - name: "Inputs", - tree: [ - { - name: "Count", - tree: groupedWindowsCumulative({ - list: addressTypes, - title: (s) => s, - metricTitle: - "Transaction Inputs from Reused Addresses by Type", - getWindowSeries: (t, key) => - addrs.reused.events.inputFromReusedAddrCount[t.key].sum[ - key - ], - getCumulativeSeries: (t) => - addrs.reused.events.inputFromReusedAddrCount[t.key] - .cumulative, - seriesFn: line, - unit: Unit.count, - }), - }, - { - name: "Share", - tree: [ - ...ROLLING_WINDOWS.map((w) => ({ - name: w.name, - title: `${w.title} Share of Transaction Inputs from Reused Addresses by Type`, - bottom: typeLines( - (t) => - addrs.reused.events.inputFromReusedAddrShare[t.key][ - w.key - ].percent, - Unit.percentage, - ), - })), - { - name: "Cumulative", - title: - "Cumulative Share of Transaction Inputs from Reused Addresses by Type", - bottom: typeLines( - (t) => - addrs.reused.events.inputFromReusedAddrShare[t.key] - .percent, - Unit.percentage, - ), - }, - ], - }, - ], - }, + reuseCompareByTypeFolder("Reused", addrs.reused), + reuseCompareByTypeFolder("Respent", addrs.respent), ], }, diff --git a/website/scripts/options/shared.js b/website/scripts/options/shared.js index e5a247d45..91710bd00 100644 --- a/website/scripts/options/shared.js +++ b/website/scripts/options/shared.js @@ -7,8 +7,7 @@ import { baseline, price, percentRatio, - chartsFromCount, - chartsFromPercentCumulative, + chartsFromPercentCumulativeEntries, sumsAndAveragesCumulativeWith, } from "./series.js"; import { priceLine, priceLines } from "./constants.js"; @@ -365,45 +364,78 @@ export function exposedSubtree(exposed, key, title) { } /** - * "Reused" subtree (per-type / per-cohort — no "Active" window, since that - * data is only tracked globally). Shape: - * Compare (funded + total) / Funded / Total / Outputs / Inputs. + * "Reused" subtree (per-type / per-cohort, no "Active" window since that + * data is only tracked globally). Respent (addresses whose outputs have + * been spent more than once) is a subset of reused, so each chart layers + * both series in two colors: reused in the primary color, respent in + * gray. Shape: Funded / Total / Outputs / Inputs / Supply / Share. * @param {ReusedTree} reused + * @param {RespentTree} respent * @param {AddressableType | "all"} key * @param {(name: string) => string} title * @returns {PartialOptionsGroup} */ -export function reusedSubtree(reused, key, title) { +export function reusedSubtree(reused, respent, key, title) { + /** + * Windowed sums + cumulative, overlaying reused (primary) and respent (gray). + * @param {CountPattern} reusedPattern + * @param {CountPattern} respentPattern + * @param {string} metric + * @returns {PartialOptionsTree} + */ + const countPair = (reusedPattern, respentPattern, metric) => [ + ...ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`${w.title} ${metric}`), + bottom: [ + line({ series: reusedPattern.sum[w.key], name: "2+ Funded", unit: Unit.count }), + line({ + series: respentPattern.sum[w.key], + name: "2+ Spent", + color: colors.gray, + unit: Unit.count, + }), + ], + })), + { + name: "Cumulative", + title: title(`Cumulative ${metric}`), + bottom: [ + line({ series: reusedPattern.cumulative, name: "2+ Funded", unit: Unit.count }), + line({ + series: respentPattern.cumulative, + name: "2+ Spent", + color: colors.gray, + unit: Unit.count, + }), + ], + }, + ]; + return { name: "Reused", tree: [ { - name: "Compare", - title: title("Reused Address Count"), + name: "Funded", + title: title("Funded Reused Addresses"), bottom: [ - line({ series: reused.count.funded[key], name: "Funded", unit: Unit.count }), + line({ series: reused.count.funded[key], name: "2+ Funded", unit: Unit.count }), line({ - series: reused.count.total[key], - name: "Total", + series: respent.count.funded[key], + name: "2+ Spent", color: colors.gray, unit: Unit.count, }), ], }, - { - name: "Funded", - title: title("Funded Reused Addresses"), - bottom: [ - line({ series: reused.count.funded[key], name: "Funded Reused", unit: Unit.count }), - ], - }, { name: "Total", title: title("Total Reused Addresses"), bottom: [ + line({ series: reused.count.total[key], name: "2+ Funded", unit: Unit.count }), line({ - series: reused.count.total[key], - name: "Total Reused", + series: respent.count.total[key], + name: "2+ Spent", color: colors.gray, unit: Unit.count, }), @@ -414,17 +446,26 @@ export function reusedSubtree(reused, key, title) { tree: [ { name: "Count", - tree: chartsFromCount({ - pattern: reused.events.outputToReusedAddrCount[key], - title, - metric: "Transaction Outputs to Reused Addresses", - unit: Unit.count, - }), + tree: countPair( + reused.events.outputToReusedAddrCount[key], + respent.events.outputToReusedAddrCount[key], + "Transaction Outputs to Reused Addresses", + ), }, { name: "Share", - tree: chartsFromPercentCumulative({ - pattern: reused.events.outputToReusedAddrShare[key], + tree: chartsFromPercentCumulativeEntries({ + entries: [ + { + name: "2+ Funded", + pattern: reused.events.outputToReusedAddrShare[key], + }, + { + name: "2+ Spent", + pattern: respent.events.outputToReusedAddrShare[key], + color: colors.gray, + }, + ], title, metric: "Share of Transaction Outputs to Reused Addresses", }), @@ -436,23 +477,56 @@ export function reusedSubtree(reused, key, title) { tree: [ { name: "Count", - tree: chartsFromCount({ - pattern: reused.events.inputFromReusedAddrCount[key], - title, - metric: "Transaction Inputs from Reused Addresses", - unit: Unit.count, - }), + tree: countPair( + reused.events.inputFromReusedAddrCount[key], + respent.events.inputFromReusedAddrCount[key], + "Transaction Inputs from Reused Addresses", + ), }, { name: "Share", - tree: chartsFromPercentCumulative({ - pattern: reused.events.inputFromReusedAddrShare[key], + tree: chartsFromPercentCumulativeEntries({ + entries: [ + { + name: "2+ Funded", + pattern: reused.events.inputFromReusedAddrShare[key], + }, + { + name: "2+ Spent", + pattern: respent.events.inputFromReusedAddrShare[key], + color: colors.gray, + }, + ], title, metric: "Share of Transaction Inputs from Reused Addresses", }), }, ], }, + { + name: "Supply", + title: title("Supply in Reused Addresses"), + bottom: [ + ...satsBtcUsd({ pattern: reused.supply[key], name: "2+ Funded" }), + ...satsBtcUsd({ + pattern: respent.supply[key], + name: "2+ Spent", + color: colors.gray, + }), + ], + }, + { + name: "Share", + title: title("Share of Supply in Reused Addresses"), + bottom: [ + ...percentRatio({ pattern: reused.supply.share[key], name: "2+ Funded" }), + ...percentRatio({ + pattern: respent.supply.share[key], + name: "2+ Spent", + color: colors.gray, + }), + ], + }, ], }; } diff --git a/website/scripts/options/types.js b/website/scripts/options/types.js index 7b5da68ec..176800a9b 100644 --- a/website/scripts/options/types.js +++ b/website/scripts/options/types.js @@ -252,7 +252,7 @@ * ============================================================================ * * Addressable cohort with address count (for "type" cohorts - uses OutputsRealizedSupplyUnrealizedPattern2) - * @typedef {{ name: string, key: AddressableType, title: string, color: Color, tree: EmptyPattern, addressCount: AddrCountPattern, avgAmount: AvgAmountPattern, exposed: ExposedTree, reused: ReusedTree }} CohortAddr + * @typedef {{ name: string, key: AddressableType, title: string, color: Color, tree: EmptyPattern, addressCount: AddrCountPattern, avgAmount: AvgAmountPattern, exposed: ExposedTree, reused: ReusedTree, respent: RespentTree }} CohortAddr * * ============================================================================ * Cohort Group Types (by capability)