From e4496742a4964a986078f3a65d3bfdc1e47a5eba Mon Sep 17 00:00:00 2001 From: nym21 Date: Thu, 23 Apr 2026 23:13:39 +0200 Subject: [PATCH] global: reused + mempool + favicon --- crates/brk_client/src/lib.rs | 252 +++++-- .../src/distribution/addr/addr_count.rs | 183 ----- .../count/vecs.rs => count/all_vecs.rs} | 15 +- .../mod.rs => count/funded_total_vecs.rs} | 44 +- .../src/distribution/addr/count/mod.rs | 7 + .../src/distribution/addr/count/state.rs | 38 ++ .../src/distribution/addr/delta.rs | 4 +- .../distribution/addr/exposed/count/state.rs | 42 -- .../distribution/addr/exposed/count/vecs.rs | 29 - .../src/distribution/addr/exposed/mod.rs | 71 +- .../src/distribution/addr/exposed/state.rs | 83 +++ .../distribution/addr/exposed/supply/mod.rs | 12 - .../distribution/addr/exposed/supply/share.rs | 36 - .../distribution/addr/exposed/supply/state.rs | 42 -- .../brk_computer/src/distribution/addr/mod.rs | 12 +- .../src/distribution/addr/reused/count/mod.rs | 78 --- .../distribution/addr/reused/count/state.rs | 42 -- .../distribution/addr/reused/events/mod.rs | 10 +- .../distribution/addr/reused/events/state.rs | 21 +- .../distribution/addr/reused/events/vecs.rs | 29 +- .../src/distribution/addr/reused/mod.rs | 69 +- .../src/distribution/addr/reused/state.rs | 201 ++++++ .../src/distribution/addr/state.rs | 181 +++++ .../src/distribution/addr/supply/mod.rs | 12 + .../src/distribution/addr/supply/share.rs | 69 ++ .../src/distribution/addr/supply/state.rs | 49 ++ .../addr/{exposed => }/supply/vecs.rs | 22 +- .../src/distribution/addr/total_addr_count.rs | 4 +- .../src/distribution/block/cohort/received.rs | 124 +--- .../src/distribution/block/cohort/sent.rs | 132 +--- .../src/distribution/compute/block_loop.rs | 129 +--- .../src/distribution/compute/write.rs | 6 +- crates/brk_computer/src/distribution/vecs.rs | 123 +++- crates/brk_computer/src/lib.rs | 6 +- .../src/block_builder/linearize/mod.rs | 22 +- .../src/block_builder/linearize/sfl.rs | 33 +- crates/brk_mempool/src/block_builder/mod.rs | 2 +- .../brk_mempool/src/block_builder/package.rs | 10 +- .../src/block_builder/partitioner.rs | 62 +- .../brk_mempool/src/projected_blocks/mod.rs | 2 + .../src/projected_blocks/verify.rs | 149 +++++ crates/brk_mempool/src/sync.rs | 4 + crates/brk_query/src/impl/urpd.rs | 16 +- crates/brk_rpc/Cargo.toml | 2 +- crates/brk_rpc/src/backend/bitcoincore.rs | 31 +- crates/brk_rpc/src/backend/corepc.rs | 39 +- crates/brk_rpc/src/backend/mod.rs | 8 +- crates/brk_rpc/src/lib.rs | 8 +- crates/brk_server/src/api/scalar.html | 4 +- crates/brk_types/src/funded_addr_data.rs | 39 +- modules/brk-client/index.js | 232 +++++-- packages/brk_client/brk_client/__init__.py | 128 +++- website/.well-known/ai-plugin.json | 2 +- website/assets/apple-icon-180.png | Bin 511 -> 0 bytes website/assets/favicon-196.png | Bin 594 -> 0 bytes website/assets/favicon/apple-touch-icon.png | Bin 0 -> 5567 bytes website/assets/favicon/favicon-96x96.png | Bin 0 -> 3847 bytes website/assets/favicon/favicon.ico | Bin 0 -> 15086 bytes website/assets/favicon/favicon.svg | 43 ++ website/assets/favicon/site.webmanifest | 21 + .../favicon/web-app-manifest-192x192.png | Bin 0 -> 6402 bytes .../favicon/web-app-manifest-512x512.png | Bin 0 -> 21883 bytes website/assets/logo/demo-svg.html | 258 +++----- website/assets/logo/logo-dark.svg | 37 ++ website/assets/logo/logo-light.svg | 37 ++ website/assets/logo/logo-orange.svg | 37 ++ website/assets/logo/logo.svg | 57 ++ website/assets/manifest-icon-192.maskable.png | Bin 562 -> 0 bytes website/assets/manifest-icon-512.maskable.png | Bin 1894 -> 0 bytes website/index.html | 12 +- website/manifest.webmanifest | 8 +- website/scripts/_types.js | 1 + website/scripts/options/distribution/data.js | 1 + website/scripts/options/distribution/index.js | 2 +- website/scripts/options/network.js | 623 ++++++++++-------- website/scripts/options/shared.js | 146 +++- website/scripts/options/types.js | 2 +- 77 files changed, 2631 insertions(+), 1624 deletions(-) delete mode 100644 crates/brk_computer/src/distribution/addr/addr_count.rs rename crates/brk_computer/src/distribution/addr/{reused/count/vecs.rs => count/all_vecs.rs} (56%) rename crates/brk_computer/src/distribution/addr/{exposed/count/mod.rs => count/funded_total_vecs.rs} (61%) create mode 100644 crates/brk_computer/src/distribution/addr/count/mod.rs create mode 100644 crates/brk_computer/src/distribution/addr/count/state.rs delete mode 100644 crates/brk_computer/src/distribution/addr/exposed/count/state.rs delete mode 100644 crates/brk_computer/src/distribution/addr/exposed/count/vecs.rs create mode 100644 crates/brk_computer/src/distribution/addr/exposed/state.rs delete mode 100644 crates/brk_computer/src/distribution/addr/exposed/supply/mod.rs delete mode 100644 crates/brk_computer/src/distribution/addr/exposed/supply/share.rs delete mode 100644 crates/brk_computer/src/distribution/addr/exposed/supply/state.rs delete mode 100644 crates/brk_computer/src/distribution/addr/reused/count/mod.rs delete mode 100644 crates/brk_computer/src/distribution/addr/reused/count/state.rs create mode 100644 crates/brk_computer/src/distribution/addr/reused/state.rs create mode 100644 crates/brk_computer/src/distribution/addr/state.rs create mode 100644 crates/brk_computer/src/distribution/addr/supply/mod.rs create mode 100644 crates/brk_computer/src/distribution/addr/supply/share.rs create mode 100644 crates/brk_computer/src/distribution/addr/supply/state.rs rename crates/brk_computer/src/distribution/addr/{exposed => }/supply/vecs.rs (51%) create mode 100644 crates/brk_mempool/src/projected_blocks/verify.rs delete mode 100644 website/assets/apple-icon-180.png delete mode 100644 website/assets/favicon-196.png create mode 100644 website/assets/favicon/apple-touch-icon.png create mode 100644 website/assets/favicon/favicon-96x96.png create mode 100644 website/assets/favicon/favicon.ico create mode 100644 website/assets/favicon/favicon.svg create mode 100644 website/assets/favicon/site.webmanifest create mode 100644 website/assets/favicon/web-app-manifest-192x192.png create mode 100644 website/assets/favicon/web-app-manifest-512x512.png create mode 100644 website/assets/logo/logo-dark.svg create mode 100644 website/assets/logo/logo-light.svg create mode 100644 website/assets/logo/logo-orange.svg create mode 100644 website/assets/logo/logo.svg delete mode 100644 website/assets/manifest-icon-192.maskable.png delete mode 100644 website/assets/manifest-icon-512.maskable.png 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 78f52028644171f7439f1600cd517dcfd2ff1a97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 511 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD2}o{QKQR?ZF%}28J29*~C-V}>-s$P$7*a9k z?IlCO0}2cW92^`&AN>yqU_R^9Ue79NbiR(QCUQIT5B=f^XB>^#Ps*r_G6qE;R59{2 WFoE diff --git a/website/assets/favicon-196.png b/website/assets/favicon-196.png deleted file mode 100644 index 807be193c9fa12b4076ffd7600a431fd6c4a5813..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 594 zcmeAS@N?(olHy`uVBq!ia0vp^M?jc^2}t@Mk+cC)jKx9jP7LeL$-HD>VB+y~aSW-L z^Y)S>CxapbgQIDq=G%VGbw`>C&Zyitey;jk!=WnK1NAdHduE;(C5Cqx)T9(LeR!n1 STOOE57(8A5T-G@yGywp!YD$0r diff --git a/website/assets/favicon/apple-touch-icon.png b/website/assets/favicon/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1740fc962ce7bb0a18dbc6e9f4ce54304b6dba66 GIT binary patch literal 5567 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 Zw^C2lJfJ4_oH()oGh?_>&5eKl{Vz%p*C+r0 literal 0 HcmV?d00001 diff --git a/website/assets/favicon/favicon-96x96.png b/website/assets/favicon/favicon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..9eaa409b05ab3b39e417a67902269fb8826af9cb GIT binary patch literal 3847 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..291226c100fbbfb952d0eee83a501eacfacf7b3e GIT binary patch literal 6402 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 - - + +
@@ -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 cac1e32843dee524be0bc227e80ec21bf71ad952..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE2}s`E_d5VcF%}28J29*~C-V}>e&gxl7*a9k z?IlOv0}2cW92^dYJhIoA(f-wO+8xaoi$A|fKM?-9jNyNpf)MjWiH;) iJT6J2LPIJfPI5EPW_saM6(0&r1Pq?8elF{r5}E)>x}3HE diff --git a/website/assets/manifest-icon-512.maskable.png b/website/assets/manifest-icon-512.maskable.png deleted file mode 100644 index 1808e99fa4b87ebb47aa658da7f7b99f46617038..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1894 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7&t&wwUqN(AjMc54= - - + + + + + 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)