From 6fa53aca9f38a781e3e8efb6f1ea858f884ddd55 Mon Sep 17 00:00:00 2001 From: nym21 Date: Thu, 18 Dec 2025 22:18:28 +0100 Subject: [PATCH] computer: stateful snapshot --- .../src/stateful/cohorts/utxo_cohorts/mod.rs | 27 ++- .../stateful/cohorts/utxo_cohorts/receive.rs | 14 +- .../src/stateful/cohorts/utxo_cohorts/send.rs | 6 +- .../src/stateful/compute/block_loop.rs | 2 +- .../src/stateful/metrics/activity.rs | 8 +- crates/brk_grouper/src/by_year.rs | 159 ++++++++++++++++++ crates/brk_grouper/src/filter.rs | 6 +- crates/brk_grouper/src/lib.rs | 2 + crates/brk_grouper/src/utxo.rs | 10 +- crates/brk_types/src/lib.rs | 2 + crates/brk_types/src/year.rs | 130 ++++++++++++++ 11 files changed, 350 insertions(+), 16 deletions(-) create mode 100644 crates/brk_grouper/src/by_year.rs create mode 100644 crates/brk_types/src/year.rs diff --git a/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/mod.rs b/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/mod.rs index 76f9f6114..e64a709f9 100644 --- a/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/mod.rs +++ b/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/mod.rs @@ -9,11 +9,11 @@ use std::path::Path; use brk_error::Result; use brk_grouper::{ AmountFilter, ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, - ByMaxAge, ByMinAge, BySpendableType, ByTerm, Filter, Filtered, StateLevel, Term, TimeFilter, - UTXOGroups, + ByMaxAge, ByMinAge, BySpendableType, ByTerm, ByYear, Filter, Filtered, StateLevel, Term, + TimeFilter, UTXOGroups, }; use brk_traversable::Traversable; -use brk_types::{Bitcoin, DateIndex, Dollars, HalvingEpoch, Height, OutputType, Sats, Version}; +use brk_types::{Bitcoin, DateIndex, Dollars, HalvingEpoch, Height, OutputType, Sats, Version, Year}; use derive_deref::{Deref, DerefMut}; use rayon::prelude::*; use vecdb::{Database, Exit, IterableVec}; @@ -75,6 +75,27 @@ impl UTXOCohorts { _4: full(Filter::Epoch(HalvingEpoch::new(4)))?, }, + year: ByYear { + _2009: full(Filter::Year(Year::new(2009)))?, + _2010: full(Filter::Year(Year::new(2010)))?, + _2011: full(Filter::Year(Year::new(2011)))?, + _2012: full(Filter::Year(Year::new(2012)))?, + _2013: full(Filter::Year(Year::new(2013)))?, + _2014: full(Filter::Year(Year::new(2014)))?, + _2015: full(Filter::Year(Year::new(2015)))?, + _2016: full(Filter::Year(Year::new(2016)))?, + _2017: full(Filter::Year(Year::new(2017)))?, + _2018: full(Filter::Year(Year::new(2018)))?, + _2019: full(Filter::Year(Year::new(2019)))?, + _2020: full(Filter::Year(Year::new(2020)))?, + _2021: full(Filter::Year(Year::new(2021)))?, + _2022: full(Filter::Year(Year::new(2022)))?, + _2023: full(Filter::Year(Year::new(2023)))?, + _2024: full(Filter::Year(Year::new(2024)))?, + _2025: full(Filter::Year(Year::new(2025)))?, + _2026: full(Filter::Year(Year::new(2026)))?, + }, + type_: BySpendableType { p2pk65: full(Filter::Type(OutputType::P2PK65))?, p2pk33: full(Filter::Type(OutputType::P2PK33))?, diff --git a/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/receive.rs b/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/receive.rs index c22767356..70b58360d 100644 --- a/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/receive.rs +++ b/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/receive.rs @@ -1,7 +1,7 @@ //! Processing received outputs (new UTXOs). use brk_grouper::{Filter, Filtered}; -use brk_types::{Dollars, Height}; +use brk_types::{Dollars, Height, Timestamp}; use crate::stateful::states::Transacted; @@ -13,15 +13,23 @@ impl UTXOCohorts { /// New UTXOs are added to: /// - The "up_to_1d" age cohort (all new UTXOs start at 0 days old) /// - The appropriate epoch cohort based on block height + /// - The appropriate year cohort based on block timestamp /// - The appropriate output type cohort (P2PKH, P2SH, etc.) /// - The appropriate amount range cohort based on value - pub fn receive(&mut self, received: Transacted, height: Height, price: Option) { + pub fn receive( + &mut self, + received: Transacted, + height: Height, + timestamp: Timestamp, + price: Option, + ) { let supply_state = received.spendable_supply; - // New UTXOs go into up_to_1d and current epoch + // New UTXOs go into up_to_1d, current epoch, and current year [ &mut self.0.age_range.up_to_1d, self.0.epoch.mut_vec_from_height(height), + self.0.year.mut_vec_from_timestamp(timestamp), ] .into_iter() .for_each(|v| { diff --git a/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/send.rs b/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/send.rs index b497a7a4a..05633fe9f 100644 --- a/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/send.rs +++ b/crates/brk_computer/src/stateful/cohorts/utxo_cohorts/send.rs @@ -1,7 +1,7 @@ //! Processing spent inputs (UTXOs being spent). use brk_grouper::{Filter, Filtered, TimeFilter}; -use brk_types::{CheckedSub, HalvingEpoch, Height}; +use brk_types::{CheckedSub, HalvingEpoch, Height, Year}; use rustc_hash::FxHashMap; use vecdb::VecIndex; @@ -26,12 +26,13 @@ impl UTXOCohorts { return; } - // Time-based cohorts: age_range + epoch + // Time-based cohorts: age_range + epoch + year let mut time_cohorts: Vec<_> = self .0 .age_range .iter_mut() .chain(self.0.epoch.iter_mut()) + .chain(self.0.year.iter_mut()) .collect(); let last_block = chain_state.last().unwrap(); @@ -62,6 +63,7 @@ impl UTXOCohorts { Filter::Time(TimeFilter::LowerThan(to)) => *to > days_old, Filter::Time(TimeFilter::Range(range)) => range.contains(&days_old), Filter::Epoch(e) => *e == HalvingEpoch::from(height), + Filter::Year(y) => *y == Year::from(block_state.timestamp), _ => unreachable!(), }) .for_each(|vecs| { diff --git a/crates/brk_computer/src/stateful/compute/block_loop.rs b/crates/brk_computer/src/stateful/compute/block_loop.rs index 03ab134fe..75ac5baf6 100644 --- a/crates/brk_computer/src/stateful/compute/block_loop.rs +++ b/crates/brk_computer/src/stateful/compute/block_loop.rs @@ -420,7 +420,7 @@ pub fn process_blocks( }); // Main thread: Update UTXO cohorts - vecs.utxo_cohorts.receive(transacted, height, block_price); + vecs.utxo_cohorts.receive(transacted, height, timestamp, block_price); vecs.utxo_cohorts.send(height_to_sent, chain_state); }); diff --git a/crates/brk_computer/src/stateful/metrics/activity.rs b/crates/brk_computer/src/stateful/metrics/activity.rs index 7a533e17a..ec31725f3 100644 --- a/crates/brk_computer/src/stateful/metrics/activity.rs +++ b/crates/brk_computer/src/stateful/metrics/activity.rs @@ -42,7 +42,7 @@ impl ActivityMetrics { pub fn forced_import(cfg: &ImportConfig) -> Result { let v0 = Version::ZERO; let compute_dollars = cfg.compute_dollars(); - let sum = VecBuilderOptions::default().add_sum(); + let sum_cum = VecBuilderOptions::default().add_sum().add_cumulative(); Ok(Self { height_to_sent: EagerVec::forced_import(cfg.db, &cfg.name("sent"), cfg.version + v0)?, @@ -52,7 +52,7 @@ impl ActivityMetrics { &cfg.name("sent"), Source::None, cfg.version + v0, - sum, + sum_cum, compute_dollars, cfg.indexes, )?, @@ -75,7 +75,7 @@ impl ActivityMetrics { Source::Compute, cfg.version + v0, cfg.indexes, - sum, + sum_cum, )?, indexes_to_coindays_destroyed: ComputedVecsFromHeight::forced_import( @@ -84,7 +84,7 @@ impl ActivityMetrics { Source::Compute, cfg.version + v0, cfg.indexes, - sum, + sum_cum, )?, }) } diff --git a/crates/brk_grouper/src/by_year.rs b/crates/brk_grouper/src/by_year.rs new file mode 100644 index 000000000..a559aa393 --- /dev/null +++ b/crates/brk_grouper/src/by_year.rs @@ -0,0 +1,159 @@ +use brk_traversable::Traversable; +use brk_types::{Timestamp, Year}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; + +use super::Filter; + +#[derive(Default, Clone, Traversable)] +pub struct ByYear { + pub _2009: T, + pub _2010: T, + pub _2011: T, + pub _2012: T, + pub _2013: T, + pub _2014: T, + pub _2015: T, + pub _2016: T, + pub _2017: T, + pub _2018: T, + pub _2019: T, + pub _2020: T, + pub _2021: T, + pub _2022: T, + pub _2023: T, + pub _2024: T, + pub _2025: T, + pub _2026: T, +} + +impl ByYear { + pub fn new(mut create: F) -> Self + where + F: FnMut(Filter) -> T, + { + Self { + _2009: create(Filter::Year(Year::new(2009))), + _2010: create(Filter::Year(Year::new(2010))), + _2011: create(Filter::Year(Year::new(2011))), + _2012: create(Filter::Year(Year::new(2012))), + _2013: create(Filter::Year(Year::new(2013))), + _2014: create(Filter::Year(Year::new(2014))), + _2015: create(Filter::Year(Year::new(2015))), + _2016: create(Filter::Year(Year::new(2016))), + _2017: create(Filter::Year(Year::new(2017))), + _2018: create(Filter::Year(Year::new(2018))), + _2019: create(Filter::Year(Year::new(2019))), + _2020: create(Filter::Year(Year::new(2020))), + _2021: create(Filter::Year(Year::new(2021))), + _2022: create(Filter::Year(Year::new(2022))), + _2023: create(Filter::Year(Year::new(2023))), + _2024: create(Filter::Year(Year::new(2024))), + _2025: create(Filter::Year(Year::new(2025))), + _2026: create(Filter::Year(Year::new(2026))), + } + } + + pub fn iter(&self) -> impl Iterator { + [ + &self._2009, + &self._2010, + &self._2011, + &self._2012, + &self._2013, + &self._2014, + &self._2015, + &self._2016, + &self._2017, + &self._2018, + &self._2019, + &self._2020, + &self._2021, + &self._2022, + &self._2023, + &self._2024, + &self._2025, + &self._2026, + ] + .into_iter() + } + + pub fn iter_mut(&mut self) -> impl Iterator { + [ + &mut self._2009, + &mut self._2010, + &mut self._2011, + &mut self._2012, + &mut self._2013, + &mut self._2014, + &mut self._2015, + &mut self._2016, + &mut self._2017, + &mut self._2018, + &mut self._2019, + &mut self._2020, + &mut self._2021, + &mut self._2022, + &mut self._2023, + &mut self._2024, + &mut self._2025, + &mut self._2026, + ] + .into_iter() + } + + pub fn par_iter_mut(&mut self) -> impl ParallelIterator + where + T: Send + Sync, + { + [ + &mut self._2009, + &mut self._2010, + &mut self._2011, + &mut self._2012, + &mut self._2013, + &mut self._2014, + &mut self._2015, + &mut self._2016, + &mut self._2017, + &mut self._2018, + &mut self._2019, + &mut self._2020, + &mut self._2021, + &mut self._2022, + &mut self._2023, + &mut self._2024, + &mut self._2025, + &mut self._2026, + ] + .into_par_iter() + } + + pub fn mut_vec_from_timestamp(&mut self, timestamp: Timestamp) -> &mut T { + let year = Year::from(timestamp); + self.get_mut(year) + } + + pub fn get_mut(&mut self, year: Year) -> &mut T { + match u16::from(year) { + 2009 => &mut self._2009, + 2010 => &mut self._2010, + 2011 => &mut self._2011, + 2012 => &mut self._2012, + 2013 => &mut self._2013, + 2014 => &mut self._2014, + 2015 => &mut self._2015, + 2016 => &mut self._2016, + 2017 => &mut self._2017, + 2018 => &mut self._2018, + 2019 => &mut self._2019, + 2020 => &mut self._2020, + 2021 => &mut self._2021, + 2022 => &mut self._2022, + 2023 => &mut self._2023, + 2024 => &mut self._2024, + 2025 => &mut self._2025, + 2026 => &mut self._2026, + _ => todo!("Year {} not yet supported", u16::from(year)), + } + } +} diff --git a/crates/brk_grouper/src/filter.rs b/crates/brk_grouper/src/filter.rs index 367b623f6..6937aaac5 100644 --- a/crates/brk_grouper/src/filter.rs +++ b/crates/brk_grouper/src/filter.rs @@ -1,4 +1,4 @@ -use brk_types::{HalvingEpoch, OutputType, Sats}; +use brk_types::{HalvingEpoch, OutputType, Sats, Year}; use super::{AmountFilter, CohortContext, Term, TimeFilter}; @@ -9,6 +9,7 @@ pub enum Filter { Time(TimeFilter), Amount(AmountFilter), Epoch(HalvingEpoch), + Year(Year), Type(OutputType), } @@ -35,6 +36,7 @@ impl Filter { Filter::Time(t) => t.to_name_suffix(), Filter::Amount(a) => a.to_name_suffix(), Filter::Epoch(e) => format!("epoch_{}", usize::from(*e)), + Filter::Year(y) => format!("year_{}", u16::from(*y)), Filter::Type(t) => match t { OutputType::P2MS => "p2ms_outputs".to_string(), OutputType::Empty => "empty_outputs".to_string(), @@ -57,7 +59,7 @@ impl Filter { } let needs_prefix = match self { - Filter::All | Filter::Term(_) | Filter::Epoch(_) | Filter::Type(_) => false, + Filter::All | Filter::Term(_) | Filter::Epoch(_) | Filter::Year(_) | Filter::Type(_) => false, Filter::Time(_) | Filter::Amount(_) => true, }; diff --git a/crates/brk_grouper/src/lib.rs b/crates/brk_grouper/src/lib.rs index 0bc328e8a..89eff122f 100644 --- a/crates/brk_grouper/src/lib.rs +++ b/crates/brk_grouper/src/lib.rs @@ -8,6 +8,7 @@ mod by_amount_range; mod by_any_address; mod by_epoch; mod by_ge_amount; +mod by_year; mod by_lt_amount; mod by_max_age; mod by_min_age; @@ -31,6 +32,7 @@ pub use by_amount_range::*; pub use by_any_address::*; pub use by_epoch::*; pub use by_ge_amount::*; +pub use by_year::*; pub use by_lt_amount::*; pub use by_max_age::*; pub use by_min_age::*; diff --git a/crates/brk_grouper/src/utxo.rs b/crates/brk_grouper/src/utxo.rs index 50ddce915..176033f5a 100644 --- a/crates/brk_grouper/src/utxo.rs +++ b/crates/brk_grouper/src/utxo.rs @@ -3,7 +3,7 @@ use rayon::prelude::*; use crate::{ ByAgeRange, ByAmountRange, ByEpoch, ByGreatEqualAmount, ByLowerThanAmount, ByMaxAge, ByMinAge, - BySpendableType, ByTerm, Filter, + BySpendableType, ByTerm, ByYear, Filter, }; #[derive(Default, Clone, Traversable)] @@ -11,6 +11,7 @@ pub struct UTXOGroups { pub all: T, pub age_range: ByAgeRange, pub epoch: ByEpoch, + pub year: ByYear, pub min_age: ByMinAge, pub ge_amount: ByGreatEqualAmount, pub amount_range: ByAmountRange, @@ -29,6 +30,7 @@ impl UTXOGroups { all: create(Filter::All), age_range: ByAgeRange::new(&mut create), epoch: ByEpoch::new(&mut create), + year: ByYear::new(&mut create), min_age: ByMinAge::new(&mut create), ge_amount: ByGreatEqualAmount::new(&mut create), amount_range: ByAmountRange::new(&mut create), @@ -48,6 +50,7 @@ impl UTXOGroups { .chain(self.ge_amount.iter()) .chain(self.age_range.iter()) .chain(self.epoch.iter()) + .chain(self.year.iter()) .chain(self.amount_range.iter()) .chain(self.lt_amount.iter()) .chain(self.type_.iter()) @@ -62,6 +65,7 @@ impl UTXOGroups { .chain(self.ge_amount.iter_mut()) .chain(self.age_range.iter_mut()) .chain(self.epoch.iter_mut()) + .chain(self.year.iter_mut()) .chain(self.amount_range.iter_mut()) .chain(self.lt_amount.iter_mut()) .chain(self.type_.iter_mut()) @@ -79,6 +83,7 @@ impl UTXOGroups { .chain(self.ge_amount.par_iter_mut()) .chain(self.age_range.par_iter_mut()) .chain(self.epoch.par_iter_mut()) + .chain(self.year.par_iter_mut()) .chain(self.amount_range.par_iter_mut()) .chain(self.lt_amount.par_iter_mut()) .chain(self.type_.par_iter_mut()) @@ -88,6 +93,7 @@ impl UTXOGroups { self.age_range .iter() .chain(self.epoch.iter()) + .chain(self.year.iter()) .chain(self.amount_range.iter()) .chain(self.type_.iter()) } @@ -96,6 +102,7 @@ impl UTXOGroups { self.age_range .iter_mut() .chain(self.epoch.iter_mut()) + .chain(self.year.iter_mut()) .chain(self.amount_range.iter_mut()) .chain(self.type_.iter_mut()) } @@ -107,6 +114,7 @@ impl UTXOGroups { self.age_range .par_iter_mut() .chain(self.epoch.par_iter_mut()) + .chain(self.year.par_iter_mut()) .chain(self.amount_range.par_iter_mut()) .chain(self.type_.par_iter_mut()) } diff --git a/crates/brk_types/src/lib.rs b/crates/brk_types/src/lib.rs index 92860772a..9dec96ae2 100644 --- a/crates/brk_types/src/lib.rs +++ b/crates/brk_types/src/lib.rs @@ -152,6 +152,7 @@ mod vout; mod vsize; mod weekindex; mod weight; +mod year; mod yearindex; pub use address::*; @@ -304,4 +305,5 @@ pub use vout::*; pub use vsize::*; pub use weekindex::*; pub use weight::*; +pub use year::*; pub use yearindex::*; diff --git a/crates/brk_types/src/year.rs b/crates/brk_types/src/year.rs new file mode 100644 index 000000000..cd6b2ed7c --- /dev/null +++ b/crates/brk_types/src/year.rs @@ -0,0 +1,130 @@ +use std::{ + fmt::Debug, + ops::{Add, AddAssign, Div}, +}; + +use serde::{Deserialize, Serialize}; +use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; + +use super::{Date, Timestamp}; + +/// Bitcoin year (2009, 2010, ..., 2025+) +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash, Serialize, Deserialize, Pco, +)] +pub struct Year(u16); + +impl Year { + pub const GENESIS: Self = Self(2009); + + pub fn new(value: u16) -> Self { + Self(value) + } + + /// Returns the year as an index (0 = 2009, 1 = 2010, etc.) + pub fn to_index(self) -> usize { + (self.0 - 2009) as usize + } +} + +impl From for Year { + #[inline] + fn from(value: u16) -> Self { + Self(value) + } +} + +impl From for Year { + #[inline] + fn from(value: usize) -> Self { + Self(value as u16) + } +} + +impl From for usize { + #[inline] + fn from(value: Year) -> Self { + value.0 as usize + } +} + +impl From for u16 { + #[inline] + fn from(value: Year) -> Self { + value.0 + } +} + +impl Add for Year { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self::from(self.0 + rhs.0) + } +} + +impl AddAssign for Year { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs + } +} + +impl Add for Year { + type Output = Self; + + fn add(self, rhs: usize) -> Self::Output { + Self::from(self.0 + rhs as u16) + } +} + +impl From for Year { + #[inline] + fn from(value: Timestamp) -> Self { + Self(Date::from(value).year()) + } +} + +impl From for Year { + #[inline] + fn from(value: Date) -> Self { + Self(value.year()) + } +} + +impl CheckedSub for Year { + fn checked_sub(self, rhs: Self) -> Option { + self.0.checked_sub(rhs.0).map(Self) + } +} + +impl Div for Year { + type Output = Self; + fn div(self, rhs: usize) -> Self::Output { + Self::from(self.0 as usize / rhs) + } +} + +impl PrintableIndex for Year { + fn to_string() -> &'static str { + "year" + } + + fn to_possible_strings() -> &'static [&'static str] { + &["year"] + } +} + +impl std::fmt::Display for Year { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut buf = itoa::Buffer::new(); + let str = buf.format(self.0); + f.write_str(str) + } +} + +impl Formattable for Year { + #[inline(always)] + fn may_need_escaping() -> bool { + false + } +}