Compare commits

...

51 Commits

Author SHA1 Message Date
nym21 e543e4a5db release: v0.3.0-beta.6 2026-04-27 10:41:26 +02:00
nym21 9b639ef7d1 docs: update generated docs 2026-04-27 10:40:58 +02:00
nym21 07bc2d42b8 server: reorg 2026-04-27 10:35:18 +02:00
nym21 7a0b4b5890 global: big snapshot 2026-04-26 23:12:17 +02:00
nym21 2210443e37 website: lightweight-charts bump 2026-04-25 23:52:22 +02:00
nym21 8bf6570843 clients: bump versions 2026-04-24 10:33:08 +02:00
nym21 26a3b0f5e8 release: v0.3.0-beta.5 2026-04-23 23:19:37 +02:00
nym21 741c957f31 docs: update generated docs 2026-04-23 23:19:11 +02:00
nym21 e4496742a4 global: reused + mempool + favicon 2026-04-23 23:13:39 +02:00
nym21 ce00de5da8 version: bump 2026-04-23 10:20:57 +02:00
nym21 f5c50e69fc release: v0.3.0-beta.4 2026-04-22 22:30:09 +02:00
nym21 9709c2040d docs: update generated docs 2026-04-22 22:29:43 +02:00
nym21 3faa989691 global: cost basis -> urpd 2026-04-22 22:23:52 +02:00
nym21 84e924b77e global: refactor 2026-04-22 22:23:39 +02:00
nym21 c5b16e7048 release: v0.3.0-beta.3 2026-04-22 15:56:15 +02:00
nym21 c1335cec31 docs: update generated docs 2026-04-22 15:55:53 +02:00
nym21 bdc3ba1df6 mempool: snap 2026-04-22 15:30:08 +02:00
nym21 6afce0bbdc mempool: fixes 2026-04-21 12:43:50 +02:00
nym21 327873d010 website: fixes 2026-04-20 18:11:30 +02:00
nym21 08175009d2 website: snap 2026-04-19 20:28:12 +02:00
nym21 a5d3be465e website: snap 2026-04-19 17:13:47 +02:00
nym21 fd2b93367d global: snap 2026-04-18 17:23:12 +02:00
nym21 2a93f51e81 global: snap 2026-04-17 21:23:11 +02:00
nym21 008143ff00 mempool: fix pending tx info 2026-04-16 23:35:55 +02:00
nym21 d340855c8b global: snap 2026-04-16 22:17:41 +02:00
nym21 78d6d9d6f1 changelog: updated 2026-04-16 10:15:23 +02:00
nym21 5cc85b0619 release: v0.3.0-beta.2 2026-04-15 19:49:13 +02:00
nym21 7433ce0d0e docs: update generated docs 2026-04-15 19:48:50 +02:00
nym21 75a97b4da9 client: js: add cache support to text fetches 2026-04-15 19:19:59 +02:00
nym21 c23e0f2a3c global: snap 2026-04-15 13:04:22 +02:00
nym21 08ba4ad996 global: snap 2026-04-15 12:51:30 +02:00
nym21 39da441d14 global: snapshot 2026-04-14 22:53:10 +02:00
nym21 904ec93668 deps: bumped 2026-04-14 01:42:24 +02:00
nym21 4cd8d9eb56 reader: snap 2026-04-14 01:37:04 +02:00
nym21 283baca848 global: big snapshot part 2 2026-04-13 22:47:08 +02:00
nym21 765261648d global: big snapshot 2026-04-13 22:46:56 +02:00
nym21 c3cef71aa3 global: snap 2026-04-12 18:00:02 +02:00
nym21 18d9c166d8 computer: snap 2026-04-11 22:11:48 +02:00
nym21 286256ebf0 global: veccached change 2026-04-10 11:30:29 +02:00
nym21 12aae503c9 global: snapshot pre cached change 2026-04-10 10:27:07 +02:00
nym21 95e5168244 deps: bumped 2026-04-09 15:08:06 +02:00
nym21 5fd9fff9cf global: speed improvement part4 2026-04-09 15:06:19 +02:00
nym21 db5b3887f9 global: speed improvement part3 2026-04-09 14:58:25 +02:00
nym21 5a3e1b4e6e global: speed improvement part2 2026-04-09 14:02:26 +02:00
nym21 21a0226a19 global: speed improvement 2026-04-09 11:52:01 +02:00
nym21 c5c49f62d1 clients: bump versions 2026-04-08 22:19:28 +02:00
nym21 dac66c988d release: v0.3.0-beta.1 2026-04-08 17:12:32 +02:00
nym21 303d168681 docs: update generated docs 2026-04-08 17:12:03 +02:00
nym21 1ddb3385e2 website: css 2026-04-08 17:06:54 +02:00
nym21 eb75274dbf website: fixes 2026-04-08 13:11:03 +02:00
nym21 3a7887348c global: snapshot 2026-04-08 12:09:35 +02:00
1175 changed files with 36577 additions and 20672 deletions
+1
View File
@@ -39,6 +39,7 @@ flamegraph.svg
# AI
.claude/settings*
!CLAUDE.md
# Expand
expand.rs
+1
View File
@@ -0,0 +1 @@
Codex will review your output once you are done.
Generated
+149 -285
View File
File diff suppressed because it is too large Load Diff
+33 -34
View File
@@ -4,7 +4,7 @@ members = ["crates/*"]
package.description = "The Bitcoin Research Kit is a suite of tools designed to extract, compute and display data stored on a Bitcoin Core node"
package.license = "MIT"
package.edition = "2024"
package.version = "0.3.0-beta.0"
package.version = "0.3.0-beta.6"
package.homepage = "https://bitcoinresearchkit.org"
package.repository = "https://github.com/bitcoinresearchkit/brk"
package.readme = "README.md"
@@ -36,45 +36,44 @@ inherits = "release"
debug = true
[workspace.dependencies]
aide = { version = "0.16.0-alpha.3", features = ["axum-json", "axum-query"] }
axum = { version = "0.8.8", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
aide = { version = "0.16.0-alpha.4", features = ["axum-json", "axum-query"] }
axum = { version = "0.8.9", default-features = false, features = ["http1", "json", "query", "tokio", "tracing"] }
bitcoin = { version = "0.32.8", features = ["serde"] }
bitcoincore-rpc = "0.19.0"
brk_alloc = { version = "0.3.0-beta.0", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.0-beta.0", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.0-beta.0", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.0-beta.0", path = "crates/brk_cli" }
brk_client = { version = "0.3.0-beta.0", path = "crates/brk_client" }
brk_cohort = { version = "0.3.0-beta.0", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.0-beta.0", path = "crates/brk_computer" }
brk_error = { version = "0.3.0-beta.0", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.0-beta.0", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.0-beta.0", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.0-beta.0", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.0-beta.0", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.0-beta.0", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.0-beta.0", path = "crates/brk_oracle" }
brk_query = { version = "0.3.0-beta.0", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.0-beta.0", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.0-beta.0", path = "crates/brk_rpc" }
brk_server = { version = "0.3.0-beta.0", path = "crates/brk_server" }
brk_store = { version = "0.3.0-beta.0", path = "crates/brk_store" }
brk_traversable = { version = "0.3.0-beta.0", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.0-beta.0", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.0-beta.0", path = "crates/brk_types" }
brk_website = { version = "0.3.0-beta.0", path = "crates/brk_website" }
brk_alloc = { version = "0.3.0-beta.6", path = "crates/brk_alloc" }
brk_bencher = { version = "0.3.0-beta.6", path = "crates/brk_bencher" }
brk_bindgen = { version = "0.3.0-beta.6", path = "crates/brk_bindgen" }
brk_cli = { version = "0.3.0-beta.6", path = "crates/brk_cli" }
brk_client = { version = "0.3.0-beta.6", path = "crates/brk_client" }
brk_cohort = { version = "0.3.0-beta.6", path = "crates/brk_cohort" }
brk_computer = { version = "0.3.0-beta.6", path = "crates/brk_computer" }
brk_error = { version = "0.3.0-beta.6", path = "crates/brk_error" }
brk_fetcher = { version = "0.3.0-beta.6", path = "crates/brk_fetcher" }
brk_indexer = { version = "0.3.0-beta.6", path = "crates/brk_indexer" }
brk_iterator = { version = "0.3.0-beta.6", path = "crates/brk_iterator" }
brk_logger = { version = "0.3.0-beta.6", path = "crates/brk_logger" }
brk_mempool = { version = "0.3.0-beta.6", path = "crates/brk_mempool" }
brk_oracle = { version = "0.3.0-beta.6", path = "crates/brk_oracle" }
brk_query = { version = "0.3.0-beta.6", path = "crates/brk_query", features = ["tokio"] }
brk_reader = { version = "0.3.0-beta.6", path = "crates/brk_reader" }
brk_rpc = { version = "0.3.0-beta.6", path = "crates/brk_rpc" }
brk_server = { version = "0.3.0-beta.6", path = "crates/brk_server" }
brk_store = { version = "0.3.0-beta.6", path = "crates/brk_store" }
brk_traversable = { version = "0.3.0-beta.6", path = "crates/brk_traversable", features = ["pco", "derive"] }
brk_traversable_derive = { version = "0.3.0-beta.6", path = "crates/brk_traversable_derive" }
brk_types = { version = "0.3.0-beta.6", path = "crates/brk_types" }
brk_website = { version = "0.3.0-beta.6", path = "crates/brk_website" }
byteview = "0.10.1"
color-eyre = "0.6.5"
corepc-client = { package = "brk-corepc-client", version = "0.11.0", features = ["client-sync"] }
corepc-jsonrpc = { package = "brk-corepc-jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
corepc-jsonrpc = { package = "jsonrpc", version = "0.19.0", features = ["simple_http"], default-features = false }
corepc-types = { version = "0.12.0", features = ["std"], default-features = false }
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
fjall = "=3.0.4"
indexmap = { version = "2.13.0", features = ["serde"] }
jiff = { version = "0.2.23", features = ["perf-inline", "tz-system"], default-features = false }
indexmap = { version = "2.14.0", features = ["serde"] }
jiff = { version = "0.2.24", features = ["perf-inline", "tz-system"], default-features = false }
owo-colors = "4.3.0"
parking_lot = "0.12.5"
pco = "1.0.1"
rayon = "1.11.0"
rayon = "1.12.0"
rustc-hash = "2.1.2"
schemars = { version = "1.2.1", features = ["indexmap2"] }
serde = "1.0.228"
@@ -82,12 +81,12 @@ serde_bytes = "0.11.19"
serde_derive = "1.0.228"
serde_json = { version = "1.0.149", features = ["float_roundtrip", "preserve_order"] }
smallvec = "1.15.1"
tokio = { version = "1.50.0", features = ["rt-multi-thread"] }
tokio = { version = "1.52.1", features = ["rt-multi-thread"] }
tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
tower-layer = "0.3"
tracing = { version = "0.1", default-features = false, features = ["std"] }
ureq = { version = "3.3.0", features = ["json"] }
vecdb = { version = "0.9.3", features = ["derive", "serde_json", "pco", "schemars"] }
vecdb = { version = "0.10.2", features = ["derive", "serde_json", "pco", "schemars"] }
# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
[workspace.metadata.release]
+1 -1
View File
@@ -64,7 +64,7 @@ brk_mempool = { workspace = true, optional = true }
brk_oracle = { workspace = true, optional = true }
brk_query = { workspace = true, optional = true }
brk_reader = { workspace = true, optional = true }
brk_rpc = { workspace = true, optional = true, features = ["corepc"] }
brk_rpc = { workspace = true, optional = true }
brk_server = { workspace = true, optional = true }
brk_store = { workspace = true, optional = true }
brk_traversable = { workspace = true, optional = true }
+2 -2
View File
@@ -8,5 +8,5 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
libmimalloc-sys = { version = "0.1.44", features = ["extended"] }
mimalloc = { version = "0.1.48", features = ["v3"] }
libmimalloc-sys = { version = "0.1.47", features = ["extended"] }
mimalloc = { version = "0.1.50" }
+5 -2
View File
@@ -3,7 +3,10 @@
//! This module detects repeating tree structures and analyzes them
//! using the bottom-up name deconstruction algorithm.
use std::collections::{BTreeMap, BTreeSet};
use std::{
cmp::Reverse,
collections::{BTreeMap, BTreeSet},
};
use brk_types::{TreeNode, extract_json_type};
@@ -111,7 +114,7 @@ pub fn detect_structural_patterns(
// Also collects node bases for each tree path
let node_bases = analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len()));
patterns.sort_by_key(|p| Reverse(p.fields.len()));
(patterns, concrete_to_pattern, type_mappings, node_bases)
}
+1 -1
View File
@@ -31,7 +31,7 @@ impl ClientConstants {
let pools = pools();
let mut sorted_pools: Vec<_> = pools.iter().collect();
sorted_pools.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
sorted_pools.sort_by_key(|p| p.name.to_lowercase());
let pool_map: BTreeMap<PoolSlug, &'static str> =
sorted_pools.iter().map(|p| (p.slug(), p.name)).collect();
@@ -88,13 +88,14 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
let path = build_path_template(&endpoint.path, &endpoint.path_params);
let fetch_call = if endpoint.returns_json() {
"this.getJson(path, { signal, onUpdate })"
} else {
"this.getText(path, { signal, onUpdate })"
};
if endpoint.query_params.is_empty() {
writeln!(
output,
" return this.getJson(`{}`, {{ signal, onUpdate }});",
path
)
.unwrap();
writeln!(output, " const path = `{}`;", path).unwrap();
} else {
writeln!(output, " const params = new URLSearchParams();").unwrap();
for param in &endpoint.query_params {
@@ -122,17 +123,13 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
path
)
.unwrap();
if endpoint.supports_csv {
writeln!(output, " if (format === 'csv') {{").unwrap();
writeln!(output, " return this.getText(path, {{ signal }});").unwrap();
writeln!(output, " }}").unwrap();
writeln!(output, " return this.getJson(path, {{ signal, onUpdate }});").unwrap();
} else {
writeln!(output, " return this.getJson(path, {{ signal, onUpdate }});").unwrap();
}
}
if endpoint.supports_csv {
writeln!(output, " if (format === 'csv') return this.getText(path, {{ signal, onUpdate }});").unwrap();
}
writeln!(output, " return {};", fetch_call).unwrap();
writeln!(output, " }}\n").unwrap();
}
}
@@ -417,13 +417,15 @@ class BrkClientBase {{
}}
/**
* Make a GET request - races cache vs network, first to resolve calls onUpdate
* Make a GET request - races cache vs network, first to resolve calls onUpdate.
* Shared implementation backing `getJson` and `getText`.
* @template T
* @param {{string}} path
* @param {{(res: Response) => Promise<T>}} parse - Response body reader
* @param {{{{ onUpdate?: (value: T) => void, signal?: AbortSignal }}}} [options]
* @returns {{Promise<T>}}
*/
async getJson(path, {{ onUpdate, signal }} = {{}}) {{
async _getCached(path, parse, {{ onUpdate, signal }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
const cache = this._cache ?? await this._cachePromise;
@@ -435,52 +437,61 @@ class BrkClientBase {{
const cachePromise = cache?.match(url).then(async (res) => {{
cachedRes = res ?? null;
if (!res) return null;
const json = _addCamelGetters(await res.json());
const value = await parse(res);
if (!resolved && onUpdate) {{
resolved = true;
onUpdate(json);
onUpdate(value);
}}
return json;
return value;
}});
const networkPromise = this.get(path, {{ signal }}).then(async (res) => {{
const cloned = res.clone();
const json = _addCamelGetters(await res.json());
const value = await parse(res);
// Skip update if ETag matches and cache already delivered
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) {{
if (!resolved && onUpdate) {{
resolved = true;
onUpdate(json);
onUpdate(value);
}}
return json;
return value;
}}
resolved = true;
if (onUpdate) {{
onUpdate(json);
}}
if (onUpdate) onUpdate(value);
if (cache) _runIdle(() => cache.put(url, cloned));
return json;
return value;
}});
try {{
return await networkPromise;
}} catch (e) {{
// Network failed - wait for cache
const cachedJson = await cachePromise?.catch(() => null);
if (cachedJson) return cachedJson;
const cachedValue = await cachePromise?.catch(() => null);
if (cachedValue != null) return cachedValue;
throw e;
}}
}}
/**
* Make a GET request and return raw text (for CSV responses)
* Make a GET request expecting a JSON response. Cached and supports `onUpdate`.
* @template T
* @param {{string}} path
* @param {{{{ signal?: AbortSignal }}}} [options]
* @param {{{{ onUpdate?: (value: T) => void, signal?: AbortSignal }}}} [options]
* @returns {{Promise<T>}}
*/
getJson(path, options) {{
return this._getCached(path, async (res) => _addCamelGetters(await res.json()), options);
}}
/**
* Make a GET request expecting a text response (text/plain, text/csv, ...).
* Cached and supports `onUpdate`, same as `getJson`.
* @param {{string}} path
* @param {{{{ onUpdate?: (value: string) => void, signal?: AbortSignal }}}} [options]
* @returns {{Promise<string>}}
*/
async getText(path, {{ signal }} = {{}}) {{
const res = await this.get(path, {{ signal }});
return res.text();
getText(path, options) {{
return this._getCached(path, (res) => res.text(), options);
}}
/**
@@ -108,15 +108,14 @@ pub fn generate_main_client(
writeln!(output, " constructor(options) {{").unwrap();
writeln!(output, " super(options);").unwrap();
writeln!(output, " /** @type {{SeriesTree}} */").unwrap();
writeln!(output, " this.series = this._buildTree('');").unwrap();
writeln!(output, " this.series = this._buildTree();").unwrap();
writeln!(output, " }}\n").unwrap();
writeln!(output, " /**").unwrap();
writeln!(output, " * @private").unwrap();
writeln!(output, " * @param {{string}} basePath").unwrap();
writeln!(output, " * @returns {{SeriesTree}}").unwrap();
writeln!(output, " */").unwrap();
writeln!(output, " _buildTree(basePath) {{").unwrap();
writeln!(output, " _buildTree() {{").unwrap();
writeln!(output, " return {{").unwrap();
let mut generated = BTreeSet::new();
generate_tree_initializer(
+1 -1
View File
@@ -17,7 +17,7 @@ brk_logger = { workspace = true }
brk_mempool = { workspace = true }
brk_query = { workspace = true }
brk_reader = { workspace = true }
brk_rpc = { workspace = true, features = ["corepc"] }
brk_rpc = { workspace = true }
brk_server = { workspace = true }
brk_types = { workspace = true }
lexopt = "0.3"
+77 -1
View File
@@ -5,7 +5,9 @@ use std::{
use brk_error::{Error, Result};
use brk_rpc::{Auth, Client};
use brk_server::Website;
use brk_server::{
CdnCacheMode, DEFAULT_CACHE_SIZE, DEFAULT_MAX_WEIGHT, DEFAULT_MAX_WEIGHT_LOCALHOST, Website,
};
use brk_types::Port;
use owo_colors::OwoColorize;
use serde::{Deserialize, Deserializer, Serialize};
@@ -23,6 +25,18 @@ pub struct Config {
#[serde(default, deserialize_with = "default_on_error")]
website: Option<Website>,
#[serde(default, deserialize_with = "default_on_error")]
cdn: Option<bool>,
#[serde(default, deserialize_with = "default_on_error")]
maxweight: Option<usize>,
#[serde(default, deserialize_with = "default_on_error")]
maxweightlocal: Option<usize>,
#[serde(default, deserialize_with = "default_on_error")]
cachesize: Option<usize>,
#[serde(default, deserialize_with = "default_on_error")]
bitcoindir: Option<String>,
@@ -66,6 +80,18 @@ impl Config {
if let Some(v) = config_args.website {
config.website = Some(v);
}
if let Some(v) = config_args.cdn {
config.cdn = Some(v);
}
if let Some(v) = config_args.maxweight {
config.maxweight = Some(v);
}
if let Some(v) = config_args.maxweightlocal {
config.maxweightlocal = Some(v);
}
if let Some(v) = config_args.cachesize {
config.cachesize = Some(v);
}
if let Some(v) = config_args.bitcoindir {
config.bitcoindir = Some(v);
}
@@ -112,6 +138,16 @@ impl Config {
Long("brkdir") => config.brkdir = Some(parser.value().unwrap().parse().unwrap()),
Long("brkport") => config.brkport = Some(parser.value().unwrap().parse().unwrap()),
Long("website") => config.website = Some(parser.value().unwrap().parse().unwrap()),
Long("cdn") => config.cdn = Some(parser.value().unwrap().parse().unwrap()),
Long("maxweight") => {
config.maxweight = Some(parser.value().unwrap().parse().unwrap())
}
Long("maxweightlocal") => {
config.maxweightlocal = Some(parser.value().unwrap().parse().unwrap())
}
Long("cachesize") => {
config.cachesize = Some(parser.value().unwrap().parse().unwrap())
}
Long("bitcoindir") => {
config.bitcoindir = Some(parser.value().unwrap().parse().unwrap())
}
@@ -171,6 +207,26 @@ impl Config {
"<BOOL|PATH>".bright_black(),
"[true]".bright_black()
);
println!(
" --cdn {} Aggressive CDN cache, requires purge on deploy {}",
"<BOOL>".bright_black(),
"[false]".bright_black()
);
println!(
" --maxweight {} Max series response weight in bytes for external clients {}",
"<BYTES>".bright_black(),
format!("[{}]", DEFAULT_MAX_WEIGHT).bright_black()
);
println!(
" --maxweightlocal {} Max series response weight in bytes for loopback clients {}",
"<BYTES>".bright_black(),
format!("[{}]", DEFAULT_MAX_WEIGHT_LOCALHOST).bright_black()
);
println!(
" --cachesize {} LRU capacity for the in-process response cache {}",
"<ENTRIES>".bright_black(),
format!("[{}]", DEFAULT_CACHE_SIZE).bright_black()
);
println!();
println!(
" --bitcoindir {} Bitcoin directory {}",
@@ -333,6 +389,26 @@ Finally, you can run the program with '-h' for help."
self.website.clone().unwrap_or_default()
}
pub fn cdn_cache_mode(&self) -> CdnCacheMode {
if self.cdn.unwrap_or(false) {
CdnCacheMode::Aggressive
} else {
CdnCacheMode::Live
}
}
pub fn max_weight(&self) -> usize {
self.maxweight.unwrap_or(DEFAULT_MAX_WEIGHT)
}
pub fn max_weight_localhost(&self) -> usize {
self.maxweightlocal.unwrap_or(DEFAULT_MAX_WEIGHT_LOCALHOST)
}
pub fn cache_size(&self) -> usize {
self.cachesize.unwrap_or(DEFAULT_CACHE_SIZE)
}
pub fn brkport(&self) -> Option<Port> {
self.brkport
}
+16 -8
View File
@@ -13,7 +13,7 @@ use brk_indexer::Indexer;
use brk_mempool::Mempool;
use brk_query::AsyncQuery;
use brk_reader::Reader;
use brk_server::Server;
use brk_server::{Server, ServerConfig};
use tracing::info;
use vecdb::Exit;
@@ -60,21 +60,29 @@ pub fn main() -> anyhow::Result<()> {
let mempool = Mempool::new(&client);
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool.clone()));
let mempool_clone = mempool.clone();
let query_clone = query.clone();
thread::spawn(move || {
mempool_clone.start();
mempool_clone.start_with(|| {
query_clone.sync(|q| q.fill_mempool_prevouts());
});
});
let query = AsyncQuery::build(&reader, &indexer, &computer, Some(mempool));
let data_path = config.brkdir();
let website = config.website();
let server_config = ServerConfig {
data_path: config.brkdir(),
website: config.website(),
cdn_cache_mode: config.cdn_cache_mode(),
max_weight: config.max_weight(),
max_weight_localhost: config.max_weight_localhost(),
cache_size: config.cache_size(),
};
let port = config.brkport();
let future = async move {
let server = Server::new(&query, data_path, website);
let server = Server::new(&query, server_config);
tokio::spawn(async move {
server.serve(port).await.unwrap();
+1304 -573
View File
File diff suppressed because it is too large Load Diff
+63 -2
View File
@@ -1,16 +1,34 @@
use std::ops::{Add, AddAssign};
use brk_traversable::Traversable;
use brk_types::OutputType;
use rayon::prelude::*;
use super::{SpendableType, UnspendableType};
use super::{Filter, SpendableType, UnspendableType};
#[derive(Default, Clone, Debug)]
pub const OP_RETURN: &str = "op_return";
#[derive(Default, Clone, Debug, Traversable)]
pub struct ByType<T> {
#[traversable(flatten)]
pub spendable: SpendableType<T>,
#[traversable(flatten)]
pub unspendable: UnspendableType<T>,
}
impl<T> ByType<T> {
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
where
F: FnMut(Filter, &'static str) -> Result<T, E>,
{
Ok(Self {
spendable: SpendableType::try_new(&mut create)?,
unspendable: UnspendableType {
op_return: create(Filter::Type(OutputType::OpReturn), OP_RETURN)?,
},
})
}
pub fn get(&self, output_type: OutputType) -> &T {
match output_type {
OutputType::P2PK65 => &self.spendable.p2pk65,
@@ -44,6 +62,49 @@ impl<T> ByType<T> {
OutputType::OpReturn => &mut self.unspendable.op_return,
}
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.spendable
.iter()
.chain(std::iter::once(&self.unspendable.op_return))
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.spendable
.iter_mut()
.chain(std::iter::once(&mut self.unspendable.op_return))
}
pub fn par_iter_mut(&mut self) -> impl ParallelIterator<Item = &mut T>
where
T: Send + Sync,
{
let Self {
spendable,
unspendable,
} = self;
spendable
.par_iter_mut()
.chain([&mut unspendable.op_return].into_par_iter())
}
pub fn iter_typed(&self) -> impl Iterator<Item = (OutputType, &T)> {
self.spendable
.iter_typed()
.chain(std::iter::once((
OutputType::OpReturn,
&self.unspendable.op_return,
)))
}
pub fn iter_typed_mut(&mut self) -> impl Iterator<Item = (OutputType, &mut T)> {
self.spendable
.iter_typed_mut()
.chain(std::iter::once((
OutputType::OpReturn,
&mut self.unspendable.op_return,
)))
}
}
impl<T> Add for ByType<T>
+17
View File
@@ -116,6 +116,23 @@ impl<T> SpendableType<T> {
})
}
pub fn get(&self, output_type: OutputType) -> &T {
match output_type {
OutputType::P2PK65 => &self.p2pk65,
OutputType::P2PK33 => &self.p2pk33,
OutputType::P2PKH => &self.p2pkh,
OutputType::P2MS => &self.p2ms,
OutputType::P2SH => &self.p2sh,
OutputType::P2WPKH => &self.p2wpkh,
OutputType::P2WSH => &self.p2wsh,
OutputType::P2TR => &self.p2tr,
OutputType::P2A => &self.p2a,
OutputType::Unknown => &self.unknown,
OutputType::Empty => &self.empty,
_ => unreachable!(),
}
}
pub fn get_mut(&mut self, output_type: OutputType) -> &mut T {
match output_type {
OutputType::P2PK65 => &mut self.p2pk65,
+1 -1
View File
@@ -15,7 +15,7 @@ brk_cohort = { workspace = true }
brk_indexer = { workspace = true }
brk_oracle = { workspace = true }
brk_logger = { workspace = true }
brk_rpc = { workspace = true, features = ["corepc"] }
brk_rpc = { workspace = true }
brk_traversable = { workspace = true }
brk_types = { workspace = true }
derive_more = { workspace = true }
@@ -7,7 +7,7 @@ use crate::{
indexes,
internal::{
BlockCountTarget1m, BlockCountTarget1w, BlockCountTarget1y, BlockCountTarget24h,
CachedWindowStarts, ConstantVecs, PerBlockCumulativeRolling, Windows,
ConstantVecs, PerBlockCumulativeRolling, WindowStartVec, Windows,
},
};
@@ -16,7 +16,7 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self {
target: Windows {
@@ -30,7 +30,7 @@ impl Vecs {
self.blocks_to_retarget.height.compute_transform(
starting_indexes.height,
&indexes.height.identity,
&indexes.height.epoch,
|(h, ..)| (h, StoredU32::from(h.left_before_next_diff_adj())),
exit,
)?;
@@ -20,10 +20,10 @@ impl Vecs {
) -> Result<Self> {
let v2 = Version::TWO;
let hashrate = LazyPerBlock::from_height_source::<DifficultyToHashF64>(
let hashrate = LazyPerBlock::from_height_source::<DifficultyToHashF64, _>(
"difficulty_hashrate",
version,
indexer.vecs.blocks.difficulty.read_only_boxed_clone(),
indexer.vecs.blocks.difficulty.read_only_clone(),
indexes,
);
@@ -40,7 +40,7 @@ impl Vecs {
Ok(Self {
value: Resolutions::forced_import(
"difficulty",
indexer.vecs.blocks.difficulty.read_only_boxed_clone(),
indexer.vecs.blocks.difficulty.read_only_clone(),
version,
indexes,
),
@@ -21,7 +21,7 @@ impl Vecs {
self.blocks_to_halving.height.compute_transform(
starting_indexes.height,
&indexes.height.identity,
&indexes.height.halving,
|(h, ..)| (h, StoredU32::from(h.left_before_next_halving())),
exit,
)?;
+5 -5
View File
@@ -24,11 +24,11 @@ impl Vecs {
let version = parent_version;
let lookback = LookbackVecs::forced_import(&db, version)?;
let cached_starts = &lookback.cached_window_starts;
let count = CountVecs::forced_import(&db, version, indexes, cached_starts)?;
let interval = IntervalVecs::forced_import(&db, version, indexes, cached_starts)?;
let size = SizeVecs::forced_import(&db, version, indexes, cached_starts)?;
let weight = WeightVecs::forced_import(&db, version, indexes, cached_starts, &size)?;
let cached_starts = lookback.cached_window_starts();
let count = CountVecs::forced_import(&db, version, indexes, &cached_starts)?;
let interval = IntervalVecs::forced_import(&db, version, indexes, &cached_starts)?;
let size = SizeVecs::forced_import(&db, version, indexes, &cached_starts)?;
let weight = WeightVecs::forced_import(&db, version, indexes, &cached_starts, &size)?;
let difficulty = DifficultyVecs::forced_import(&db, version, indexer, indexes)?;
let halving = HalvingVecs::forced_import(&db, version, indexes)?;
@@ -5,7 +5,7 @@ use vecdb::Database;
use super::Vecs;
use crate::{
indexes,
internal::{CachedWindowStarts, PerBlockRollingAverage},
internal::{PerBlockRollingAverage, WindowStartVec, Windows},
};
impl Vecs {
@@ -13,7 +13,7 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let interval = PerBlockRollingAverage::forced_import(
db,
@@ -8,5 +8,5 @@ use crate::internal::PerBlockRollingAverage;
#[derive(Deref, DerefMut, Traversable)]
pub struct Vecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub PerBlockRollingAverage<Timestamp, M>,
#[traversable(flatten)] pub PerBlockRollingAverage<Timestamp, Timestamp, M>,
);
+28 -29
View File
@@ -8,17 +8,15 @@ use vecdb::{
use crate::{
indexes,
internal::{CachedWindowStarts, WindowStarts, Windows},
internal::{WindowStartVec, WindowStarts, Windows},
};
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
#[traversable(skip)]
pub cached_window_starts: CachedWindowStarts,
pub _1h: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _24h: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 1d
pub _24h: CachedVec<M::Stored<EagerVec<PcoVec<Height, Height>>>>, // 1d
pub _3d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _1w: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 7d
pub _1w: CachedVec<M::Stored<EagerVec<PcoVec<Height, Height>>>>, // 7d
pub _8d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _9d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _12d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
@@ -26,7 +24,7 @@ pub struct Vecs<M: StorageMode = Rw> {
pub _2w: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 14d
pub _21d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _26d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _1m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 30d
pub _1m: CachedVec<M::Stored<EagerVec<PcoVec<Height, Height>>>>, // 30d
pub _34d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _55d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _2m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 60d
@@ -43,7 +41,7 @@ pub struct Vecs<M: StorageMode = Rw> {
pub _9m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 270d
pub _350d: M::Stored<EagerVec<PcoVec<Height, Height>>>,
pub _12m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 360d
pub _1y: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 365d
pub _1y: CachedVec<M::Stored<EagerVec<PcoVec<Height, Height>>>>, // 365d
pub _14m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 420d
pub _2y: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 730d
pub _26m: M::Stored<EagerVec<PcoVec<Height, Height>>>, // 780d
@@ -106,19 +104,11 @@ impl Vecs {
let _14y = ImportableVec::forced_import(db, "height_14y_ago", version)?;
let _26y = ImportableVec::forced_import(db, "height_26y_ago", version)?;
let cached_window_starts = CachedWindowStarts(Windows {
_24h: CachedVec::new(&_24h),
_1w: CachedVec::new(&_1w),
_1m: CachedVec::new(&_1m),
_1y: CachedVec::new(&_1y),
});
Ok(Self {
cached_window_starts,
_1h,
_24h,
_24h: CachedVec::wrap(_24h),
_3d,
_1w,
_1w: CachedVec::wrap(_1w),
_8d,
_9d,
_12d,
@@ -126,7 +116,7 @@ impl Vecs {
_2w,
_21d,
_26d,
_1m,
_1m: CachedVec::wrap(_1m),
_34d,
_55d,
_2m,
@@ -143,7 +133,7 @@ impl Vecs {
_9m,
_350d,
_12m,
_1y,
_1y: CachedVec::wrap(_1y),
_14m,
_2y,
_26m,
@@ -161,8 +151,8 @@ impl Vecs {
})
}
pub fn window_starts(&self) -> WindowStarts<'_> {
WindowStarts {
pub fn cached_window_starts(&self) -> Windows<&WindowStartVec> {
Windows {
_24h: &self._24h,
_1w: &self._1w,
_1m: &self._1m,
@@ -170,11 +160,20 @@ impl Vecs {
}
}
pub fn window_starts(&self) -> WindowStarts<'_> {
WindowStarts {
_24h: &self._24h.inner,
_1w: &self._1w.inner,
_1m: &self._1m.inner,
_1y: &self._1y.inner,
}
}
pub fn start_vec(&self, days: usize) -> &EagerVec<PcoVec<Height, Height>> {
match days {
1 => &self._24h,
1 => &self._24h.inner,
3 => &self._3d,
7 => &self._1w,
7 => &self._1w.inner,
8 => &self._8d,
9 => &self._9d,
12 => &self._12d,
@@ -182,7 +181,7 @@ impl Vecs {
14 => &self._2w,
21 => &self._21d,
26 => &self._26d,
30 => &self._1m,
30 => &self._1m.inner,
34 => &self._34d,
55 => &self._55d,
60 => &self._2m,
@@ -199,7 +198,7 @@ impl Vecs {
270 => &self._9m,
350 => &self._350d,
360 => &self._12m,
365 => &self._1y,
365 => &self._1y.inner,
420 => &self._14m,
730 => &self._2y,
780 => &self._26m,
@@ -225,9 +224,9 @@ impl Vecs {
exit: &Exit,
) -> Result<()> {
self.compute_rolling_start_hours(indexes, starting_indexes, exit, 1, |s| &mut s._1h)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 1, |s| &mut s._24h)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 1, |s| &mut s._24h.inner)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 3, |s| &mut s._3d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 7, |s| &mut s._1w)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 7, |s| &mut s._1w.inner)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 8, |s| &mut s._8d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 9, |s| &mut s._9d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 12, |s| &mut s._12d)?;
@@ -235,7 +234,7 @@ impl Vecs {
self.compute_rolling_start(indexes, starting_indexes, exit, 14, |s| &mut s._2w)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 21, |s| &mut s._21d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 26, |s| &mut s._26d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 30, |s| &mut s._1m)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 30, |s| &mut s._1m.inner)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 34, |s| &mut s._34d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 55, |s| &mut s._55d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 60, |s| &mut s._2m)?;
@@ -252,7 +251,7 @@ impl Vecs {
self.compute_rolling_start(indexes, starting_indexes, exit, 270, |s| &mut s._9m)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 350, |s| &mut s._350d)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 360, |s| &mut s._12m)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 365, |s| &mut s._1y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 365, |s| &mut s._1y.inner)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 420, |s| &mut s._14m)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 730, |s| &mut s._2y)?;
self.compute_rolling_start(indexes, starting_indexes, exit, 780, |s| &mut s._26m)?;
@@ -5,7 +5,7 @@ use vecdb::Database;
use super::Vecs;
use crate::{
indexes,
internal::{CachedWindowStarts, PerBlockFull, PerBlockRolling},
internal::{PerBlockFull, PerBlockRolling, WindowStartVec, Windows},
};
impl Vecs {
@@ -13,7 +13,7 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self {
vbytes: PerBlockFull::forced_import(
@@ -6,7 +6,7 @@ use super::Vecs;
use crate::{
blocks::SizeVecs,
indexes,
internal::{CachedWindowStarts, LazyPerBlockRolling, PercentVec, VBytesToWeight},
internal::{LazyPerBlockRolling, PercentVec, VBytesToWeight, WindowStartVec, Windows},
};
impl Vecs {
@@ -14,7 +14,7 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
size: &SizeVecs,
) -> Result<Self> {
let weight = LazyPerBlockRolling::from_per_block_full::<VBytesToWeight>(
@@ -6,7 +6,7 @@ use super::Vecs;
use crate::{
indexes,
internal::{
CachedWindowStarts, LazyPerBlock, OneMinusF64, PerBlock, PerBlockCumulativeRolling,
LazyPerBlock, OneMinusF64, PerBlock, PerBlockCumulativeRolling, WindowStartVec, Windows,
},
};
@@ -15,7 +15,7 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let liveliness = PerBlock::forced_import(db, "liveliness", version, indexes)?;
+2 -2
View File
@@ -13,14 +13,14 @@ use super::{
ValueVecs, Vecs,
};
use crate::internal::CachedWindowStarts;
use crate::internal::{WindowStartVec, Windows};
impl Vecs {
pub(crate) fn forced_import(
parent_path: &Path,
parent_version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let db = open_db(parent_path, DB_NAME, 250_000)?;
let version = parent_version;
@@ -23,7 +23,7 @@ impl Vecs {
self.hodl_bank.compute_cumulative_transformed_binary(
starting_indexes.height,
&prices.cached_spot_usd,
&prices.spot.usd.height,
&self.vocdd_median_1y,
|price, median| StoredF64::from(f64::from(price) - f64::from(median)),
exit,
@@ -31,7 +31,7 @@ impl Vecs {
self.value.height.compute_divide(
starting_indexes.height,
&prices.cached_spot_usd,
&prices.spot.usd.height,
&self.hodl_bank,
exit,
)?;
@@ -3,7 +3,7 @@ use brk_types::Version;
use vecdb::Database;
use super::Vecs;
use crate::{indexes, internal::AmountPerBlock};
use crate::{indexes, internal::ValuePerBlock};
impl Vecs {
pub(crate) fn forced_import(
@@ -12,8 +12,8 @@ impl Vecs {
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
vaulted: AmountPerBlock::forced_import(db, "vaulted_supply", version, indexes)?,
active: AmountPerBlock::forced_import(db, "active_supply", version, indexes)?,
vaulted: ValuePerBlock::forced_import(db, "vaulted_supply", version, indexes)?,
active: ValuePerBlock::forced_import(db, "active_supply", version, indexes)?,
})
}
}
@@ -1,10 +1,10 @@
use brk_traversable::Traversable;
use vecdb::{Rw, StorageMode};
use crate::internal::AmountPerBlock;
use crate::internal::ValuePerBlock;
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
pub vaulted: AmountPerBlock<M>,
pub active: AmountPerBlock<M>,
pub vaulted: ValuePerBlock<M>,
pub active: ValuePerBlock<M>,
}
@@ -24,7 +24,7 @@ impl Vecs {
.compute(starting_indexes.height, exit, |vec| {
vec.compute_multiply(
starting_indexes.height,
&prices.cached_spot_usd,
&prices.spot.usd.height,
&coinblocks_destroyed.block,
exit,
)?;
@@ -34,7 +34,7 @@ impl Vecs {
self.created.compute(starting_indexes.height, exit, |vec| {
vec.compute_multiply(
starting_indexes.height,
&prices.cached_spot_usd,
&prices.spot.usd.height,
&activity.coinblocks_created.block,
exit,
)?;
@@ -44,7 +44,7 @@ impl Vecs {
self.stored.compute(starting_indexes.height, exit, |vec| {
vec.compute_multiply(
starting_indexes.height,
&prices.cached_spot_usd,
&prices.spot.usd.height,
&activity.coinblocks_stored.block,
exit,
)?;
@@ -57,7 +57,7 @@ impl Vecs {
self.vocdd.compute(starting_indexes.height, exit, |vec| {
vec.compute_transform3(
starting_indexes.height,
&prices.cached_spot_usd,
&prices.spot.usd.height,
&coindays_destroyed.block,
circulating_supply,
|(i, price, cdd, supply, _): (_, Dollars, StoredF64, Bitcoin, _)| {
@@ -5,7 +5,7 @@ use vecdb::Database;
use super::Vecs;
use crate::{
indexes,
internal::{CachedWindowStarts, PerBlockCumulativeRolling},
internal::{PerBlockCumulativeRolling, WindowStartVec, Windows},
};
impl Vecs {
@@ -13,7 +13,7 @@ impl Vecs {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self {
destroyed: PerBlockCumulativeRolling::forced_import(
@@ -7,19 +7,20 @@
//! | `receiving` | Unique addresses that received this block |
//! | `sending` | Unique addresses that sent this block |
//! | `reactivated` | Addresses that were empty and now have funds |
//! | `both` | Addresses that both sent AND received same block |
//! | `bidirectional` | Addresses that both sent AND received in same block |
//! | `active` | Distinct addresses involved (sent received) |
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, StoredU32, Version};
use brk_types::{Height, StoredU32, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
use crate::{
indexes,
internal::{CachedWindowStarts, PerBlockRollingAverage},
internal::{PerBlockRollingAverage, WindowStartVec, Windows},
};
/// Per-block activity counts - reset each block.
@@ -28,7 +29,7 @@ pub struct BlockActivityCounts {
pub reactivated: u32,
pub sending: u32,
pub receiving: u32,
pub both: u32,
pub bidirectional: u32,
}
impl BlockActivityCounts {
@@ -56,7 +57,7 @@ impl AddrTypeToActivityCounts {
total.reactivated += counts.reactivated;
total.sending += counts.sending;
total.receiving += counts.receiving;
total.both += counts.both;
total.bidirectional += counts.bidirectional;
}
total
}
@@ -65,45 +66,61 @@ impl AddrTypeToActivityCounts {
/// Activity count vectors for a single category (e.g., one address type or "all").
#[derive(Traversable)]
pub struct ActivityCountVecs<M: StorageMode = Rw> {
pub reactivated: PerBlockRollingAverage<StoredU32, M>,
pub sending: PerBlockRollingAverage<StoredU32, M>,
pub receiving: PerBlockRollingAverage<StoredU32, M>,
pub both: PerBlockRollingAverage<StoredU32, M>,
pub reactivated: PerBlockRollingAverage<StoredU32, StoredU64, M>,
pub sending: PerBlockRollingAverage<StoredU32, StoredU64, M>,
pub receiving: PerBlockRollingAverage<StoredU32, StoredU64, M>,
pub bidirectional: PerBlockRollingAverage<StoredU32, StoredU64, M>,
/// Distinct addresses involved in this block (sent received),
/// computed at push time as `sending + receiving - bidirectional`
/// via inclusion-exclusion. For per-type instances this is
/// per-type. For the `all` aggregate it's the cross-type total.
pub active: PerBlockRollingAverage<StoredU32, StoredU64, M>,
}
impl ActivityCountVecs {
/// `prefix` is prepended to each field's disk name. Use `""` for the
/// "all" aggregate and `"{type}_"` for per-address-type instances.
/// Field names are suffixed with `_addrs` so the final disk series
/// are e.g. `active_addrs`, `p2tr_bidirectional_addrs`.
pub(crate) fn forced_import(
db: &Database,
name: &str,
prefix: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self {
reactivated: PerBlockRollingAverage::forced_import(
db,
&format!("{name}_reactivated"),
&format!("{prefix}reactivated_addrs"),
version,
indexes,
cached_starts,
)?,
sending: PerBlockRollingAverage::forced_import(
db,
&format!("{name}_sending"),
&format!("{prefix}sending_addrs"),
version,
indexes,
cached_starts,
)?,
receiving: PerBlockRollingAverage::forced_import(
db,
&format!("{name}_receiving"),
&format!("{prefix}receiving_addrs"),
version,
indexes,
cached_starts,
)?,
both: PerBlockRollingAverage::forced_import(
bidirectional: PerBlockRollingAverage::forced_import(
db,
&format!("{name}_both"),
&format!("{prefix}bidirectional_addrs"),
version,
indexes,
cached_starts,
)?,
active: PerBlockRollingAverage::forced_import(
db,
&format!("{prefix}active_addrs"),
version,
indexes,
cached_starts,
@@ -117,7 +134,8 @@ impl ActivityCountVecs {
.len()
.min(self.sending.block.len())
.min(self.receiving.block.len())
.min(self.both.block.len())
.min(self.bidirectional.block.len())
.min(self.active.block.len())
}
pub(crate) fn par_iter_height_mut(
@@ -125,9 +143,10 @@ impl ActivityCountVecs {
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
[
&mut self.reactivated.block as &mut dyn AnyStoredVec,
&mut self.sending.block as &mut dyn AnyStoredVec,
&mut self.receiving.block as &mut dyn AnyStoredVec,
&mut self.both.block as &mut dyn AnyStoredVec,
&mut self.sending.block,
&mut self.receiving.block,
&mut self.bidirectional.block,
&mut self.active.block,
]
.into_par_iter()
}
@@ -136,7 +155,8 @@ impl ActivityCountVecs {
self.reactivated.block.reset()?;
self.sending.block.reset()?;
self.receiving.block.reset()?;
self.both.block.reset()?;
self.bidirectional.block.reset()?;
self.active.block.reset()?;
Ok(())
}
@@ -145,14 +165,19 @@ impl ActivityCountVecs {
self.reactivated.block.push(counts.reactivated.into());
self.sending.block.push(counts.sending.into());
self.receiving.block.push(counts.receiving.into());
self.both.block.push(counts.both.into());
self.bidirectional
.block
.push(counts.bidirectional.into());
let active = counts.sending + counts.receiving - counts.bidirectional;
self.active.block.push(active.into());
}
pub(crate) fn compute_rest(&mut self, max_from: Height, exit: &Exit) -> Result<()> {
self.reactivated.compute_rest(max_from, exit)?;
self.sending.compute_rest(max_from, exit)?;
self.receiving.compute_rest(max_from, exit)?;
self.both.compute_rest(max_from, exit)?;
self.bidirectional.compute_rest(max_from, exit)?;
self.active.compute_rest(max_from, exit)?;
Ok(())
}
}
@@ -171,16 +196,15 @@ impl From<ByAddrType<ActivityCountVecs>> for AddrTypeToActivityCountVecs {
impl AddrTypeToActivityCountVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self::from(ByAddrType::<ActivityCountVecs>::new_with_name(
|type_name| {
ActivityCountVecs::forced_import(
db,
&format!("{type_name}_{name}"),
&format!("{type_name}_"),
version,
indexes,
cached_starts,
@@ -205,7 +229,8 @@ impl AddrTypeToActivityCountVecs {
vecs.push(&mut type_vecs.reactivated.block);
vecs.push(&mut type_vecs.sending.block);
vecs.push(&mut type_vecs.receiving.block);
vecs.push(&mut type_vecs.both.block);
vecs.push(&mut type_vecs.bidirectional.block);
vecs.push(&mut type_vecs.active.block);
}
vecs.into_par_iter()
}
@@ -243,16 +268,14 @@ pub struct AddrActivityVecs<M: StorageMode = Rw> {
impl AddrActivityVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self {
all: ActivityCountVecs::forced_import(db, name, version, indexes, cached_starts)?,
all: ActivityCountVecs::forced_import(db, "", version, indexes, cached_starts)?,
by_addr_type: AddrTypeToActivityCountVecs::forced_import(
db,
name,
version,
indexes,
cached_starts,
@@ -1,183 +0,0 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, Database, EagerVec, Exit, PcoVec, ReadableVec, Rw, StorageMode,
WritableVec,
};
use crate::{indexes, internal::PerBlock};
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrCountVecs<M: StorageMode = Rw>(#[traversable(flatten)] pub PerBlock<StoredU64, M>);
impl AddrCountVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(PerBlock::forced_import(db, name, version, indexes)?))
}
}
/// Address count per address type (runtime state).
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToAddrCount(ByAddrType<u64>);
impl AddrTypeToAddrCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
}
}
impl From<(&AddrTypeToAddrCountVecs, Height)> for AddrTypeToAddrCount {
#[inline]
fn from((groups, starting_height): (&AddrTypeToAddrCountVecs, Height)) -> Self {
if let Some(prev_height) = starting_height.decremented() {
Self(ByAddrType {
p2pk65: groups
.p2pk65
.height
.collect_one(prev_height)
.unwrap()
.into(),
p2pk33: groups
.p2pk33
.height
.collect_one(prev_height)
.unwrap()
.into(),
p2pkh: groups.p2pkh.height.collect_one(prev_height).unwrap().into(),
p2sh: groups.p2sh.height.collect_one(prev_height).unwrap().into(),
p2wpkh: groups
.p2wpkh
.height
.collect_one(prev_height)
.unwrap()
.into(),
p2wsh: groups.p2wsh.height.collect_one(prev_height).unwrap().into(),
p2tr: groups.p2tr.height.collect_one(prev_height).unwrap().into(),
p2a: groups.p2a.height.collect_one(prev_height).unwrap().into(),
})
} else {
Default::default()
}
}
}
/// Address count per address type, with height + derived indexes.
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrTypeToAddrCountVecs<M: StorageMode = Rw>(ByAddrType<AddrCountVecs<M>>);
impl From<ByAddrType<AddrCountVecs>> for AddrTypeToAddrCountVecs {
#[inline]
fn from(value: ByAddrType<AddrCountVecs>) -> Self {
Self(value)
}
}
impl AddrTypeToAddrCountVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self::from(ByAddrType::<AddrCountVecs>::new_with_name(
|type_name| {
AddrCountVecs::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
},
)?))
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.0.values().map(|v| v.height.len()).min().unwrap()
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.0
.par_values_mut()
.map(|v| &mut v.height as &mut dyn AnyStoredVec)
}
#[inline(always)]
pub(crate) fn push_height(&mut self, addr_counts: &AddrTypeToAddrCount) {
for (vecs, &count) in self.0.values_mut().zip(addr_counts.values()) {
vecs.height.push(count.into());
}
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
for v in self.0.values_mut() {
v.height.reset()?;
}
Ok(())
}
pub(crate) fn by_height(&self) -> Vec<&EagerVec<PcoVec<Height, StoredU64>>> {
self.0.values().map(|v| &v.height).collect()
}
}
#[derive(Traversable)]
pub struct AddrCountsVecs<M: StorageMode = Rw> {
pub all: AddrCountVecs<M>,
#[traversable(flatten)]
pub by_addr_type: AddrTypeToAddrCountVecs<M>,
}
impl AddrCountsVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
all: AddrCountVecs::forced_import(db, name, version, indexes)?,
by_addr_type: AddrTypeToAddrCountVecs::forced_import(db, name, version, indexes)?,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.all
.height
.len()
.min(self.by_addr_type.min_stateful_len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
rayon::iter::once(&mut self.all.height as &mut dyn AnyStoredVec)
.chain(self.by_addr_type.par_iter_height_mut())
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.all.height.reset()?;
self.by_addr_type.reset_height()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(&mut self, total: u64, addr_counts: &AddrTypeToAddrCount) {
self.all.height.push(total.into());
self.by_addr_type.push_height(addr_counts);
}
pub(crate) fn compute_rest(&mut self, starting_indexes: &Indexes, exit: &Exit) -> Result<()> {
let sources = self.by_addr_type.by_height();
self.all
.height
.compute_sum_of_others(starting_indexes.height, &sources, exit)?;
Ok(())
}
}
@@ -0,0 +1,38 @@
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},
};
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 AddrCountsVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
);
impl AddrCountsVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
db, name, version, indexes,
)?))
}
#[inline(always)]
pub(crate) fn push_counts(&mut self, counts: &AddrTypeToAddrCount) {
self.push_height(counts.sum(), counts.values().copied());
}
}
@@ -0,0 +1,78 @@
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;
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 AddrCountFundedTotalVecs<M: StorageMode = Rw> {
pub funded: AddrCountsVecs<M>,
pub total: AddrCountsVecs<M>,
}
impl AddrCountFundedTotalVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
funded: AddrCountsVecs::forced_import(
db,
&format!("{name}_addr_count"),
version,
indexes,
)?,
total: AddrCountsVecs::forced_import(
db,
&format!("total_{name}_addr_count"),
version,
indexes,
)?,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.funded
.min_stateful_len()
.min(self.total.min_stateful_len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.funded
.par_iter_height_mut()
.chain(self.total.par_iter_height_mut())
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.funded.reset_height()?;
self.total.reset_height()?;
Ok(())
}
#[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)?;
Ok(())
}
}
@@ -0,0 +1,7 @@
mod all_vecs;
mod funded_total_vecs;
mod state;
pub use all_vecs::AddrCountsVecs;
pub use funded_total_vecs::AddrCountFundedTotalVecs;
pub use state::AddrTypeToAddrCount;
@@ -0,0 +1,38 @@
use brk_cohort::ByAddrType;
use brk_types::Height;
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use super::AddrCountsVecs;
/// Per-addr-type address-count running total. Shared runtime state across
/// funded / empty / exposed / reused / respent counters; paired with
/// [`AddrCountsVecs`] on disk.
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToAddrCount(ByAddrType<u64>);
impl AddrTypeToAddrCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
}
}
impl From<ByAddrType<u64>> for AddrTypeToAddrCount {
#[inline]
fn from(value: ByAddrType<u64>) -> Self {
Self(value)
}
}
impl From<(&AddrCountsVecs, Height)> for AddrTypeToAddrCount {
#[inline]
fn from((vecs, starting_height): (&AddrCountsVecs, Height)) -> Self {
let Some(prev_height) = starting_height.decremented() else {
return Self::default();
};
vecs.by_addr_type
.map_with_name(|_, v| v.height.collect_one(prev_height).unwrap().into())
.into()
}
}
@@ -1,28 +1,24 @@
use brk_cohort::ByAddrType;
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, StoredI64, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use crate::{
indexes,
internal::{CachedWindowStarts, LazyRollingDeltasFromHeight},
internal::{LazyRollingDeltasFromHeight, WindowStartVec, Windows, WithAddrTypes},
};
use super::AddrCountsVecs;
type AddrDelta = LazyRollingDeltasFromHeight<StoredU64, StoredI64, BasisPointsSigned32>;
#[derive(Clone, Traversable)]
pub struct DeltaVecs {
pub all: AddrDelta,
#[traversable(flatten)]
pub by_addr_type: ByAddrType<AddrDelta>,
}
#[derive(Clone, Deref, DerefMut, Traversable)]
pub struct DeltaVecs(#[traversable(flatten)] pub WithAddrTypes<AddrDelta>);
impl DeltaVecs {
pub(crate) fn new(
version: Version,
addr_count: &AddrCountsVecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
indexes: &indexes::Vecs,
) -> Self {
let version = version + Version::TWO;
@@ -30,7 +26,7 @@ impl DeltaVecs {
let all = LazyRollingDeltasFromHeight::new(
"addr_count",
version,
&addr_count.all.0.height,
&addr_count.all.height,
cached_starts,
indexes,
);
@@ -39,12 +35,12 @@ impl DeltaVecs {
LazyRollingDeltasFromHeight::new(
&format!("{name}_addr_count"),
version,
&addr.0.height,
&addr.height,
cached_starts,
indexes,
)
});
Self { all, by_addr_type }
Self(WithAddrTypes { all, by_addr_type })
}
}
@@ -0,0 +1,123 @@
//! Exposed address tracking (quantum / pubkey-exposure sense).
//!
//! An address is "exposed" once its public key is in the blockchain. Once
//! exposed, any funds at that address are at cryptographic risk (e.g. from
//! a quantum attacker capable of recovering the private key from the pubkey).
//!
//! When the pubkey gets exposed depends on the address type:
//!
//! - **P2PK33, P2PK65, P2TR**: the pubkey (or P2TR's tweaked output key) is
//! directly in the locking script of the funding output. These addresses are
//! exposed the moment they receive any funds.
//! - **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,
//! 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.
//! - **P2A**: anyone-can-spend, no pubkey at all. Excluded from both counters.
//!
//! Formally, with `is_funding_exposed` = `output_type.pubkey_exposed_at_funding()`:
//! - `funded` (count): `(utxo_count > 0) AND (is_funding_exposed OR spent_txo_count >= 1)`
//! - `total` (count): `(is_funding_exposed AND ever received) OR spent_txo_count >= 1`
//! - `supply` (sats): sum of balances of addresses currently in the funded set
//!
//! For P2PK/P2TR types this means `total ≡ total_addr_count` and
//! `funded ≡ funded_addr_count` (every address of those types is exposed by
//! virtue of existing). For P2PKH/P2SH/P2WPKH/P2WSH it's the strict subset of
//! addresses that have been spent from. The aggregate `all` exposed counter
//! 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
//! `process_received` and `process_sent`.
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use super::{
count::AddrCountFundedTotalVecs,
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
};
use crate::{indexes, prices};
mod state;
pub use state::ExposedAddrState;
/// Top-level container for all exposed address tracking: counts (funded +
/// total), the funded supply, and share of supply.
#[derive(Traversable)]
pub struct ExposedAddrVecs<M: StorageMode = Rw> {
pub count: AddrCountFundedTotalVecs<M>,
pub supply: AddrSupplyVecs<M>,
#[traversable(wrap = "supply", rename = "share")]
pub supply_share: AddrSupplyShareVecs<M>,
}
impl ExposedAddrVecs {
pub(crate) fn forced_import(
db: &Database,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self {
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)?,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.count
.min_stateful_len()
.min(self.supply.min_stateful_len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.count
.par_iter_height_mut()
.chain(self.supply.par_iter_height_mut())
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.count.reset_height()?;
self.supply.reset_height()?;
self.supply_share.reset_height()?;
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,
prices: &prices::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
self.count.compute_rest(starting_indexes, exit)?;
self.supply
.compute_rest(starting_indexes.height, prices, exit)?;
self.supply_share.compute_rest(
starting_indexes.height,
&self.supply,
all_supply_sats,
type_supply_sats,
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,83 @@
use brk_types::{FundedAddrData, Height, OutputType};
use crate::distribution::{
addr::{AddrReceivePreState, AddrSendPreState, AddrTypeToAddrCount, AddrTypeToSupply},
block::TrackingStatus,
};
use super::ExposedAddrVecs;
/// Runtime running totals for exposed-addr tracking. Mirrors the persistent
/// fields of [`ExposedAddrVecs`]: funded count, total count, funded supply.
/// Recovered from disk at the start of a block-loop run.
#[derive(Debug, Default)]
pub struct ExposedAddrState {
pub funded: AddrTypeToAddrCount,
pub total: AddrTypeToAddrCount,
pub supply: AddrTypeToSupply,
}
impl ExposedAddrState {
/// Apply exposed-addr updates for a received output, AFTER the receive
/// has mutated `addr_data`. `pre` is the snapshot taken before the mutation.
#[inline]
pub(crate) fn on_receive(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrReceivePreState,
status: TrackingStatus,
) {
// Pubkey-exposure state is unchanged by a receive, so `pre.was_pubkey_exposed`
// equals the post-receive value.
if !pre.was_funded && pre.was_pubkey_exposed {
*self.funded.get_mut_unwrap(output_type) += 1;
}
// Total for pk-exposed-at-funding types fires here on first receive.
// Other types fire on first spend in [`Self::on_send`].
if output_type.pubkey_exposed_at_funding() && matches!(status, TrackingStatus::New) {
*self.total.get_mut_unwrap(output_type) += 1;
}
let after = addr_data.exposed_supply_contribution(output_type);
self.supply
.apply_delta(output_type, pre.exposed_contribution, after);
}
/// Apply exposed-addr updates for a spent UTXO, AFTER the send has mutated
/// `addr_data`. `pre` is the snapshot taken before the mutation.
#[inline]
pub(crate) fn on_send(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrSendPreState,
will_be_empty: bool,
) {
let after = addr_data.exposed_supply_contribution(output_type);
self.supply
.apply_delta(output_type, pre.exposed_contribution, after);
// First-ever pubkey exposure. Non-pk-exposed types fire on first spend.
// Pk-exposed types never fire here (was already exposed at first receive).
if !pre.was_pubkey_exposed {
*self.total.get_mut_unwrap(output_type) += 1;
if !will_be_empty {
*self.funded.get_mut_unwrap(output_type) += 1;
}
}
// Leaving the funded exposed set: was in it iff pubkey was exposed pre-spend.
if will_be_empty && pre.was_pubkey_exposed {
*self.funded.get_mut_unwrap(output_type) -= 1;
}
}
}
impl From<(&ExposedAddrVecs, Height)> for ExposedAddrState {
#[inline]
fn from((vecs, starting_height): (&ExposedAddrVecs, Height)) -> Self {
Self {
funded: AddrTypeToAddrCount::from((&vecs.count.funded, starting_height)),
total: AddrTypeToAddrCount::from((&vecs.count.total, starting_height)),
supply: AddrTypeToSupply::from((&vecs.supply, starting_height)),
}
}
}
@@ -1,17 +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::{ExposedAddrState, ExposedAddrVecs};
pub use indexes::AnyAddrIndexesVecs;
pub use new_addr_count::NewAddrCountVecs;
pub use reused::{ReusedAddrState, ReusedAddrVecs};
pub use state::{AddrMetricsState, AddrReceivePreState, AddrSendPreState};
pub use supply::AddrTypeToSupply;
pub use total_addr_count::TotalAddrCountVecs;
pub use type_map::{AddrTypeToTypeIndexMap, AddrTypeToVec, HeightToAddrTypeToVec};
@@ -1,50 +1,35 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, Rw, StorageMode};
use crate::{
indexes,
internal::{CachedWindowStarts, PerBlockCumulativeRolling},
internal::{PerBlockCumulativeRolling, WindowStartVec, Windows, WithAddrTypes},
};
use super::TotalAddrCountVecs;
/// New address count per block (global + per-type)
#[derive(Traversable)]
pub struct NewAddrCountVecs<M: StorageMode = Rw> {
pub all: PerBlockCumulativeRolling<StoredU64, StoredU64, M>,
/// New address count per block (global + per-type).
#[derive(Deref, DerefMut, Traversable)]
pub struct NewAddrCountVecs<M: StorageMode = Rw>(
#[traversable(flatten)]
pub by_addr_type: ByAddrType<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
}
pub WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
);
impl NewAddrCountVecs {
pub(crate) fn forced_import(
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let all = PerBlockCumulativeRolling::forced_import(
db,
"new_addr_count",
version,
indexes,
cached_starts,
)?;
let by_addr_type = ByAddrType::new_with_name(|name| {
PerBlockCumulativeRolling::forced_import(
db,
&format!("{name}_new_addr_count"),
version,
indexes,
cached_starts,
)
})?;
Ok(Self { all, by_addr_type })
Ok(Self(WithAddrTypes::<
PerBlockCumulativeRolling<StoredU64, StoredU64>,
>::forced_import(
db, "new_addr_count", version, indexes, cached_starts
)?))
}
pub(crate) fn compute(
@@ -53,11 +38,12 @@ impl NewAddrCountVecs {
total_addr_count: &TotalAddrCountVecs,
exit: &Exit,
) -> Result<()> {
self.all.compute(max_from, exit, |height_vec| {
self.0.all.compute(max_from, exit, |height_vec| {
Ok(height_vec.compute_change(max_from, &total_addr_count.all.height, 1, exit)?)
})?;
for ((_, new), (_, total)) in self
.0
.by_addr_type
.iter_mut()
.zip(total_addr_count.by_addr_type.iter())
@@ -0,0 +1,11 @@
//! 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.
//! 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::AddrTypeToAddrEventCount;
pub use vecs::AddrEventsVecs;
@@ -0,0 +1,27 @@
use brk_cohort::ByAddrType;
use derive_more::{Deref, DerefMut};
/// 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 AddrTypeToAddrEventCount(ByAddrType<u64>);
impl AddrTypeToAddrEventCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
}
#[inline]
pub(crate) fn reset(&mut self) {
for v in self.0.values_mut() {
*v = 0;
}
}
}
@@ -0,0 +1,266 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPoints16, Indexes, OutputType, StoredF32, StoredU32, StoredU64, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
use crate::{
indexes, inputs,
internal::{
PerBlockCumulativeRolling, PerBlockRollingAverage, PercentCumulativeRolling,
WindowStartVec, Windows, WithAddrTypes,
},
outputs,
};
use super::state::AddrTypeToAddrEventCount;
/// Per-block reused-address event metrics. Holds three families of
/// signals: output-level (use), input-level (spend), and address-level
/// (active in block).
///
/// `output_to_reused_addr_count`: every output landing on an address that had
/// already received at least one prior output anywhere in its lifetime,
/// i.e. an output-level reuse event. Outputs are not deduplicated per
/// address within a block: an address receiving N outputs in one block
/// that had `before` lifetime outputs contributes
/// `max(0, N - max(0, 1 - before))` events. Only the very first output
/// an address ever sees is excluded. Every subsequent output counts,
/// matching the standard "% of outputs to previously-used addresses"
/// reuse ratio reported by external sources. `output_to_reused_addr_share`
/// uses `outputs::ByTypeVecs::output_count` (all 12 output types) as
/// denominator. `spendable_output_to_reused_addr_share` uses the
/// op_return-excluded 11-type aggregate (`spendable_output_count`).
///
/// `input_from_reused_addr_count`: every input spending from an address
/// whose lifetime `funded_txo_count > 1` at the time of the spend (i.e.
/// the address is in the same reused set tracked by
/// `reused_addr_count`). Every input is checked independently. If a
/// single address has multiple inputs in one block each one counts.
/// This is a *stable-predicate* signal about the sending address, not
/// an output-level repeat event: the first spend from a reused address
/// counts just as much as the tenth. Denominator
/// (`input_from_reused_addr_share`): `inputs::ByTypeVecs::input_count` (11
/// spendable types, where `p2ms`, `unknown`, `empty` count as true
/// negatives).
///
/// `active_reused_addr_count` / `active_reused_addr_share`: block-level
/// *address* signals (single aggregate, not per-type).
/// `active_reused_addr_count` is the count of distinct addresses
/// involved in this block (sent received) that satisfy `is_reused()`
/// after the block's events, populated inline in `process_received`
/// (each receiver, post-receive) and in `process_sent` (each
/// first-encounter sender, deduped against `received_addrs` so
/// addresses that did both aren't double-counted).
/// `active_reused_addr_share` is the per-block ratio
/// `reused / active * 100` as a percentage in `[0, 100]` (or `0.0` for
/// empty blocks). The denominator (distinct active addrs per block)
/// lives on `ActivityCountVecs::active` (`addrs.activity.all.active`),
/// derived from `sending + receiving - bidirectional`. Both fields
/// use `PerBlockRollingAverage` so their lazy 24h/1w/1m/1y series are
/// rolling *averages* of the per-block values. Sums and cumulatives of
/// distinct-address counts would be misleading because the same
/// address can appear in multiple blocks.
#[derive(Traversable)]
pub struct AddrEventsVecs<M: StorageMode = Rw> {
pub output_to_reused_addr_count:
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
pub output_to_reused_addr_share: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
pub spendable_output_to_reused_addr_share: PercentCumulativeRolling<BasisPoints16, M>,
pub input_from_reused_addr_count:
WithAddrTypes<PerBlockCumulativeRolling<StoredU64, StoredU64, M>>,
pub input_from_reused_addr_share: WithAddrTypes<PercentCumulativeRolling<BasisPoints16, M>>,
pub active_reused_addr_count: PerBlockRollingAverage<StoredU32, StoredU64, M>,
pub active_reused_addr_share: PerBlockRollingAverage<StoredF32, StoredF32, M>,
}
impl AddrEventsVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let import_count = |name: &str| {
WithAddrTypes::<PerBlockCumulativeRolling<StoredU64, StoredU64>>::forced_import(
db,
name,
version,
indexes,
cached_starts,
)
};
let import_percent = |name: &str| -> Result<WithAddrTypes<
PercentCumulativeRolling<BasisPoints16>,
>> {
Ok(WithAddrTypes {
all: PercentCumulativeRolling::forced_import(db, name, version, indexes)?,
by_addr_type: ByAddrType::new_with_name(|type_name| {
PercentCumulativeRolling::forced_import(
db,
&format!("{type_name}_{name}"),
version,
indexes,
)
})?,
})
};
let output_to_reused_addr_count =
import_count(&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,
&format!("spendable_output_to_{name}_addr_share"),
version,
indexes,
)?;
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,
&format!("active_{name}_addr_count"),
version,
indexes,
cached_starts,
)?;
let active_reused_addr_share = PerBlockRollingAverage::forced_import(
db,
&format!("active_{name}_addr_share"),
version,
indexes,
cached_starts,
)?;
Ok(Self {
output_to_reused_addr_count,
output_to_reused_addr_share,
spendable_output_to_reused_addr_share,
input_from_reused_addr_count,
input_from_reused_addr_share,
active_reused_addr_count,
active_reused_addr_share,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.output_to_reused_addr_count
.min_stateful_len()
.min(self.input_from_reused_addr_count.min_stateful_len())
.min(self.active_reused_addr_count.block.len())
.min(self.active_reused_addr_share.block.len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.output_to_reused_addr_count
.par_iter_height_mut()
.chain(self.input_from_reused_addr_count.par_iter_height_mut())
.chain([
&mut self.active_reused_addr_count.block as &mut dyn AnyStoredVec,
&mut self.active_reused_addr_share.block as &mut dyn AnyStoredVec,
])
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.output_to_reused_addr_count.reset_height()?;
self.input_from_reused_addr_count.reset_height()?;
self.active_reused_addr_count.block.reset()?;
self.active_reused_addr_share.block.reset()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(
&mut self,
uses: &AddrTypeToAddrEventCount,
spends: &AddrTypeToAddrEventCount,
active_addr_count: u32,
active_reused_addr_count: u32,
) {
self.output_to_reused_addr_count
.push_height(uses.sum(), uses.values().copied());
self.input_from_reused_addr_count
.push_height(spends.sum(), spends.values().copied());
self.active_reused_addr_count
.block
.push(StoredU32::from(active_reused_addr_count));
// Stored as a percentage in [0, 100] to match the rest of the
// codebase (Unit.percentage on the website expects 0..100). The
// `active_addr_count` denominator lives on `ActivityCountVecs`
// (`addrs.activity.all.active`), passed in here so we can
// compute the per-block ratio inline.
let share = if active_addr_count > 0 {
100.0 * (active_reused_addr_count as f32 / active_addr_count as f32)
} else {
0.0
};
self.active_reused_addr_share
.block
.push(StoredF32::from(share));
}
pub(crate) fn compute_rest(
&mut self,
starting_indexes: &Indexes,
outputs_by_type: &outputs::ByTypeVecs,
inputs_by_type: &inputs::ByTypeVecs,
exit: &Exit,
) -> Result<()> {
self.output_to_reused_addr_count
.compute_rest(starting_indexes.height, exit)?;
self.input_from_reused_addr_count
.compute_rest(starting_indexes.height, exit)?;
self.active_reused_addr_count
.compute_rest(starting_indexes.height, exit)?;
self.active_reused_addr_share
.compute_rest(starting_indexes.height, exit)?;
self.output_to_reused_addr_share.all.compute_count_ratio(
&self.output_to_reused_addr_count.all,
&outputs_by_type.output_count.all,
starting_indexes.height,
exit,
)?;
self.spendable_output_to_reused_addr_share.compute_count_ratio(
&self.output_to_reused_addr_count.all,
&outputs_by_type.spendable_output_count,
starting_indexes.height,
exit,
)?;
self.input_from_reused_addr_share.all.compute_count_ratio(
&self.input_from_reused_addr_count.all,
&inputs_by_type.input_count.all,
starting_indexes.height,
exit,
)?;
for otype in OutputType::ADDR_TYPES {
self.output_to_reused_addr_share
.by_addr_type
.get_mut_unwrap(otype)
.compute_count_ratio(
self.output_to_reused_addr_count.by_addr_type.get_unwrap(otype),
outputs_by_type.output_count.by_type.get(otype),
starting_indexes.height,
exit,
)?;
self.input_from_reused_addr_share
.by_addr_type
.get_mut_unwrap(otype)
.compute_count_ratio(
self.input_from_reused_addr_count.by_addr_type.get_unwrap(otype),
inputs_by_type.input_count.by_type.get(otype),
starting_indexes.height,
exit,
)?;
}
Ok(())
}
}
@@ -0,0 +1,133 @@
//! Reused address tracking.
//!
//! An address is "reused" if its lifetime `funded_txo_count > 1`, i.e.
//! it has received more than one output across its lifetime. This is
//! the simplest output-multiplicity proxy for address linkability.
//!
//! Two facets are tracked here:
//! - [`count`]: how many distinct addresses are currently reused
//! (funded) and how many have *ever* been reused (total). Per address
//! type plus an aggregated `all`.
//! - [`events`]: per-block address-reuse event counts on both sides.
//! Output-side (`output_to_reused_addr_count`, outputs landing on
//! addresses that had already received ≥ 1 prior output) and
//! input-side (`input_from_reused_addr_count`, inputs spending from
//! addresses with lifetime `funded_txo_count > 1`). Each count is
//! paired with a percent over the matching block-level output/input
//! total.
mod events;
pub use events::{AddrEventsVecs, AddrTypeToAddrEventCount};
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, Sats, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use super::{
count::AddrCountFundedTotalVecs,
supply::{AddrSupplyShareVecs, AddrSupplyVecs},
};
use crate::{
indexes, inputs,
internal::{WindowStartVec, Windows},
outputs, prices,
};
mod state;
pub use state::ReusedAddrState;
/// Top-level container for all reused address tracking: counts (funded +
/// total), per-block reuse events (output-side + input-side), and funded
/// supply + share.
#[derive(Traversable)]
pub struct ReusedAddrVecs<M: StorageMode = Rw> {
pub count: AddrCountFundedTotalVecs<M>,
pub events: AddrEventsVecs<M>,
pub supply: AddrSupplyVecs<M>,
#[traversable(wrap = "supply", rename = "share")]
pub supply_share: AddrSupplyShareVecs<M>,
}
impl ReusedAddrVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self {
count: 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)?,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.count
.min_stateful_len()
.min(self.events.min_stateful_len())
.min(self.supply.min_stateful_len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.count
.par_iter_height_mut()
.chain(self.events.par_iter_height_mut())
.chain(self.supply.par_iter_height_mut())
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.count.reset_height()?;
self.events.reset_height()?;
self.supply.reset_height()?;
self.supply_share.reset_height()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(&mut self, state: &ReusedAddrState, active_addr_count: u32) {
self.count.push_counts(&state.funded, &state.total);
self.supply.push_supply(&state.supply);
self.events.push_height(
&state.output_events,
&state.input_events,
active_addr_count,
u32::try_from(state.active.sum()).unwrap(),
);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_rest(
&mut self,
starting_indexes: &Indexes,
outputs_by_type: &outputs::ByTypeVecs,
inputs_by_type: &inputs::ByTypeVecs,
prices: &prices::Vecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
self.count.compute_rest(starting_indexes, exit)?;
self.events
.compute_rest(starting_indexes, outputs_by_type, inputs_by_type, exit)?;
self.supply
.compute_rest(starting_indexes.height, prices, exit)?;
self.supply_share.compute_rest(
starting_indexes.height,
&self.supply,
all_supply_sats,
type_supply_sats,
exit,
)?;
Ok(())
}
}
@@ -0,0 +1,201 @@
use brk_types::{FundedAddrData, Height, OutputType};
use crate::distribution::addr::{
AddrReceivePreState, AddrSendPreState, AddrTypeToAddrCount, AddrTypeToSupply,
};
use super::{AddrTypeToAddrEventCount, ReusedAddrVecs};
/// Runtime state for receive-based (reused) or spend-based (respent)
/// address tracking. Mirrors the persistent fields of [`ReusedAddrVecs`]
/// (funded + total counts, funded supply) plus per-block event counters
/// that reset every block.
///
/// `output_events`, `input_events`, and `active` are cleared via
/// [`Self::reset_per_block`] at the start of each block. The three running
/// totals (`funded`, `total`, `supply`) are recovered from disk at the start
/// of a run via [`From<(&ReusedAddrVecs, Height)>`].
#[derive(Debug, Default)]
pub struct ReusedAddrState {
pub funded: AddrTypeToAddrCount,
pub total: AddrTypeToAddrCount,
pub supply: AddrTypeToSupply,
pub output_events: AddrTypeToAddrEventCount,
pub input_events: AddrTypeToAddrEventCount,
pub active: AddrTypeToAddrEventCount,
}
impl ReusedAddrState {
#[inline]
pub(crate) fn reset_per_block(&mut self) {
self.output_events.reset();
self.input_events.reset();
self.active.reset();
}
/// Apply reused-flavor (receive-based: `funded_txo_count > 1`) updates
/// for a received output, AFTER the receive has mutated `addr_data`.
#[inline]
pub(crate) fn on_receive_as_reused(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrReceivePreState,
output_count: u32,
) {
let is_now_reused = addr_data.is_reused();
// Threshold crossing: the 2nd lifetime receive lands here. The address
// is always funded post-receive.
if is_now_reused && !pre.was_reused {
*self.total.get_mut_unwrap(output_type) += 1;
*self.funded.get_mut_unwrap(output_type) += 1;
} else if pre.was_reused && !pre.was_funded {
// Reactivation: already-reused address was empty, now funded.
*self.funded.get_mut_unwrap(output_type) += 1;
}
// output-to-reused events: outputs landing on addresses that had
// already received >= 1 prior output, i.e. every output except the
// very first one the address ever sees. With `before =
// prev_funded_txo_count` and `N = output_count`: events = N - max(0, 1 - before).
let skip_first = 1u32.saturating_sub(pre.prev_funded_txo_count.min(1));
let reused_events = output_count.saturating_sub(skip_first);
if reused_events > 0 {
*self.output_events.get_mut_unwrap(output_type) += u64::from(reused_events);
}
if is_now_reused {
*self.active.get_mut_unwrap(output_type) += 1;
}
let after = addr_data.reused_supply_contribution();
self.supply
.apply_delta(output_type, pre.reused_contribution, after);
}
/// Apply respent-flavor (spend-based: `spent_txo_count > 1`) updates for a
/// received output, AFTER the receive has mutated `addr_data`. Receives
/// don't cross the respent threshold. The only transition is an
/// already-respent empty address reactivating into the funded set.
#[inline]
pub(crate) fn on_receive_as_respent(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrReceivePreState,
output_count: u32,
) {
if pre.was_respent && !pre.was_funded {
*self.funded.get_mut_unwrap(output_type) += 1;
}
// Respent status is stable across a receive, so every output lands on
// a respent address iff the address was already respent.
if pre.was_respent {
*self.output_events.get_mut_unwrap(output_type) += u64::from(output_count);
*self.active.get_mut_unwrap(output_type) += 1;
}
let after = addr_data.respent_supply_contribution();
self.supply
.apply_delta(output_type, pre.respent_contribution, after);
}
/// Apply reused-flavor updates for a spent UTXO, AFTER the send has
/// mutated `addr_data`. Sends don't change the reused predicate, so
/// `pre.was_reused == is_reused` post-spend.
#[inline]
pub(crate) fn on_send_as_reused(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrSendPreState,
is_first_encounter: bool,
also_received: bool,
will_be_empty: bool,
) {
if pre.was_reused {
*self.input_events.get_mut_unwrap(output_type) += 1;
}
// Active reused: first-encounter sender, currently reused, and not
// already counted on the receive side.
if is_first_encounter && pre.was_reused && !also_received {
*self.active.get_mut_unwrap(output_type) += 1;
}
if will_be_empty && pre.was_reused {
*self.funded.get_mut_unwrap(output_type) -= 1;
}
let after = addr_data.reused_supply_contribution();
self.supply
.apply_delta(output_type, pre.reused_contribution, after);
}
/// Apply respent-flavor updates for a spent UTXO, AFTER the send has
/// mutated `addr_data`. Sends CAN cross the respent threshold on the
/// 2nd lifetime spend.
#[inline]
pub(crate) fn on_send_as_respent(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrSendPreState,
is_first_encounter: bool,
also_received: bool,
will_be_empty: bool,
) {
if pre.was_respent {
*self.input_events.get_mut_unwrap(output_type) += 1;
}
let is_now_respent = addr_data.is_respent();
// Threshold crossing: the 2nd spend ever on this address. Always
// bumps the monotonic total. Bumps the funded count iff the address
// still has a balance. If the crossing spend also empties the
// address, the `will_be_empty` branch below doesn't decrement
// (was_respent is false), so the funded count stays correct.
if is_now_respent && !pre.was_respent {
*self.total.get_mut_unwrap(output_type) += 1;
if !will_be_empty {
*self.funded.get_mut_unwrap(output_type) += 1;
}
}
// Active respent splits cleanly into two disjoint branches (gated on
// `pre.was_respent`):
// - was already respent + active this block, and not also counted
// on the receive side: pure senders on first spend.
// - crosses the respent threshold this block: fires once per
// address ever, on the exact crossing spend.
if (is_first_encounter && pre.was_respent && !also_received)
|| (is_now_respent && !pre.was_respent)
{
*self.active.get_mut_unwrap(output_type) += 1;
}
// Leaving the funded respent set on empty uses pre-spend state: a
// threshold-crossing spend that also empties the address never
// entered the funded set above (gated on !will_be_empty), so we
// don't double-decrement.
if will_be_empty && pre.was_respent {
*self.funded.get_mut_unwrap(output_type) -= 1;
}
let after = addr_data.respent_supply_contribution();
self.supply
.apply_delta(output_type, pre.respent_contribution, after);
}
}
impl From<(&ReusedAddrVecs, Height)> for ReusedAddrState {
#[inline]
fn from((vecs, starting_height): (&ReusedAddrVecs, Height)) -> Self {
Self {
funded: AddrTypeToAddrCount::from((&vecs.count.funded, starting_height)),
total: AddrTypeToAddrCount::from((&vecs.count.total, starting_height)),
supply: AddrTypeToSupply::from((&vecs.supply, starting_height)),
output_events: AddrTypeToAddrEventCount::default(),
input_events: AddrTypeToAddrEventCount::default(),
active: AddrTypeToAddrEventCount::default(),
}
}
}
@@ -0,0 +1,181 @@
use brk_types::{FundedAddrData, Height, OutputType, Sats};
use crate::distribution::{block::TrackingStatus, vecs::AddrMetricsVecs};
use super::{
AddrTypeToActivityCounts, AddrTypeToAddrCount, ExposedAddrState, ReusedAddrState,
};
/// Bundle of per-block runtime state for the full address-metrics pipeline.
/// Feeds `process_received` / `process_sent` and is pushed to [`AddrMetricsVecs`]
/// once per block.
///
/// Recovery: [`From<(&AddrMetricsVecs, Height)>`] reads the prior block from
/// disk to seed all persistent running totals. Per-block counters (activity,
/// and event counts inside each [`ReusedAddrState`]) default to zero and are
/// cleared at the top of each block via [`Self::reset_per_block`].
#[derive(Debug, Default)]
pub struct AddrMetricsState {
pub funded: AddrTypeToAddrCount,
pub empty: AddrTypeToAddrCount,
pub activity: AddrTypeToActivityCounts,
pub reused: ReusedAddrState,
pub respent: ReusedAddrState,
pub exposed: ExposedAddrState,
}
/// Snapshot of [`FundedAddrData`] taken BEFORE a receive mutates it.
/// Feeds delta-based updates in [`AddrMetricsState::on_receive_applied`].
#[derive(Debug)]
pub struct AddrReceivePreState {
pub was_funded: bool,
pub was_reused: bool,
pub was_respent: bool,
pub was_pubkey_exposed: bool,
pub prev_funded_txo_count: u32,
pub exposed_contribution: Sats,
pub reused_contribution: Sats,
pub respent_contribution: Sats,
}
impl AddrReceivePreState {
#[inline]
pub fn capture(addr_data: &FundedAddrData, output_type: OutputType) -> Self {
Self {
was_funded: addr_data.is_funded(),
was_reused: addr_data.is_reused(),
was_respent: addr_data.is_respent(),
was_pubkey_exposed: addr_data.is_pubkey_exposed(output_type),
prev_funded_txo_count: addr_data.funded_txo_count,
exposed_contribution: addr_data.exposed_supply_contribution(output_type),
reused_contribution: addr_data.reused_supply_contribution(),
respent_contribution: addr_data.respent_supply_contribution(),
}
}
}
/// Snapshot of [`FundedAddrData`] taken BEFORE a spend mutates it.
/// Feeds delta-based updates in [`AddrMetricsState::on_send_applied`].
#[derive(Debug)]
pub struct AddrSendPreState {
pub was_reused: bool,
pub was_respent: bool,
pub was_pubkey_exposed: bool,
pub exposed_contribution: Sats,
pub reused_contribution: Sats,
pub respent_contribution: Sats,
}
impl AddrSendPreState {
#[inline]
pub fn capture(addr_data: &FundedAddrData, output_type: OutputType) -> Self {
Self {
was_reused: addr_data.is_reused(),
was_respent: addr_data.is_respent(),
was_pubkey_exposed: addr_data.is_pubkey_exposed(output_type),
exposed_contribution: addr_data.exposed_supply_contribution(output_type),
reused_contribution: addr_data.reused_supply_contribution(),
respent_contribution: addr_data.respent_supply_contribution(),
}
}
}
impl AddrMetricsState {
#[inline]
pub(crate) fn reset_per_block(&mut self) {
self.activity.reset();
self.reused.reset_per_block();
self.respent.reset_per_block();
}
/// Apply all state updates for a received output, AFTER the cohort and
/// `addr_data` have been mutated. `pre` is the snapshot captured before
/// the mutation, `addr_data` is the post-receive view.
#[inline]
pub(crate) fn on_receive_applied(
&mut self,
output_type: OutputType,
status: TrackingStatus,
addr_data: &FundedAddrData,
pre: &AddrReceivePreState,
output_count: u32,
) {
let activity = self.activity.get_mut_unwrap(output_type);
activity.receiving += 1;
match status {
TrackingStatus::New => {
*self.funded.get_mut_unwrap(output_type) += 1;
}
TrackingStatus::WasEmpty => {
activity.reactivated += 1;
*self.funded.get_mut_unwrap(output_type) += 1;
*self.empty.get_mut_unwrap(output_type) -= 1;
}
TrackingStatus::Tracked => {}
}
self.reused
.on_receive_as_reused(output_type, addr_data, pre, output_count);
self.respent
.on_receive_as_respent(output_type, addr_data, pre, output_count);
self.exposed.on_receive(output_type, addr_data, pre, status);
}
/// Apply all state updates for a spent UTXO, AFTER the cohort and
/// `addr_data` have been mutated. `pre` is the snapshot captured before
/// the mutation. `is_first_encounter` / `also_received` come from the
/// caller's per-block seen/received tracking. `will_be_empty` is from
/// the pre-mutation `addr_data.has_1_utxos()`.
#[inline]
pub(crate) fn on_send_applied(
&mut self,
output_type: OutputType,
addr_data: &FundedAddrData,
pre: &AddrSendPreState,
is_first_encounter: bool,
also_received: bool,
will_be_empty: bool,
) {
if is_first_encounter {
let activity = self.activity.get_mut_unwrap(output_type);
activity.sending += 1;
if also_received {
activity.bidirectional += 1;
}
}
if will_be_empty {
*self.funded.get_mut_unwrap(output_type) -= 1;
*self.empty.get_mut_unwrap(output_type) += 1;
}
self.reused.on_send_as_reused(
output_type,
addr_data,
pre,
is_first_encounter,
also_received,
will_be_empty,
);
self.respent.on_send_as_respent(
output_type,
addr_data,
pre,
is_first_encounter,
also_received,
will_be_empty,
);
self.exposed.on_send(output_type, addr_data, pre, will_be_empty);
}
}
impl From<(&AddrMetricsVecs, Height)> for AddrMetricsState {
#[inline]
fn from((vecs, starting_height): (&AddrMetricsVecs, Height)) -> Self {
Self {
funded: AddrTypeToAddrCount::from((&vecs.funded, starting_height)),
empty: AddrTypeToAddrCount::from((&vecs.empty, starting_height)),
activity: AddrTypeToActivityCounts::default(),
reused: ReusedAddrState::from((&vecs.reused, starting_height)),
respent: ReusedAddrState::from((&vecs.respent, starting_height)),
exposed: ExposedAddrState::from((&vecs.exposed, starting_height)),
}
}
}
@@ -0,0 +1,12 @@
//! Generic per-address-type supply tracking, shared across predicate-based
//! supply categories (exposed, reused, respent). A "category supply" is the
//! running sum of balances held by addresses currently in the funded subset
//! defined by some predicate.
mod share;
mod state;
mod vecs;
pub use share::AddrSupplyShareVecs;
pub use state::AddrTypeToSupply;
pub use vecs::AddrSupplyVecs;
@@ -0,0 +1,69 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPoints16, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{
indexes,
internal::{PercentPerBlock, RatioSatsBp16, WithAddrTypes},
};
use super::vecs::AddrSupplyVecs;
/// Share of a predicate-based supply category relative to total supply.
///
/// - `all`: category supply / circulating supply
/// - Per-type: type's category supply / type's total supply
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrSupplyShareVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PercentPerBlock<BasisPoints16, M>>,
);
impl AddrSupplyShareVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(
WithAddrTypes::<PercentPerBlock<BasisPoints16>>::forced_import(
db,
&format!("{name}_addr_supply_share"),
version,
indexes,
)?,
))
}
pub(crate) fn compute_rest(
&mut self,
max_from: Height,
supply: &AddrSupplyVecs,
all_supply_sats: &impl ReadableVec<Height, Sats>,
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
self.all.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&supply.all.sats.height,
all_supply_sats,
exit,
)?;
for ((_, share), ((_, cat), (_, denom))) in self
.by_addr_type
.iter_mut()
.zip(supply.by_addr_type.iter().zip(type_supply_sats.iter()))
{
share.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&cat.sats.height,
*denom,
exit,
)?;
}
Ok(())
}
}
@@ -0,0 +1,49 @@
use brk_cohort::ByAddrType;
use brk_types::{Height, OutputType, Sats};
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use super::vecs::AddrSupplyVecs;
/// Per-addr-type running-total of a supply category (sats). Shared across
/// predicate-based supply categories (exposed, reused, respent).
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToSupply(ByAddrType<Sats>);
impl AddrTypeToSupply {
#[inline]
pub(crate) fn sum(&self) -> Sats {
self.0.values().copied().sum()
}
/// Apply a signed `after - before` delta to the slot for `output_type`.
/// Sats is unsigned, so branch on sign.
#[inline]
pub(crate) fn apply_delta(&mut self, output_type: OutputType, before: Sats, after: Sats) {
let slot = self.get_mut_unwrap(output_type);
if after >= before {
*slot += after - before;
} else {
*slot -= before - after;
}
}
}
impl From<ByAddrType<Sats>> for AddrTypeToSupply {
#[inline]
fn from(value: ByAddrType<Sats>) -> Self {
Self(value)
}
}
impl From<(&AddrSupplyVecs, Height)> for AddrTypeToSupply {
#[inline]
fn from((vecs, starting_height): (&AddrSupplyVecs, Height)) -> Self {
let Some(prev_height) = starting_height.decremented() else {
return Self::default();
};
vecs.by_addr_type
.map_with_name(|_, v| v.sats.height.collect_one(prev_height).unwrap())
.into()
}
}
@@ -0,0 +1,42 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Rw, StorageMode};
use crate::{
indexes,
internal::{ValuePerBlock, WithAddrTypes},
};
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 AddrSupplyVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<ValuePerBlock<M>>,
);
impl AddrSupplyVecs {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(WithAddrTypes::<ValuePerBlock>::forced_import(
db,
&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());
}
}
@@ -1,20 +1,21 @@
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, Rw, StorageMode};
use crate::{indexes, internal::PerBlock};
use crate::{
indexes,
internal::{PerBlock, WithAddrTypes},
};
use super::AddrCountsVecs;
/// Total address count (global + per-type) with all derived indexes
#[derive(Traversable)]
pub struct TotalAddrCountVecs<M: StorageMode = Rw> {
pub all: PerBlock<StoredU64, M>,
#[traversable(flatten)]
pub by_addr_type: ByAddrType<PerBlock<StoredU64, M>>,
}
/// Total address count (global + per-type) with all derived indexes.
#[derive(Deref, DerefMut, Traversable)]
pub struct TotalAddrCountVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
);
impl TotalAddrCountVecs {
pub(crate) fn forced_import(
@@ -22,13 +23,12 @@ impl TotalAddrCountVecs {
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let all = PerBlock::forced_import(db, "total_addr_count", version, indexes)?;
let by_addr_type: ByAddrType<PerBlock<StoredU64>> = ByAddrType::new_with_name(|name| {
PerBlock::forced_import(db, &format!("{name}_total_addr_count"), version, indexes)
})?;
Ok(Self { all, by_addr_type })
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
db,
"total_addr_count",
version,
indexes,
)?))
}
/// Eagerly compute total = addr_count + empty_addr_count.
@@ -67,14 +67,14 @@ pub(crate) fn process_funded_addrs(
// Pure pushes - no holes remain
addrs_data.funded.reserve_pushed(pushes_iter.len());
let mut next_index = addrs_data.funded.len();
for (addr_type, type_index, data) in pushes_iter {
for (next_index, (addr_type, type_index, data)) in
(addrs_data.funded.len()..).zip(pushes_iter)
{
addrs_data.funded.push(data);
result.get_mut(addr_type).unwrap().insert(
type_index,
AnyAddrIndex::from(FundedAddrIndex::from(next_index)),
);
next_index += 1;
}
Ok(result)
@@ -138,14 +138,14 @@ pub(crate) fn process_empty_addrs(
// Pure pushes - no holes remain
addrs_data.empty.reserve_pushed(pushes_iter.len());
let mut next_index = addrs_data.empty.len();
for (addr_type, type_index, data) in pushes_iter {
for (next_index, (addr_type, type_index, data)) in
(addrs_data.empty.len()..).zip(pushes_iter)
{
addrs_data.empty.push(data);
result.get_mut(addr_type).unwrap().insert(
type_index,
AnyAddrIndex::from(EmptyAddrIndex::from(next_index)),
);
next_index += 1;
}
Ok(result)
@@ -1,9 +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, AddrTypeToVec},
addr::{AddrMetricsState, AddrReceivePreState, AddrTypeToVec},
cohorts::AddrCohorts,
};
@@ -16,15 +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<u64>,
empty_addr_count: &mut ByAddrType<u64>,
activity_counts: &mut AddrTypeToActivityCounts,
state: &mut AddrMetricsState,
) {
let max_type_len = received_data
.iter()
@@ -39,12 +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);
// 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;
@@ -53,32 +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;
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()
@@ -90,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)
@@ -98,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\
@@ -124,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)
@@ -134,6 +104,8 @@ pub(crate) fn process_received(
.receive_outputs(addr_data, recv.total_value, price, recv.output_count);
}
}
state.on_receive_applied(output_type, status, addr_data, &pre, recv.output_count);
}
}
}
@@ -5,26 +5,20 @@ use rustc_hash::FxHashSet;
use vecdb::VecIndex;
use crate::distribution::{
addr::{AddrTypeToActivityCounts, 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)>,
@@ -32,9 +26,7 @@ pub(crate) fn process_sent(
lookup: &mut AddrLookup<'_>,
current_price: Cents,
price_range_max: &PriceRangeMax,
addr_count: &mut ByAddrType<u64>,
empty_addr_count: &mut ByAddrType<u64>,
activity_counts: &mut AddrTypeToActivityCounts,
state: &mut AddrMetricsState,
received_addrs: &ByAddrType<FxHashSet<TypeIndex>>,
height_to_price: &[Cents],
height_to_timestamp: &[Timestamp],
@@ -48,37 +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_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);
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;
// Track "both" - addresses that sent AND received this block
if type_received.is_some_and(|s| s.contains(&type_index)) {
type_activity.both += 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();
// Compute buckets once
let prev_bucket = AmountBucket::from(prev_balance);
let new_bucket = AmountBucket::from(new_balance);
let crossing_boundary = prev_bucket != new_bucket;
@@ -90,17 +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)?;
state.on_send_applied(
output_type,
addr_data,
&pre,
is_first_encounter,
also_received,
will_be_empty,
);
// 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;
lookup.move_to_empty(output_type, type_index);
} else if crossing_boundary {
cohorts
@@ -3,12 +3,17 @@ use std::path::Path;
use brk_cohort::{AddrGroups, AmountRange, Filter, Filtered, OverAmount, UnderAmount};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, StoredU64, Version};
use brk_types::{Height, Indexes, Sats, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode};
use crate::{distribution::DynCohortVecs, indexes, internal::CachedWindowStarts, prices};
use crate::{
distribution::DynCohortVecs,
indexes,
internal::{WindowStartVec, Windows},
prices,
};
use super::{super::traits::CohortVecs, vecs::AddrCohortVecs};
@@ -25,7 +30,7 @@ impl AddrCohorts {
version: Version,
indexes: &indexes::Vecs,
states_path: &Path,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let v = version + VERSION;
@@ -104,12 +109,19 @@ impl AddrCohorts {
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()> {
self.0
.par_iter_mut()
.try_for_each(|v| v.compute_rest_part2(prices, starting_indexes, all_utxo_count, exit))
self.0.par_iter_mut().try_for_each(|v| {
v.compute_rest_part2(
prices,
starting_indexes,
all_supply_sats,
all_utxo_count,
exit,
)
})
}
/// Returns a parallel iterator over all vecs for parallel writing.
@@ -3,14 +3,14 @@ use std::path::Path;
use brk_cohort::{CohortContext, Filter, Filtered};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, Cents, Height, Indexes, StoredI64, StoredU64, Version};
use brk_types::{BasisPointsSigned32, Cents, Height, Indexes, Sats, StoredI64, StoredU64, Version};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{
distribution::state::{AddrCohortState, MinimalRealizedState},
indexes,
internal::{CachedWindowStarts, PerBlockWithDeltas},
internal::{PerBlockWithDeltas, WindowStartVec, Windows},
prices,
};
@@ -38,7 +38,7 @@ impl AddrCohortVecs {
version: Version,
indexes: &indexes::Vecs,
states_path: Option<&Path>,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let full_name = CohortContext::Addr.full_name(&filter, name);
@@ -230,10 +230,16 @@ impl CohortVecs for AddrCohortVecs {
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()> {
self.metrics
.compute_rest_part2(prices, starting_indexes, all_utxo_count, exit)
self.metrics.compute_rest_part2(
prices,
starting_indexes,
all_supply_sats,
all_utxo_count,
exit,
)
}
}
@@ -1,5 +1,5 @@
use brk_error::Result;
use brk_types::{Cents, Height, Indexes, StoredU64, Version};
use brk_types::{Cents, Height, Indexes, Sats, StoredU64, Version};
use vecdb::{Exit, ReadableVec};
use crate::prices;
@@ -62,6 +62,7 @@ pub trait CohortVecs: DynCohortVecs {
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()>;
@@ -25,7 +25,7 @@ use crate::{
state::UTXOCohortState,
},
indexes,
internal::{AmountPerBlockCumulativeRolling, CachedWindowStarts},
internal::{ValuePerBlockCumulativeRolling, WindowStartVec, Windows},
prices,
};
@@ -50,7 +50,7 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
#[traversable(rename = "type")]
pub type_: SpendableType<UTXOCohortVecs<TypeCohortMetrics<M>>>,
pub profitability: ProfitabilityMetrics<M>,
pub matured: AgeRange<AmountPerBlockCumulativeRolling<M>>,
pub matured: AgeRange<ValuePerBlockCumulativeRolling<M>>,
#[traversable(skip)]
pub(super) caches: UTXOCohortsTransientState,
}
@@ -75,7 +75,7 @@ impl UTXOCohorts<Rw> {
version: Version,
indexes: &indexes::Vecs,
states_path: &Path,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let v = version + VERSION;
@@ -264,8 +264,8 @@ impl UTXOCohorts<Rw> {
let prefix = CohortContext::Utxo.prefix();
let matured = AgeRange::try_new(&|_f: Filter,
name: &'static str|
-> Result<AmountPerBlockCumulativeRolling> {
AmountPerBlockCumulativeRolling::forced_import(
-> Result<ValuePerBlockCumulativeRolling> {
ValuePerBlockCumulativeRolling::forced_import(
db,
&format!("{prefix}_{name}_matured_supply"),
v,
@@ -542,17 +542,14 @@ impl UTXOCohorts<Rw> {
}
/// Second phase of post-processing: compute relative metrics.
pub(crate) fn compute_rest_part2<HM>(
pub(crate) fn compute_rest_part2(
&mut self,
blocks: &blocks::Vecs,
prices: &prices::Vecs,
starting_indexes: &Indexes,
height_to_market_cap: &HM,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()>
where
HM: ReadableVec<Height, Dollars> + Sync,
{
) -> Result<()> {
// Get under_1h value sources for adjusted computation (cloned to avoid borrow conflicts).
let under_1h_value_created = self
.age_range
@@ -663,7 +660,7 @@ impl UTXOCohorts<Rw> {
Box::new(|| {
over_amount.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, au, exit)
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
})
}),
Box::new(|| {
@@ -681,19 +678,19 @@ impl UTXOCohorts<Rw> {
Box::new(|| {
amount_range.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, au, exit)
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
})
}),
Box::new(|| {
under_amount.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, au, exit)
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
})
}),
Box::new(|| {
type_.par_iter_mut().try_for_each(|v| {
v.metrics
.compute_rest_part2(prices, starting_indexes, au, exit)
.compute_rest_part2(prices, starting_indexes, ss, au, exit)
})
}),
];
@@ -830,26 +827,26 @@ impl UTXOCohorts<Rw> {
let mut sth_acc = RealizedFullAccum::default();
let mut lth_acc = RealizedFullAccum::default();
let mut all_icap = (0u128, 0u128);
let mut sth_icap = (0u128, 0u128);
let mut lth_icap = (0u128, 0u128);
let mut all_ccap = (0u128, 0u128);
let mut sth_ccap = (0u128, 0u128);
let mut lth_ccap = (0u128, 0u128);
for ar in age_range.iter_mut() {
if let Some(state) = ar.state.as_mut() {
all_acc.add(&state.realized);
let u = state.compute_unrealized_state(height_price);
all_icap.0 += u.investor_cap_in_profit_raw;
all_icap.1 += u.investor_cap_in_loss_raw;
all_ccap.0 += u.capitalized_cap_in_profit_raw;
all_ccap.1 += u.capitalized_cap_in_loss_raw;
if sth_filter.includes(&ar.metrics.filter) {
sth_acc.add(&state.realized);
sth_icap.0 += u.investor_cap_in_profit_raw;
sth_icap.1 += u.investor_cap_in_loss_raw;
sth_ccap.0 += u.capitalized_cap_in_profit_raw;
sth_ccap.1 += u.capitalized_cap_in_loss_raw;
} else {
lth_acc.add(&state.realized);
lth_icap.0 += u.investor_cap_in_profit_raw;
lth_icap.1 += u.investor_cap_in_loss_raw;
lth_ccap.0 += u.capitalized_cap_in_profit_raw;
lth_ccap.1 += u.capitalized_cap_in_loss_raw;
}
}
}
@@ -860,28 +857,28 @@ impl UTXOCohorts<Rw> {
all.metrics
.unrealized
.investor_cap_in_profit_raw
.push(CentsSquaredSats::new(all_icap.0));
.capitalized_cap_in_profit_raw
.push(CentsSquaredSats::new(all_ccap.0));
all.metrics
.unrealized
.investor_cap_in_loss_raw
.push(CentsSquaredSats::new(all_icap.1));
.capitalized_cap_in_loss_raw
.push(CentsSquaredSats::new(all_ccap.1));
sth.metrics
.unrealized
.investor_cap_in_profit_raw
.push(CentsSquaredSats::new(sth_icap.0));
.capitalized_cap_in_profit_raw
.push(CentsSquaredSats::new(sth_ccap.0));
sth.metrics
.unrealized
.investor_cap_in_loss_raw
.push(CentsSquaredSats::new(sth_icap.1));
.capitalized_cap_in_loss_raw
.push(CentsSquaredSats::new(sth_ccap.1));
lth.metrics
.unrealized
.investor_cap_in_profit_raw
.push(CentsSquaredSats::new(lth_icap.0));
.capitalized_cap_in_profit_raw
.push(CentsSquaredSats::new(lth_ccap.0));
lth.metrics
.unrealized
.investor_cap_in_loss_raw
.push(CentsSquaredSats::new(lth_icap.1));
.capitalized_cap_in_loss_raw
.push(CentsSquaredSats::new(lth_ccap.1));
}
}
@@ -1,8 +1,9 @@
use std::{cmp::Reverse, collections::BinaryHeap, fs, path::Path};
use brk_cohort::{Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES};
use brk_cohort::{AGE_RANGE_NAMES, CohortContext, Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES};
use rayon::prelude::*;
use brk_error::Result;
use brk_types::{BasisPoints16, Cents, CentsCompact, CostBasisDistribution, Date, Dollars, Sats};
use brk_types::{BasisPoints16, Cents, CentsCompact, UrpdRaw, Date, Dollars, Sats};
use crate::distribution::metrics::{CostBasis, ProfitabilityMetrics};
@@ -57,6 +58,30 @@ impl UTXOCohorts {
fn write_disk_distributions(&mut self, date: Date, states_path: &Path) -> Result<()> {
let sth_filter = self.sth.metrics.filter.clone();
self.age_range
.iter()
.zip(AGE_RANGE_NAMES.iter())
.collect::<Vec<_>>()
.into_par_iter()
.try_for_each(|(sub, name)| -> Result<()> {
let Some(state) = sub.state.as_ref() else {
return Ok(());
};
let mut merged: Vec<(CentsCompact, Sats)> = Vec::new();
for (&price, &sats) in state.cost_basis_map().iter() {
let rounded = price.round_to_dollar(COST_BASIS_PRICE_DIGITS);
if let Some(last) = merged.last_mut()
&& last.0 == rounded
{
last.1 += sats;
} else {
merged.push((rounded, sats));
}
}
let full = CohortContext::Utxo.prefixed(name.id);
write_distribution(states_path, &full, date, merged)
})?;
let maps: Vec<_> = self
.age_range
.iter()
@@ -84,9 +109,13 @@ impl UTXOCohorts {
merge_k_way(&maps, &mut targets);
write_distribution(states_path, "all", date, targets.all.merged)?;
write_distribution(states_path, TERM_NAMES.short.id, date, targets.sth.merged)?;
write_distribution(states_path, TERM_NAMES.long.id, date, targets.lth.merged)?;
[
("all", targets.all.merged),
(TERM_NAMES.short.id, targets.sth.merged),
(TERM_NAMES.long.id, targets.lth.merged),
]
.into_par_iter()
.try_for_each(|(name, merged)| write_distribution(states_path, name, date, merged))?;
Ok(())
}
@@ -122,15 +151,15 @@ fn push_profitability(
fn write_distribution(
states_path: &Path,
name: &str,
full_name: &str,
date: Date,
merged: Vec<(CentsCompact, Sats)>,
) -> Result<()> {
let dir = states_path.join(format!("utxo_{name}_cost_basis/by_date"));
let dir = states_path.join(full_name).join("urpd");
fs::create_dir_all(&dir)?;
fs::write(
dir.join(date.to_string()),
CostBasisDistribution::serialize_iter(merged.into_iter())?,
UrpdRaw::serialize_iter(merged.into_iter())?,
)?;
Ok(())
}
@@ -11,7 +11,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, VecIndex, WritableVec, unli
use crate::{
distribution::{
addr::{AddrTypeToActivityCounts, AddrTypeToAddrCount},
addr::AddrMetricsState,
block::{
AddrCache, InputsResult, process_inputs, process_outputs, process_received,
process_sent,
@@ -190,24 +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) = if starting_height > Height::ZERO {
let addr_counts =
AddrTypeToAddrCount::from((&vecs.addrs.funded.by_addr_type, starting_height));
let empty_addr_counts =
AddrTypeToAddrCount::from((&vecs.addrs.empty.by_addr_type, starting_height));
(addr_counts, empty_addr_counts)
} else {
(
AddrTypeToAddrCount::default(),
AddrTypeToAddrCount::default(),
)
};
debug!("addr_counts recovered");
// Track activity counts - reset each block
let mut activity_counts = AddrTypeToActivityCounts::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();
@@ -223,9 +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.par_iter_height_mut())
.chain(rayon::iter::once(
&mut vecs.coinblocks_destroyed.block as &mut dyn AnyStoredVec,
))
@@ -276,8 +259,7 @@ pub(crate) fn process_blocks(
p2wsh: TypeIndex::from(first_p2wsh_vec[offset].to_usize()),
};
// Reset per-block activity counts
activity_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
@@ -438,27 +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 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 state,
&received_addrs,
height_to_price_vec,
height_to_timestamp_vec,
@@ -473,14 +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);
let activity_totals = state.activity.totals();
let active_addr_count =
activity_totals.sending + activity_totals.receiving - activity_totals.bidirectional;
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));
@@ -553,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| {
@@ -76,9 +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.par_iter_stateful_height_mut())
.chain(
[
&mut vecs.supply_state as &mut dyn AnyStoredVec,
@@ -9,7 +9,7 @@ use crate::{
metrics::ImportConfig,
state::{CohortState, CostBasisOps, RealizedOps},
},
internal::{AmountPerBlockCumulativeRolling, PerBlockCumulativeRolling},
internal::{ValuePerBlockCumulativeRolling, PerBlockCumulativeRolling},
prices,
};
@@ -24,9 +24,9 @@ pub struct ActivityCore<M: StorageMode = Rw> {
pub coindays_destroyed: PerBlockCumulativeRolling<StoredF64, StoredF64, M>,
#[traversable(wrap = "transfer_volume", rename = "in_profit")]
pub transfer_volume_in_profit: AmountPerBlockCumulativeRolling<M>,
pub transfer_volume_in_profit: ValuePerBlockCumulativeRolling<M>,
#[traversable(wrap = "transfer_volume", rename = "in_loss")]
pub transfer_volume_in_loss: AmountPerBlockCumulativeRolling<M>,
pub transfer_volume_in_loss: ValuePerBlockCumulativeRolling<M>,
}
impl ActivityCore {
@@ -2,7 +2,7 @@ use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Indexes, StoredF32, StoredF64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, ReadableCloneableVec, Rw, StorageMode};
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
use crate::internal::{Identity, LazyPerBlock, PerBlock, Windows};
@@ -33,15 +33,10 @@ impl ActivityFull {
let v1 = Version::ONE;
let inner = ActivityCore::forced_import(cfg)?;
let coinyears_destroyed = LazyPerBlock::from_height_source::<Identity<StoredF64>>(
let coinyears_destroyed = LazyPerBlock::from_height_source::<Identity<StoredF64>, _>(
&cfg.name("coinyears_destroyed"),
cfg.version + v1,
inner
.coindays_destroyed
.sum
._1y
.height
.read_only_boxed_clone(),
inner.coindays_destroyed.sum._1y.height.clone(),
cfg.indexes,
);
@@ -8,13 +8,13 @@ use crate::{
metrics::ImportConfig,
state::{CohortState, CostBasisOps, RealizedOps},
},
internal::AmountPerBlockCumulativeRolling,
internal::ValuePerBlockCumulativeRolling,
prices,
};
#[derive(Traversable)]
pub struct ActivityMinimal<M: StorageMode = Rw> {
pub transfer_volume: AmountPerBlockCumulativeRolling<M>,
pub transfer_volume: ValuePerBlockCumulativeRolling<M>,
}
impl ActivityMinimal {
@@ -2,8 +2,7 @@ use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, Indexes, Version};
use vecdb::AnyStoredVec;
use vecdb::{Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode};
use vecdb::{AnyStoredVec, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode};
use crate::{
blocks,
@@ -119,7 +118,7 @@ impl AllCohortMetrics {
self.unrealized.compute(
starting_indexes.height,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
@@ -139,23 +138,28 @@ impl AllCohortMetrics {
self.cost_basis.compute_prices(
starting_indexes,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.unrealized.invested_capital.in_profit.cents.height,
&self.unrealized.invested_capital.in_loss.cents.height,
&self.supply.in_profit.sats.height,
&self.supply.in_loss.sats.height,
&self.unrealized.investor_cap_in_profit_raw,
&self.unrealized.investor_cap_in_loss_raw,
&self.unrealized.capitalized_cap_in_profit_raw,
&self.unrealized.capitalized_cap_in_loss_raw,
exit,
)?;
self.unrealized
.compute_sentiment(starting_indexes, &prices.cached_spot_cents, exit)?;
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
let own_supply_sats = self.supply.total.sats.height.read_only_clone();
self.supply
.compute_dominance(starting_indexes.height, &own_supply_sats, exit)?;
self.relative.compute(
starting_indexes.height,
&self.supply,
&self.unrealized,
&self.realized,
height_to_market_cap,
exit,
)?;
@@ -6,14 +6,13 @@ use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
distribution::metrics::{
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, RelativeToAll,
SupplyCore, UnrealizedCore,
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore,
},
prices,
};
/// Basic cohort metrics: no extensions, with relative (rel_to_all).
/// Used by: age_range cohorts.
/// Basic cohort metrics: no extensions, used by age_range cohorts.
#[derive(Traversable)]
pub struct BasicCohortMetrics<M: StorageMode = Rw> {
#[traversable(skip)]
@@ -23,8 +22,6 @@ pub struct BasicCohortMetrics<M: StorageMode = Rw> {
pub activity: Box<ActivityCore<M>>,
pub realized: Box<RealizedCore<M>>,
pub unrealized: Box<UnrealizedCore<M>>,
#[traversable(flatten)]
pub relative: Box<RelativeToAll<M>>,
}
impl CohortMetricsBase for BasicCohortMetrics {
@@ -51,8 +48,6 @@ impl BasicCohortMetrics {
let unrealized = UnrealizedCore::forced_import(cfg)?;
let realized = RealizedCore::forced_import(cfg)?;
let relative = RelativeToAll::forced_import(cfg)?;
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(supply),
@@ -60,7 +55,6 @@ impl BasicCohortMetrics {
activity: Box::new(ActivityCore::forced_import(cfg)?),
realized: Box::new(realized),
unrealized: Box::new(unrealized),
relative: Box::new(relative),
})
}
@@ -82,13 +76,13 @@ impl BasicCohortMetrics {
self.unrealized.compute(
starting_indexes.height,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.relative
.compute(starting_indexes.height, &self.supply, all_supply_sats, exit)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
@@ -6,8 +6,8 @@ use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
distribution::metrics::{
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, RelativeToAll,
SupplyCore, UnrealizedCore,
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore,
},
prices,
};
@@ -21,8 +21,6 @@ pub struct CoreCohortMetrics<M: StorageMode = Rw> {
pub activity: Box<ActivityCore<M>>,
pub realized: Box<RealizedCore<M>>,
pub unrealized: Box<UnrealizedCore<M>>,
#[traversable(flatten)]
pub relative: Box<RelativeToAll<M>>,
}
impl CoreCohortMetrics {
@@ -34,7 +32,6 @@ impl CoreCohortMetrics {
activity: Box::new(ActivityCore::forced_import(cfg)?),
realized: Box::new(RealizedCore::forced_import(cfg)?),
unrealized: Box::new(UnrealizedCore::forced_import(cfg)?),
relative: Box::new(RelativeToAll::forced_import(cfg)?),
})
}
@@ -140,13 +137,13 @@ impl CoreCohortMetrics {
self.unrealized.compute(
starting_indexes.height,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.relative
.compute(starting_indexes.height, &self.supply, all_supply_sats, exit)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
@@ -108,32 +108,35 @@ impl ExtendedCohortMetrics {
self.unrealized.compute(
starting_indexes.height,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.cost_basis.compute_prices(
starting_indexes,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.unrealized.invested_capital.in_profit.cents.height,
&self.unrealized.invested_capital.in_loss.cents.height,
&self.supply.in_profit.sats.height,
&self.supply.in_loss.sats.height,
&self.unrealized.investor_cap_in_profit_raw,
&self.unrealized.investor_cap_in_loss_raw,
&self.unrealized.capitalized_cap_in_profit_raw,
&self.unrealized.capitalized_cap_in_loss_raw,
exit,
)?;
self.unrealized
.compute_sentiment(starting_indexes, &prices.cached_spot_cents, exit)?;
.compute_sentiment(starting_indexes, &prices.spot.cents.height, exit)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
self.relative.compute(
starting_indexes.height,
&self.supply,
&self.unrealized,
&self.realized,
height_to_market_cap,
all_supply_sats,
&self.supply.total.usd.height,
exit,
)?;
@@ -1,7 +1,7 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, StoredU64};
use brk_types::{Height, Indexes, Sats, StoredU64};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
@@ -112,6 +112,7 @@ impl MinimalCohortMetrics {
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()> {
@@ -124,11 +125,14 @@ impl MinimalCohortMetrics {
self.unrealized.compute(
starting_indexes.height,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
@@ -1,7 +1,7 @@
use brk_cohort::Filter;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Indexes, StoredU64};
use brk_types::{Height, Indexes, Sats, StoredU64};
use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
@@ -74,6 +74,7 @@ impl TypeCohortMetrics {
&mut self,
prices: &prices::Vecs,
starting_indexes: &Indexes,
all_supply_sats: &impl ReadableVec<Height, Sats>,
all_utxo_count: &impl ReadableVec<Height, StoredU64>,
exit: &Exit,
) -> Result<()> {
@@ -86,11 +87,14 @@ impl TypeCohortMetrics {
self.unrealized.compute(
starting_indexes.height,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized.price.cents.height,
exit,
)?;
self.supply
.compute_dominance(starting_indexes.height, all_supply_sats, exit)?;
self.outputs
.compute_part2(starting_indexes.height, all_utxo_count, exit)?;
@@ -7,11 +7,11 @@ use vecdb::{BytesVec, BytesVecValue, Database, ImportableVec};
use crate::{
indexes,
internal::{
AmountPerBlock, AmountPerBlockCumulative, AmountPerBlockCumulativeRolling,
CachedWindowStarts, CentsType, FiatPerBlock, FiatPerBlockCumulativeWithSums, NumericValue,
PerBlock, PerBlockCumulativeRolling, PercentPerBlock, PercentRollingWindows, Price,
ValuePerBlock, ValuePerBlockCumulative, ValuePerBlockCumulativeRolling, FiatType,
FiatPerBlock, FiatPerBlockCumulativeWithSums, NumericValue, PerBlock,
PerBlockCumulativeRolling, PercentPerBlock, PercentRollingWindows, Price,
PriceWithRatioExtendedPerBlock, PriceWithRatioPerBlock, RatioPerBlock,
RollingWindow24hPerBlock, RollingWindows, RollingWindowsFrom1w,
RollingWindow24hPerBlock, RollingWindows, RollingWindowsFrom1w, WindowStartVec, Windows,
},
};
@@ -35,8 +35,8 @@ macro_rules! impl_config_import {
// Non-generic types
impl_config_import!(
AmountPerBlock,
AmountPerBlockCumulative,
ValuePerBlock,
ValuePerBlockCumulative,
PriceWithRatioPerBlock,
PriceWithRatioExtendedPerBlock,
RatioPerBlock<BasisPoints32>,
@@ -79,7 +79,7 @@ impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindow24hPerBlock<T>
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
}
impl ConfigImport for AmountPerBlockCumulativeRolling {
impl ConfigImport for ValuePerBlockCumulativeRolling {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(
cfg.db,
@@ -90,7 +90,7 @@ impl ConfigImport for AmountPerBlockCumulativeRolling {
)
}
}
impl<C: CentsType> ConfigImport for FiatPerBlockCumulativeWithSums<C> {
impl<C: FiatType> ConfigImport for FiatPerBlockCumulativeWithSums<C> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(
cfg.db,
@@ -106,7 +106,7 @@ impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindowsFrom1w<T> {
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
}
impl<C: CentsType> ConfigImport for FiatPerBlock<C> {
impl<C: FiatType> ConfigImport for FiatPerBlock<C> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
@@ -128,7 +128,7 @@ pub struct ImportConfig<'a> {
pub full_name: &'a str,
pub version: Version,
pub indexes: &'a indexes::Vecs,
pub cached_starts: &'a CachedWindowStarts,
pub cached_starts: &'a Windows<&'a WindowStartVec>,
}
impl<'a> ImportConfig<'a> {
@@ -153,8 +153,8 @@ impl CostBasis {
invested_cap_in_loss: &impl ReadableVec<Height, Cents>,
supply_in_profit_sats: &impl ReadableVec<Height, Sats>,
supply_in_loss_sats: &impl ReadableVec<Height, Sats>,
investor_cap_in_profit_raw: &impl ReadableVec<Height, CentsSquaredSats>,
investor_cap_in_loss_raw: &impl ReadableVec<Height, CentsSquaredSats>,
capitalized_cap_in_profit_raw: &impl ReadableVec<Height, CentsSquaredSats>,
capitalized_cap_in_loss_raw: &impl ReadableVec<Height, CentsSquaredSats>,
exit: &Exit,
) -> Result<()> {
self.in_profit.per_coin.cents.height.compute_transform3(
@@ -193,29 +193,29 @@ impl CostBasis {
)?;
self.in_profit.per_dollar.cents.height.compute_transform3(
starting_indexes.height,
investor_cap_in_profit_raw,
capitalized_cap_in_profit_raw,
invested_cap_in_profit,
spot,
|(h, investor_cap, invested_cents, spot, ..)| {
|(h, capitalized_cap, invested_cents, spot, ..)| {
let invested_raw = invested_cents.as_u128() * Sats::ONE_BTC_U128;
if invested_raw == 0 {
return (h, spot);
}
(h, Cents::new((investor_cap.inner() / invested_raw) as u64))
(h, Cents::new((capitalized_cap.inner() / invested_raw) as u64))
},
exit,
)?;
self.in_loss.per_dollar.cents.height.compute_transform3(
starting_indexes.height,
investor_cap_in_loss_raw,
capitalized_cap_in_loss_raw,
invested_cap_in_loss,
spot,
|(h, investor_cap, invested_cents, spot, ..)| {
|(h, capitalized_cap, invested_cents, spot, ..)| {
let invested_raw = invested_cents.as_u128() * Sats::ONE_BTC_U128;
if invested_raw == 0 {
return (h, spot);
}
(h, Cents::new((investor_cap.inner() / invested_raw) as u64))
(h, Cents::new((capitalized_cap.inner() / invested_raw) as u64))
},
exit,
)?;
@@ -132,8 +132,8 @@ pub use profitability::ProfitabilityMetrics;
pub use realized::{
AdjustedSopr, RealizedCore, RealizedFull, RealizedFullAccum, RealizedLike, RealizedMinimal,
};
pub use relative::{RelativeForAll, RelativeToAll, RelativeWithExtended};
pub use supply::{SupplyBase, SupplyCore};
pub use relative::{RelativeForAll, RelativeWithExtended};
pub use supply::{AvgAmountMetrics, SupplyBase, SupplyCore};
pub use unrealized::{
UnrealizedBasic, UnrealizedCore, UnrealizedFull, UnrealizedLike, UnrealizedMinimal,
};
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
use crate::{
indexes,
internal::{
AmountPerBlock, AmountPerBlockWithDeltas, CachedWindowStarts, PerBlock, RatioPerBlock,
ValuePerBlock, ValuePerBlockWithDeltas, PerBlock, RatioPerBlock, WindowStartVec, Windows,
},
prices,
};
@@ -20,7 +20,7 @@ pub struct WithSth<All, Sth = All> {
#[derive(Traversable)]
pub struct ProfitabilityBucket<M: StorageMode = Rw> {
pub supply: WithSth<AmountPerBlockWithDeltas<M>, AmountPerBlock<M>>,
pub supply: WithSth<ValuePerBlockWithDeltas<M>, ValuePerBlock<M>>,
pub realized_cap: WithSth<PerBlock<Dollars, M>>,
pub unrealized_pnl: WithSth<PerBlock<Dollars, M>>,
pub nupl: RatioPerBlock<BasisPointsSigned32, M>,
@@ -43,18 +43,18 @@ impl ProfitabilityBucket {
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
Ok(Self {
supply: WithSth {
all: AmountPerBlockWithDeltas::forced_import(
all: ValuePerBlockWithDeltas::forced_import(
db,
&format!("{name}_supply"),
version,
indexes,
cached_starts,
)?,
sth: AmountPerBlock::forced_import(
sth: ValuePerBlock::forced_import(
db,
&format!("{name}_sth_supply"),
version,
@@ -126,7 +126,7 @@ impl ProfitabilityBucket {
self.unrealized_pnl.all.height.compute_transform3(
max_from,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized_cap.all.height,
&self.supply.all.sats.height,
|(i, spot, cap, supply, ..)| {
@@ -139,7 +139,7 @@ impl ProfitabilityBucket {
)?;
self.unrealized_pnl.sth.height.compute_transform3(
max_from,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized_cap.sth.height,
&self.supply.sth.sats.height,
|(i, spot, cap, supply, ..)| {
@@ -153,7 +153,7 @@ impl ProfitabilityBucket {
self.nupl.bps.height.compute_transform3(
max_from,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.realized_cap.all.height,
&self.supply.all.sats.height,
|(i, spot, cap_dollars, supply_sats, ..)| {
@@ -267,7 +267,7 @@ impl ProfitabilityMetrics {
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let range = ProfitabilityRange::try_new(|name| {
ProfitabilityBucket::forced_import(db, name, version, indexes, cached_starts)
@@ -62,10 +62,10 @@ impl RealizedCore {
);
let neg_loss_sum = minimal.loss.sum.0.map_with_suffix(|suffix, slot| {
LazyPerBlock::from_height_source::<NegCentsUnsignedToDollars>(
LazyPerBlock::from_height_source::<NegCentsUnsignedToDollars, _>(
&cfg.name(&format!("realized_loss_neg_sum_{suffix}")),
cfg.version + Version::ONE,
slot.cents.height.read_only_boxed_clone(),
slot.cents.height.clone(),
cfg.indexes,
)
});
@@ -11,7 +11,7 @@ use crate::{
blocks,
distribution::state::{CohortState, CostBasisData, RealizedState, WithCapital},
internal::{
AmountPerBlockCumulativeRolling, FiatPerBlockCumulativeWithSums, PercentPerBlock,
ValuePerBlockCumulativeRolling, FiatPerBlockCumulativeWithSums, PercentPerBlock,
PercentRollingWindows, PriceWithRatioExtendedPerBlock, RatioCents64, RatioCentsBp32,
RatioCentsSignedCentsBps32, RatioCentsSignedDollarsBps32, RatioDollarsBp32,
RatioPerBlockPercentiles, RatioPerBlockStdDevBands, RatioSma, RollingWindows,
@@ -45,7 +45,7 @@ pub struct RealizedPeakRegret<M: StorageMode = Rw> {
}
#[derive(Traversable)]
pub struct RealizedInvestor<M: StorageMode = Rw> {
pub struct RealizedCapitalized<M: StorageMode = Rw> {
pub price: PriceWithRatioExtendedPerBlock<M>,
#[traversable(hidden)]
pub cap_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
@@ -63,7 +63,7 @@ pub struct RealizedFull<M: StorageMode = Rw> {
pub net_pnl: RealizedNetPnl<M>,
pub sopr: RealizedSopr<M>,
pub peak_regret: RealizedPeakRegret<M>,
pub investor: RealizedInvestor<M>,
pub capitalized: RealizedCapitalized<M>,
pub profit_to_loss_ratio: RollingWindows<StoredF64, M>,
@@ -108,10 +108,10 @@ impl RealizedFull {
value: cfg.import("realized_peak_regret", Version::new(3))?,
};
// Investor
let investor = RealizedInvestor {
price: cfg.import("investor_price", v0)?,
cap_raw: cfg.import("investor_cap_raw", v0)?,
// Capitalized
let capitalized = RealizedCapitalized {
price: cfg.import("capitalized_price", v0)?,
cap_raw: cfg.import("capitalized_cap_raw", v0)?,
};
// Price ratio stats
@@ -125,7 +125,7 @@ impl RealizedFull {
net_pnl,
sopr,
peak_regret,
investor,
capitalized,
profit_to_loss_ratio: cfg.import("realized_profit_to_loss_ratio", v1)?,
cap_raw: cfg.import("cap_raw", v0)?,
cap_to_own_mcap: cfg.import("realized_cap_to_own_mcap", v1)?,
@@ -151,13 +151,13 @@ impl RealizedFull {
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.investor
self.capitalized
.price
.cents
.height
.len()
.min(self.cap_raw.len())
.min(self.investor.cap_raw.len())
.min(self.capitalized.cap_raw.len())
.min(self.peak_regret.value.block.cents.len())
}
@@ -167,15 +167,15 @@ impl RealizedFull {
state: &CohortState<RealizedState, CostBasisData<WithCapital>>,
) {
self.core.push_state(state);
self.investor
self.capitalized
.price
.cents
.height
.push(state.realized.investor_price());
.push(state.realized.capitalized_price());
self.cap_raw.push(state.realized.cap_raw());
self.investor
self.capitalized
.cap_raw
.push(state.realized.investor_cap_raw());
.push(state.realized.capitalized_cap_raw());
self.peak_regret
.value
.block
@@ -185,9 +185,9 @@ impl RealizedFull {
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.core.collect_vecs_mut();
vecs.push(&mut self.investor.price.cents.height);
vecs.push(&mut self.capitalized.price.cents.height);
vecs.push(&mut self.cap_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.investor.cap_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.capitalized.cap_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.peak_regret.value.block.cents);
vecs
}
@@ -207,17 +207,17 @@ impl RealizedFull {
#[inline(always)]
pub(crate) fn push_accum(&mut self, accum: &RealizedFullAccum) {
self.cap_raw.push(accum.cap_raw);
self.investor.cap_raw.push(accum.investor_cap_raw);
self.capitalized.cap_raw.push(accum.capitalized_cap_raw);
let investor_price = {
let capitalized_price = {
let cap = accum.cap_raw.as_u128();
if cap == 0 {
Cents::ZERO
} else {
Cents::new((accum.investor_cap_raw / cap) as u64)
Cents::new((accum.capitalized_cap_raw / cap) as u64)
}
};
self.investor.price.cents.height.push(investor_price);
self.capitalized.price.cents.height.push(capitalized_price);
self.peak_regret.value.block.cents.push(accum.peak_regret());
}
@@ -243,7 +243,7 @@ impl RealizedFull {
starting_indexes: &Indexes,
height_to_supply: &impl ReadableVec<Height, Bitcoin>,
height_to_market_cap: &impl ReadableVec<Height, Dollars>,
activity_transfer_volume: &AmountPerBlockCumulativeRolling,
activity_transfer_volume: &ValuePerBlockCumulativeRolling,
exit: &Exit,
) -> Result<()> {
self.core.compute_rest_part2(
@@ -298,8 +298,8 @@ impl RealizedFull {
exit,
)?;
// Investor price ratio, percentiles and bands
self.investor
// Capitalized price ratio, percentiles and bands
self.capitalized
.price
.compute_rest(prices, starting_indexes, exit)?;
@@ -374,14 +374,14 @@ impl RealizedFull {
#[derive(Default)]
pub struct RealizedFullAccum {
pub(crate) cap_raw: CentsSats,
pub(crate) investor_cap_raw: CentsSquaredSats,
pub(crate) capitalized_cap_raw: CentsSquaredSats,
peak_regret: CentsSats,
}
impl RealizedFullAccum {
pub(crate) fn add(&mut self, state: &RealizedState) {
self.cap_raw += state.cap_raw();
self.investor_cap_raw += state.investor_cap_raw();
self.capitalized_cap_raw += state.capitalized_cap_raw();
self.peak_regret += CentsSats::new(state.peak_regret_raw());
}
@@ -118,11 +118,11 @@ impl RealizedMinimal {
|(i, cap_cents, supply, ..)| {
let cap = cap_cents.as_u128();
let supply_sats = Sats::from(supply).as_u128();
if supply_sats == 0 {
(i, Cents::ZERO)
} else {
(i, Cents::from(cap * Sats::ONE_BTC_U128 / supply_sats))
}
let cents = (cap * Sats::ONE_BTC_U128)
.checked_div(supply_sats)
.map(Cents::from)
.unwrap_or(Cents::ZERO);
(i, cents)
},
exit,
)?)
@@ -4,9 +4,9 @@ use brk_types::{Dollars, Height};
use derive_more::{Deref, DerefMut};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::distribution::metrics::{ImportConfig, SupplyCore, UnrealizedFull};
use crate::distribution::metrics::{ImportConfig, RealizedFull, SupplyCore, UnrealizedFull};
use super::{RelativeExtendedOwnPnl, RelativeFull};
use super::{RelativeExtendedOwnPnl, RelativeFull, RelativeInvestedCapital};
/// Relative metrics for the "all" cohort (base + own_pnl, NO rel_to_all).
#[derive(Deref, DerefMut, Traversable)]
@@ -17,6 +17,8 @@ pub struct RelativeForAll<M: StorageMode = Rw> {
pub base: RelativeFull<M>,
#[traversable(flatten)]
pub extended_own_pnl: RelativeExtendedOwnPnl<M>,
#[traversable(flatten)]
pub invested_capital: RelativeInvestedCapital<M>,
}
impl RelativeForAll {
@@ -24,6 +26,7 @@ impl RelativeForAll {
Ok(Self {
base: RelativeFull::forced_import(cfg)?,
extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg)?,
invested_capital: RelativeInvestedCapital::forced_import(cfg)?,
})
}
@@ -32,6 +35,7 @@ impl RelativeForAll {
max_from: Height,
supply: &SupplyCore,
unrealized: &UnrealizedFull,
realized: &RealizedFull,
market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
@@ -43,6 +47,8 @@ impl RelativeForAll {
&unrealized.gross_pnl.usd.height,
exit,
)?;
self.invested_capital
.compute(max_from, unrealized, realized, exit)?;
Ok(())
}
}
@@ -11,10 +11,10 @@ use crate::{
/// Full relative metrics (sth/lth/all tier).
#[derive(Traversable)]
pub struct RelativeFull<M: StorageMode = Rw> {
#[traversable(wrap = "supply/in_profit", rename = "to_own")]
pub supply_in_profit_to_own: PercentPerBlock<BasisPoints16, M>,
#[traversable(wrap = "supply/in_loss", rename = "to_own")]
pub supply_in_loss_to_own: PercentPerBlock<BasisPoints16, M>,
#[traversable(wrap = "supply/in_profit", rename = "share")]
pub supply_in_profit_share: PercentPerBlock<BasisPoints16, M>,
#[traversable(wrap = "supply/in_loss", rename = "share")]
pub supply_in_loss_share: PercentPerBlock<BasisPoints16, M>,
#[traversable(wrap = "unrealized/profit", rename = "to_mcap")]
pub unrealized_profit_to_mcap: PercentPerBlock<BasisPoints16, M>,
@@ -28,8 +28,8 @@ impl RelativeFull {
let v2 = Version::new(2);
Ok(Self {
supply_in_profit_to_own: cfg.import("supply_in_profit_to_own", v1)?,
supply_in_loss_to_own: cfg.import("supply_in_loss_to_own", v1)?,
supply_in_profit_share: cfg.import("supply_in_profit_share", v1)?,
supply_in_loss_share: cfg.import("supply_in_loss_share", v1)?,
unrealized_profit_to_mcap: cfg.import("unrealized_profit_to_mcap", v2)?,
unrealized_loss_to_mcap: cfg.import("unrealized_loss_to_mcap", v2)?,
})
@@ -43,14 +43,14 @@ impl RelativeFull {
market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.supply_in_profit_to_own
self.supply_in_profit_share
.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&supply.in_profit.sats.height,
&supply.total.sats.height,
exit,
)?;
self.supply_in_loss_to_own
self.supply_in_loss_share
.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&supply.in_loss.sats.height,
@@ -0,0 +1,53 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPoints16, Cents, Height, Version};
use vecdb::{Exit, Rw, StorageMode};
use crate::internal::{PercentPerBlock, RatioCentsBp16};
use crate::distribution::metrics::{ImportConfig, RealizedFull, UnrealizedFull};
/// Shares of invested capital in profit / in loss relative to own realized cap.
/// Present for cohorts with `UnrealizedFull` (all, sth, lth).
#[derive(Traversable)]
pub struct RelativeInvestedCapital<M: StorageMode = Rw> {
#[traversable(wrap = "invested_capital/in_profit", rename = "share")]
pub in_profit_share: PercentPerBlock<BasisPoints16, M>,
#[traversable(wrap = "invested_capital/in_loss", rename = "share")]
pub in_loss_share: PercentPerBlock<BasisPoints16, M>,
}
impl RelativeInvestedCapital {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let v0 = Version::ZERO;
Ok(Self {
in_profit_share: cfg.import("invested_capital_in_profit_share", v0)?,
in_loss_share: cfg.import("invested_capital_in_loss_share", v0)?,
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
unrealized: &UnrealizedFull,
realized: &RealizedFull,
exit: &Exit,
) -> Result<()> {
let realized_cap = &realized.core.minimal.cap.cents.height;
self.in_profit_share
.compute_binary::<Cents, Cents, RatioCentsBp16>(
max_from,
&unrealized.invested_capital.in_profit.cents.height,
realized_cap,
exit,
)?;
self.in_loss_share
.compute_binary::<Cents, Cents, RatioCentsBp16>(
max_from,
&unrealized.invested_capital.in_loss.cents.height,
realized_cap,
exit,
)?;
Ok(())
}
}
@@ -2,12 +2,12 @@ mod extended_own_market_cap;
mod extended_own_pnl;
mod for_all;
mod full;
mod to_all;
mod invested_capital;
mod with_extended;
pub use extended_own_market_cap::RelativeExtendedOwnMarketCap;
pub use extended_own_pnl::RelativeExtendedOwnPnl;
pub use for_all::RelativeForAll;
pub use full::RelativeFull;
pub use to_all::RelativeToAll;
pub use invested_capital::RelativeInvestedCapital;
pub use with_extended::RelativeWithExtended;
@@ -1,62 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPoints16, Height, Sats, Version};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::internal::{PercentPerBlock, RatioSatsBp16};
use crate::distribution::metrics::{ImportConfig, SupplyCore};
/// Relative-to-all metrics (not present for the "all" cohort itself).
#[derive(Traversable)]
pub struct RelativeToAll<M: StorageMode = Rw> {
#[traversable(wrap = "supply", rename = "to_circulating")]
pub supply_to_circulating: PercentPerBlock<BasisPoints16, M>,
#[traversable(wrap = "supply/in_profit", rename = "to_circulating")]
pub supply_in_profit_to_circulating: PercentPerBlock<BasisPoints16, M>,
#[traversable(wrap = "supply/in_loss", rename = "to_circulating")]
pub supply_in_loss_to_circulating: PercentPerBlock<BasisPoints16, M>,
}
impl RelativeToAll {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
supply_to_circulating: cfg.import("supply_to_circulating", Version::ONE)?,
supply_in_profit_to_circulating: cfg
.import("supply_in_profit_to_circulating", Version::ONE)?,
supply_in_loss_to_circulating: cfg
.import("supply_in_loss_to_circulating", Version::ONE)?,
})
}
pub(crate) fn compute(
&mut self,
max_from: Height,
supply: &SupplyCore,
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
) -> Result<()> {
self.supply_to_circulating
.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&supply.total.sats.height,
all_supply_sats,
exit,
)?;
self.supply_in_profit_to_circulating
.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&supply.in_profit.sats.height,
all_supply_sats,
exit,
)?;
self.supply_in_loss_to_circulating
.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&supply.in_loss.sats.height,
all_supply_sats,
exit,
)?;
Ok(())
}
}
@@ -1,14 +1,16 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Dollars, Height, Sats};
use brk_types::{Dollars, Height};
use derive_more::{Deref, DerefMut};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use crate::distribution::metrics::{ImportConfig, SupplyCore, UnrealizedFull};
use crate::distribution::metrics::{ImportConfig, RealizedFull, SupplyCore, UnrealizedFull};
use super::{RelativeExtendedOwnMarketCap, RelativeExtendedOwnPnl, RelativeFull, RelativeToAll};
use super::{
RelativeExtendedOwnMarketCap, RelativeExtendedOwnPnl, RelativeFull, RelativeInvestedCapital,
};
/// Full extended relative metrics (base + rel_to_all + own_market_cap + own_pnl).
/// Full extended relative metrics (base + own_market_cap + own_pnl + invested_capital).
/// Used by: sth, lth cohorts.
#[derive(Deref, DerefMut, Traversable)]
pub struct RelativeWithExtended<M: StorageMode = Rw> {
@@ -17,20 +19,20 @@ pub struct RelativeWithExtended<M: StorageMode = Rw> {
#[traversable(flatten)]
pub base: RelativeFull<M>,
#[traversable(flatten)]
pub rel_to_all: RelativeToAll<M>,
#[traversable(flatten)]
pub extended_own_market_cap: RelativeExtendedOwnMarketCap<M>,
#[traversable(flatten)]
pub extended_own_pnl: RelativeExtendedOwnPnl<M>,
#[traversable(flatten)]
pub invested_capital: RelativeInvestedCapital<M>,
}
impl RelativeWithExtended {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
Ok(Self {
base: RelativeFull::forced_import(cfg)?,
rel_to_all: RelativeToAll::forced_import(cfg)?,
extended_own_market_cap: RelativeExtendedOwnMarketCap::forced_import(cfg)?,
extended_own_pnl: RelativeExtendedOwnPnl::forced_import(cfg)?,
invested_capital: RelativeInvestedCapital::forced_import(cfg)?,
})
}
@@ -40,15 +42,13 @@ impl RelativeWithExtended {
max_from: Height,
supply: &SupplyCore,
unrealized: &UnrealizedFull,
realized: &RealizedFull,
market_cap: &impl ReadableVec<Height, Dollars>,
all_supply_sats: &impl ReadableVec<Height, Sats>,
own_market_cap: &impl ReadableVec<Height, Dollars>,
exit: &Exit,
) -> Result<()> {
self.base
.compute(max_from, supply, &unrealized.inner.basic, market_cap, exit)?;
self.rel_to_all
.compute(max_from, supply, all_supply_sats, exit)?;
self.extended_own_market_cap
.compute(max_from, &unrealized.inner, own_market_cap, exit)?;
self.extended_own_pnl.compute(
@@ -57,6 +57,8 @@ impl RelativeWithExtended {
&unrealized.gross_pnl.usd.height,
exit,
)?;
self.invested_capital
.compute(max_from, unrealized, realized, exit)?;
Ok(())
}
}
@@ -0,0 +1,77 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Sats, StoredU64, Version};
use vecdb::{AnyStoredVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{indexes, internal::ValuePerBlock, prices};
/// Average amount held per UTXO and per funded address.
///
/// `utxo = supply / utxo_count`, `addr = supply / funded_addr_count`.
#[derive(Traversable)]
pub struct AvgAmountMetrics<M: StorageMode = Rw> {
pub utxo: ValuePerBlock<M>,
pub addr: ValuePerBlock<M>,
}
impl AvgAmountMetrics {
pub(crate) fn forced_import(
db: &Database,
prefix: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let name = |suffix: &str| {
if prefix.is_empty() {
suffix.to_string()
} else {
format!("{prefix}_{suffix}")
}
};
Ok(Self {
utxo: ValuePerBlock::forced_import(db, &name("avg_utxo_amount"), version, indexes)?,
addr: ValuePerBlock::forced_import(db, &name("avg_addr_amount"), version, indexes)?,
})
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.utxo.sats.height as &mut dyn AnyStoredVec,
&mut self.utxo.cents.height,
&mut self.addr.sats.height,
&mut self.addr.cents.height,
]
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.utxo.sats.height.reset()?;
self.utxo.cents.height.reset()?;
self.addr.sats.height.reset()?;
self.addr.cents.height.reset()?;
Ok(())
}
pub(crate) fn compute(
&mut self,
prices: &prices::Vecs,
supply_sats: &impl ReadableVec<Height, Sats>,
utxo_count: &impl ReadableVec<Height, StoredU64>,
funded_addr_count: &impl ReadableVec<Height, StoredU64>,
max_from: Height,
exit: &Exit,
) -> Result<()> {
self.utxo
.sats
.height
.compute_divide(max_from, supply_sats, utxo_count, exit)?;
self.utxo.compute(prices, max_from, exit)?;
self.addr
.sats
.height
.compute_divide(max_from, supply_sats, funded_addr_count, exit)?;
self.addr.compute(prices, max_from, exit)?;
Ok(())
}
}
@@ -1,29 +1,33 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{BasisPointsSigned32, Height, Indexes, Sats, SatsSigned, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use brk_types::{BasisPoints16, BasisPointsSigned32, Height, Indexes, Sats, SatsSigned, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use crate::{
distribution::state::{CohortState, CostBasisOps, RealizedOps},
prices,
};
use crate::internal::{AmountPerBlock, LazyRollingDeltasFromHeight};
use crate::internal::{
LazyRollingDeltasAmountFromHeight, PercentPerBlock, RatioSatsBp16, ValuePerBlock,
};
use crate::distribution::metrics::ImportConfig;
/// Base supply metrics: total supply only (2 stored vecs).
/// Base supply metrics: total supply + dominance (share of circulating).
#[derive(Traversable)]
pub struct SupplyBase<M: StorageMode = Rw> {
pub total: AmountPerBlock<M>,
pub delta: LazyRollingDeltasFromHeight<Sats, SatsSigned, BasisPointsSigned32>,
pub total: ValuePerBlock<M>,
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, BasisPointsSigned32>,
#[traversable(rename = "dominance")]
pub dominance: PercentPerBlock<BasisPoints16, M>,
}
impl SupplyBase {
pub(crate) fn forced_import(cfg: &ImportConfig) -> Result<Self> {
let supply: AmountPerBlock = cfg.import("supply", Version::ZERO)?;
let supply: ValuePerBlock = cfg.import("supply", Version::ZERO)?;
let delta = LazyRollingDeltasFromHeight::new(
let delta = LazyRollingDeltasAmountFromHeight::new(
&cfg.name("supply_delta"),
cfg.version + Version::ONE,
&supply.sats.height,
@@ -31,9 +35,12 @@ impl SupplyBase {
cfg.indexes,
);
let dominance = cfg.import("supply_dominance", Version::ZERO)?;
Ok(Self {
total: supply,
delta,
dominance,
})
}
@@ -49,7 +56,8 @@ impl SupplyBase {
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.total.sats.height as &mut dyn AnyStoredVec,
&mut self.total.cents.height as &mut dyn AnyStoredVec,
&mut self.total.cents.height,
&mut self.dominance.bps.height,
]
}
@@ -62,6 +70,21 @@ impl SupplyBase {
self.total.compute(prices, max_from, exit)
}
pub(crate) fn compute_dominance(
&mut self,
max_from: Height,
all_supply_sats: &impl ReadableVec<Height, Sats>,
exit: &Exit,
) -> Result<()> {
self.dominance
.compute_binary::<Sats, Sats, RatioSatsBp16>(
max_from,
&self.total.sats.height,
all_supply_sats,
exit,
)
}
pub(crate) fn compute_from_stateful(
&mut self,
starting_indexes: &Indexes,
@@ -7,7 +7,7 @@ use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use crate::{distribution::state::UnrealizedState, prices};
use crate::internal::{
AmountPerBlock, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyAmountPerBlock,
ValuePerBlock, HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock,
};
use crate::distribution::metrics::ImportConfig;
@@ -22,9 +22,9 @@ pub struct SupplyCore<M: StorageMode = Rw> {
#[traversable(flatten)]
pub base: SupplyBase<M>,
pub half: LazyAmountPerBlock,
pub in_profit: AmountPerBlock<M>,
pub in_loss: AmountPerBlock<M>,
pub half: LazyValuePerBlock,
pub in_profit: ValuePerBlock<M>,
pub in_loss: ValuePerBlock<M>,
}
impl SupplyCore {
@@ -32,7 +32,7 @@ impl SupplyCore {
let v0 = Version::ZERO;
let base = SupplyBase::forced_import(cfg)?;
let half = LazyAmountPerBlock::from_block_source::<
let half = LazyValuePerBlock::from_block_source::<
HalveSats,
HalveSatsToBitcoin,
HalveCents,
@@ -1,5 +1,7 @@
mod avg_amount;
mod base;
mod core;
pub use avg_amount::AvgAmountMetrics;
pub use self::core::SupplyCore;
pub use base::SupplyBase;
@@ -33,8 +33,8 @@ pub struct UnrealizedFull<M: StorageMode = Rw> {
pub gross_pnl: FiatPerBlock<Cents, M>,
pub invested_capital: UnrealizedInvestedCapital<M>,
pub investor_cap_in_profit_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
pub investor_cap_in_loss_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
pub capitalized_cap_in_profit_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
pub capitalized_cap_in_loss_raw: M::Stored<BytesVec<Height, CentsSquaredSats>>,
pub sentiment: UnrealizedSentiment<M>,
}
@@ -53,8 +53,8 @@ impl UnrealizedFull {
in_loss: cfg.import("invested_capital_in_loss", v1)?,
};
let investor_cap_in_profit_raw = cfg.import("investor_cap_in_profit_raw", v0)?;
let investor_cap_in_loss_raw = cfg.import("investor_cap_in_loss_raw", v0)?;
let capitalized_cap_in_profit_raw = cfg.import("capitalized_cap_in_profit_raw", v0)?;
let capitalized_cap_in_loss_raw = cfg.import("capitalized_cap_in_loss_raw", v0)?;
let sentiment = UnrealizedSentiment {
pain_index: cfg.import("pain_index", v1)?,
@@ -66,33 +66,33 @@ impl UnrealizedFull {
inner,
gross_pnl,
invested_capital,
investor_cap_in_profit_raw,
investor_cap_in_loss_raw,
capitalized_cap_in_profit_raw,
capitalized_cap_in_loss_raw,
sentiment,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
// Only check per-block pushed vecs (investor_cap_raw).
// Only check per-block pushed vecs (capitalized_cap_raw).
// Core-level vecs (profit/loss) are aggregated from age_range, not stateful.
self.investor_cap_in_profit_raw
self.capitalized_cap_in_profit_raw
.len()
.min(self.investor_cap_in_loss_raw.len())
.min(self.capitalized_cap_in_loss_raw.len())
}
#[inline(always)]
pub(crate) fn push_state_all(&mut self, state: &UnrealizedState) {
self.inner.push_state(state);
self.investor_cap_in_profit_raw
.push(CentsSquaredSats::new(state.investor_cap_in_profit_raw));
self.investor_cap_in_loss_raw
.push(CentsSquaredSats::new(state.investor_cap_in_loss_raw));
self.capitalized_cap_in_profit_raw
.push(CentsSquaredSats::new(state.capitalized_cap_in_profit_raw));
self.capitalized_cap_in_loss_raw
.push(CentsSquaredSats::new(state.capitalized_cap_in_loss_raw));
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.inner.collect_vecs_mut();
vecs.push(&mut self.investor_cap_in_profit_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.investor_cap_in_loss_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.capitalized_cap_in_profit_raw as &mut dyn AnyStoredVec);
vecs.push(&mut self.capitalized_cap_in_loss_raw as &mut dyn AnyStoredVec);
vecs
}
@@ -122,7 +122,7 @@ impl UnrealizedFull {
.compute_transform3(
starting_indexes.height,
supply_in_profit_sats,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.inner.basic.profit.cents.height,
|(h, supply_sats, spot, profit, ..): (_, Sats, Cents, Cents, _)| {
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
@@ -142,7 +142,7 @@ impl UnrealizedFull {
.compute_transform3(
starting_indexes.height,
supply_in_loss_sats,
&prices.cached_spot_cents,
&prices.spot.cents.height,
&self.inner.basic.loss.cents.height,
|(h, supply_sats, spot, loss, ..): (_, Sats, Cents, Cents, _)| {
let market_value = supply_sats.as_u128() * spot.as_u128() / Sats::ONE_BTC_U128;
@@ -154,7 +154,7 @@ impl UnrealizedFull {
Ok(())
}
/// Compute sentiment using investor_price (original formula).
/// Compute sentiment using capitalized_price (original formula).
/// Called after cost_basis.in_profit/loss are computed at the cohort level.
pub(crate) fn compute_sentiment(
&mut self,
@@ -162,45 +162,45 @@ impl UnrealizedFull {
spot: &impl ReadableVec<Height, Cents>,
exit: &Exit,
) -> Result<()> {
// greed = spot - investor_price_winners
// investor_price = investor_cap / invested_cap
// greed = spot - capitalized_price_winners
// capitalized_price = capitalized_cap / invested_cap
// invested_cap is in Cents (already / ONE_BTC), multiply back for CentsSats scale
self.sentiment.greed_index.cents.height.compute_transform3(
starting_indexes.height,
&self.investor_cap_in_profit_raw,
&self.capitalized_cap_in_profit_raw,
&self.invested_capital.in_profit.cents.height,
spot,
|(h, investor_cap, invested_cap_cents, spot, ..)| {
|(h, capitalized_cap, invested_cap_cents, spot, ..)| {
let invested_cap_raw = invested_cap_cents.as_u128() * Sats::ONE_BTC_U128;
if invested_cap_raw == 0 {
return (h, Cents::ZERO);
}
let investor_price = investor_cap.inner() / invested_cap_raw;
let capitalized_price = capitalized_cap.inner() / invested_cap_raw;
let spot_u128 = spot.as_u128();
(
h,
Cents::new(spot_u128.saturating_sub(investor_price) as u64),
Cents::new(spot_u128.saturating_sub(capitalized_price) as u64),
)
},
exit,
)?;
// pain = investor_price_losers - spot
// pain = capitalized_price_losers - spot
self.sentiment.pain_index.cents.height.compute_transform3(
starting_indexes.height,
&self.investor_cap_in_loss_raw,
&self.capitalized_cap_in_loss_raw,
&self.invested_capital.in_loss.cents.height,
spot,
|(h, investor_cap, invested_cap_cents, spot, ..)| {
|(h, capitalized_cap, invested_cap_cents, spot, ..)| {
let invested_cap_raw = invested_cap_cents.as_u128() * Sats::ONE_BTC_U128;
if invested_cap_raw == 0 {
return (h, Cents::ZERO);
}
let investor_price = investor_cap.inner() / invested_cap_raw;
let capitalized_price = capitalized_cap.inner() / invested_cap_raw;
let spot_u128 = spot.as_u128();
(
h,
Cents::new(investor_price.saturating_sub(spot_u128) as u64),
Cents::new(capitalized_price.saturating_sub(spot_u128) as u64),
)
},
exit,
@@ -18,7 +18,7 @@ pub struct SendPrecomputed {
pub current_ps: CentsSats,
pub prev_ps: CentsSats,
pub ath_ps: CentsSats,
pub prev_investor_cap: CentsSquaredSats,
pub prev_capitalized_cap: CentsSquaredSats,
}
impl SendPrecomputed {
@@ -42,7 +42,7 @@ impl SendPrecomputed {
} else {
CentsSats::from_price_sats(ath, sats)
};
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
let prev_capitalized_cap = prev_ps.to_capitalized_cap(prev_price);
Some(Self {
sats,
prev_price,
@@ -50,7 +50,7 @@ impl SendPrecomputed {
current_ps,
prev_ps,
ath_ps,
prev_investor_cap,
prev_capitalized_cap,
})
}
}
@@ -90,7 +90,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
pub(crate) fn restore_realized_cap(&mut self) {
self.realized.set_cap_raw(self.cost_basis.cap_raw());
self.realized
.set_investor_cap_raw(self.cost_basis.investor_cap_raw());
.set_capitalized_cap_raw(self.cost_basis.capitalized_cap_raw());
}
pub(crate) fn reset_cost_basis_data_if_needed(&mut self) -> Result<()> {
@@ -117,12 +117,12 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
if s.supply_state.value > Sats::ZERO {
self.realized
.increment_snapshot(s.price_sats, s.investor_cap);
.increment_snapshot(s.price_sats, s.capitalized_cap_raw);
self.cost_basis.increment(
s.realized_price,
s.supply_state.value,
s.price_sats,
s.investor_cap,
s.capitalized_cap_raw,
);
}
}
@@ -132,12 +132,12 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
if s.supply_state.value > Sats::ZERO {
self.realized
.decrement_snapshot(s.price_sats, s.investor_cap);
.decrement_snapshot(s.price_sats, s.capitalized_cap_raw);
self.cost_basis.decrement(
s.realized_price,
s.supply_state.value,
s.price_sats,
s.investor_cap,
s.capitalized_cap_raw,
);
}
}
@@ -162,7 +162,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
snapshot.realized_price,
supply.value,
snapshot.price_sats,
snapshot.investor_cap,
snapshot.capitalized_cap_raw,
);
}
}
@@ -184,7 +184,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
current.realized_price,
current.supply_state.value,
current.price_sats,
current.investor_cap,
current.capitalized_cap_raw,
);
}
@@ -193,7 +193,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
prev.investor_cap,
prev.capitalized_cap_raw,
);
}
}
@@ -212,11 +212,11 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
pre.current_ps,
pre.prev_ps,
pre.ath_ps,
pre.prev_investor_cap,
pre.prev_capitalized_cap,
);
self.cost_basis
.decrement(pre.prev_price, pre.sats, pre.prev_ps, pre.prev_investor_cap);
.decrement(pre.prev_price, pre.sats, pre.prev_ps, pre.prev_capitalized_cap);
}
pub(crate) fn send_utxo(
@@ -265,17 +265,17 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
let current_ps = CentsSats::from_price_sats(current_price, sats);
let prev_ps = CentsSats::from_price_sats(prev_price, sats);
let ath_ps = CentsSats::from_price_sats(ath, sats);
let prev_investor_cap = prev_ps.to_investor_cap(prev_price);
let prev_capitalized_cap = prev_ps.to_capitalized_cap(prev_price);
self.realized
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
.send(sats, current_ps, prev_ps, ath_ps, prev_capitalized_cap);
if current.supply_state.value.is_not_zero() {
self.cost_basis.increment(
current.realized_price,
current.supply_state.value,
current.price_sats,
current.investor_cap,
current.capitalized_cap_raw,
);
}
@@ -284,7 +284,7 @@ impl<R: RealizedOps, C: CostBasisOps> CohortState<R, C> {
prev.realized_price,
prev.supply_state.value,
prev.price_sats,
prev.investor_cap,
prev.capitalized_cap_raw,
);
}
}
@@ -6,13 +6,13 @@ use std::{
use brk_error::{Error, Result};
use brk_types::{
Cents, CentsCompact, CentsSats, CentsSquaredSats, CostBasisDistribution, Height, Sats,
Cents, CentsCompact, CentsSats, CentsSquaredSats, UrpdRaw, Height, Sats,
};
use rustc_hash::FxHashMap;
use vecdb::{Bytes, unlikely};
use super::{Accumulate, CachedUnrealizedState, UnrealizedState};
use crate::distribution::state::pending::{PendingCapDelta, PendingDelta, PendingInvestorCapDelta};
use crate::distribution::state::pending::{PendingCapDelta, PendingDelta, PendingCapitalizedCapRawDelta};
/// Type alias for the price-to-sats map used in cost basis data.
pub(super) type CostBasisMap = BTreeMap<CentsCompact, Sats>;
@@ -27,20 +27,20 @@ pub trait CostBasisOps: Send + Sync + 'static {
fn with_price_rounding(self, digits: i32) -> Self;
fn import_at_or_before(&mut self, height: Height) -> Result<Height>;
fn cap_raw(&self) -> CentsSats;
fn investor_cap_raw(&self) -> CentsSquaredSats;
fn capitalized_cap_raw(&self) -> CentsSquaredSats;
fn increment(
&mut self,
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
capitalized_cap: CentsSquaredSats,
);
fn decrement(
&mut self,
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
capitalized_cap: CentsSquaredSats,
);
fn apply_pending(&mut self);
fn init(&mut self);
@@ -78,23 +78,18 @@ pub struct CostBasisRaw {
}
impl CostBasisRaw {
pub(super) fn path_by_height(&self) -> PathBuf {
self.pathbuf.join("by_height")
}
pub(super) fn path_state(&self, height: Height) -> PathBuf {
self.path_by_height().join(height.to_string())
self.pathbuf.join(height.to_string())
}
pub(super) fn read_dir(
&self,
keep_only_before: Option<Height>,
) -> Result<BTreeMap<Height, PathBuf>> {
let by_height = self.path_by_height();
if !by_height.exists() {
if !self.pathbuf.exists() {
return Ok(BTreeMap::new());
}
Ok(fs::read_dir(&by_height)?
Ok(fs::read_dir(&self.pathbuf)?
.filter_map(|entry| {
let path = entry.ok()?.path();
let name = path.file_name()?.to_str()?;
@@ -150,7 +145,7 @@ impl CostBasisRaw {
impl CostBasisOps for CostBasisRaw {
fn create(path: &Path, name: &str) -> Self {
Self {
pathbuf: path.join(format!("{name}_cost_basis")),
pathbuf: path.join(name).join("cost_basis"),
state: None,
pending_cap: PendingCapDelta::default(),
}
@@ -170,7 +165,7 @@ impl CostBasisOps for CostBasisRaw {
self.state = Some(if data.len() == 16 {
RawState::deserialize(&data)?
} else {
let (_, rest) = CostBasisDistribution::deserialize_with_rest(&data)?;
let (_, rest) = UrpdRaw::deserialize_with_rest(&data)?;
RawState::deserialize(rest)?
});
self.pending_cap = PendingCapDelta::default();
@@ -182,7 +177,7 @@ impl CostBasisOps for CostBasisRaw {
self.state.as_ref().unwrap().cap_raw
}
fn investor_cap_raw(&self) -> CentsSquaredSats {
fn capitalized_cap_raw(&self) -> CentsSquaredSats {
CentsSquaredSats::ZERO
}
@@ -192,7 +187,7 @@ impl CostBasisOps for CostBasisRaw {
_price: Cents,
_sats: Sats,
price_sats: CentsSats,
_investor_cap: CentsSquaredSats,
_capitalized_cap: CentsSquaredSats,
) {
self.pending_cap.inc += price_sats;
}
@@ -203,7 +198,7 @@ impl CostBasisOps for CostBasisRaw {
_price: Cents,
_sats: Sats,
price_sats: CentsSats,
_investor_cap: CentsSquaredSats,
_capitalized_cap: CentsSquaredSats,
) {
self.pending_cap.dec += price_sats;
}
@@ -219,7 +214,7 @@ impl CostBasisOps for CostBasisRaw {
fn clean(&mut self) -> Result<()> {
let _ = fs::remove_dir_all(&self.pathbuf);
fs::create_dir_all(self.path_by_height())?;
fs::create_dir_all(&self.pathbuf)?;
Ok(())
}
@@ -240,17 +235,17 @@ impl CostBasisOps for CostBasisRaw {
/// Composes `CostBasisRaw` for scalar tracking, adds map, pending, and cache.
///
/// Generic over the accumulator `S`:
/// - `WithCapital`: tracks all fields including invested capital + investor cap (128 bytes)
/// - `WithCapital`: tracks all fields including invested capital + capitalized cap (128 bytes)
/// - `WithoutCapital`: tracks only supply + unrealized profit/loss (64 bytes, 1 cache line)
#[derive(Clone, Debug)]
pub struct CostBasisData<S: Accumulate> {
raw: CostBasisRaw,
map: Option<CostBasisDistribution>,
map: Option<UrpdRaw>,
pending: FxHashMap<CentsCompact, PendingDelta>,
cache: Option<CachedUnrealizedState<S>>,
rounding_digits: Option<i32>,
investor_cap_raw: CentsSquaredSats,
pending_investor_cap: PendingInvestorCapDelta,
capitalized_cap_raw: CentsSquaredSats,
pending_capitalized_cap: PendingCapitalizedCapRawDelta,
}
impl<S: Accumulate> CostBasisData<S> {
@@ -351,8 +346,8 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
pending: FxHashMap::default(),
cache: None,
rounding_digits: None,
investor_cap_raw: CentsSquaredSats::ZERO,
pending_investor_cap: PendingInvestorCapDelta::default(),
capitalized_cap_raw: CentsSquaredSats::ZERO,
pending_capitalized_cap: PendingCapitalizedCapRawDelta::default(),
}
}
@@ -367,7 +362,7 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
"No cost basis state found at or before height".into(),
))?;
let data = fs::read(path)?;
let (base, rest) = CostBasisDistribution::deserialize_with_rest(&data)?;
let (base, rest) = UrpdRaw::deserialize_with_rest(&data)?;
self.map = Some(base);
self.raw.state = Some(RawState::deserialize(rest)?);
debug_assert!(
@@ -375,10 +370,10 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
"CostBasisData state too short: {} bytes",
rest.len()
);
self.investor_cap_raw = CentsSquaredSats::from_bytes(&rest[16..32])?;
self.capitalized_cap_raw = CentsSquaredSats::from_bytes(&rest[16..32])?;
self.pending.clear();
self.raw.pending_cap = PendingCapDelta::default();
self.pending_investor_cap = PendingInvestorCapDelta::default();
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
self.cache = None;
Ok(height)
}
@@ -387,8 +382,8 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
self.raw.cap_raw()
}
fn investor_cap_raw(&self) -> CentsSquaredSats {
self.investor_cap_raw
fn capitalized_cap_raw(&self) -> CentsSquaredSats {
self.capitalized_cap_raw
}
#[inline]
@@ -397,13 +392,13 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
capitalized_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
self.pending.entry(price.into()).or_default().inc += sats;
self.raw.pending_cap.inc += price_sats;
if investor_cap != CentsSquaredSats::ZERO {
self.pending_investor_cap.inc += investor_cap;
if capitalized_cap != CentsSquaredSats::ZERO {
self.pending_capitalized_cap.inc += capitalized_cap;
}
if let Some(cache) = self.cache.as_mut() {
cache.on_receive(price, sats);
@@ -416,13 +411,13 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
price: Cents,
sats: Sats,
price_sats: CentsSats,
investor_cap: CentsSquaredSats,
capitalized_cap: CentsSquaredSats,
) {
let price = self.round_price(price);
self.pending.entry(price.into()).or_default().dec += sats;
self.raw.pending_cap.dec += price_sats;
if investor_cap != CentsSquaredSats::ZERO {
self.pending_investor_cap.dec += investor_cap;
if capitalized_cap != CentsSquaredSats::ZERO {
self.pending_capitalized_cap.dec += capitalized_cap;
}
if let Some(cache) = self.cache.as_mut() {
cache.on_send(price, sats);
@@ -432,28 +427,28 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
fn apply_pending(&mut self) {
self.apply_map_pending();
self.raw.apply_pending_cap();
self.investor_cap_raw += self.pending_investor_cap.inc;
self.capitalized_cap_raw += self.pending_capitalized_cap.inc;
debug_assert!(
self.investor_cap_raw >= self.pending_investor_cap.dec,
"CostBasis investor_cap_raw underflow!\n\
self.capitalized_cap_raw >= self.pending_capitalized_cap.dec,
"CostBasis capitalized_cap_raw underflow!\n\
Path: {:?}\n\
Current (after increments): {:?}\n\
Trying to decrement by: {:?}",
self.raw.pathbuf,
self.investor_cap_raw,
self.pending_investor_cap.dec
self.capitalized_cap_raw,
self.pending_capitalized_cap.dec
);
self.investor_cap_raw -= self.pending_investor_cap.dec;
self.pending_investor_cap = PendingInvestorCapDelta::default();
self.capitalized_cap_raw -= self.pending_capitalized_cap.dec;
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
}
fn init(&mut self) {
self.raw.init();
self.map.replace(CostBasisDistribution::default());
self.map.replace(UrpdRaw::default());
self.pending.clear();
self.cache = None;
self.investor_cap_raw = CentsSquaredSats::ZERO;
self.pending_investor_cap = PendingInvestorCapDelta::default();
self.capitalized_cap_raw = CentsSquaredSats::ZERO;
self.pending_capitalized_cap = PendingCapitalizedCapRawDelta::default();
}
fn clean(&mut self) -> Result<()> {
@@ -469,7 +464,7 @@ impl<S: Accumulate> CostBasisOps for CostBasisData<S> {
let raw_state = self.raw.state.as_ref().unwrap();
let mut buffer = self.map.as_ref().unwrap().serialize()?;
buffer.extend(raw_state.cap_raw.to_bytes());
buffer.extend(self.investor_cap_raw.to_bytes());
buffer.extend(self.capitalized_cap_raw.to_bytes());
fs::write(self.raw.path_state(height), buffer)?;
Ok(())
@@ -18,11 +18,11 @@ pub trait RealizedOps: Default + Clone + Send + Sync + 'static {
Sats::ZERO
}
fn set_cap_raw(&mut self, cap_raw: CentsSats);
fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats);
fn set_capitalized_cap_raw(&mut self, capitalized_cap_raw: CentsSquaredSats);
fn reset_single_iteration_values(&mut self);
fn increment(&mut self, price: Cents, sats: Sats);
fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats);
fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats);
fn increment_snapshot(&mut self, price_sats: CentsSats, capitalized_cap: CentsSquaredSats);
fn decrement_snapshot(&mut self, price_sats: CentsSats, capitalized_cap: CentsSquaredSats);
fn receive(&mut self, price: Cents, sats: Sats) {
self.increment(price, sats);
}
@@ -32,7 +32,7 @@ pub trait RealizedOps: Default + Clone + Send + Sync + 'static {
current_ps: CentsSats,
prev_ps: CentsSats,
ath_ps: CentsSats,
prev_investor_cap: CentsSquaredSats,
prev_capitalized_cap: CentsSquaredSats,
);
}
@@ -85,7 +85,7 @@ impl RealizedOps for MinimalRealizedState {
}
#[inline]
fn set_investor_cap_raw(&mut self, _investor_cap_raw: CentsSquaredSats) {}
fn set_capitalized_cap_raw(&mut self, _capitalized_cap_raw: CentsSquaredSats) {}
#[inline]
fn reset_single_iteration_values(&mut self) {
@@ -104,12 +104,12 @@ impl RealizedOps for MinimalRealizedState {
}
#[inline]
fn increment_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) {
fn increment_snapshot(&mut self, price_sats: CentsSats, _capitalized_cap: CentsSquaredSats) {
self.cap_raw += price_sats.as_u128();
}
#[inline]
fn decrement_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) {
fn decrement_snapshot(&mut self, price_sats: CentsSats, _capitalized_cap: CentsSquaredSats) {
self.cap_raw -= price_sats.as_u128();
}
@@ -120,7 +120,7 @@ impl RealizedOps for MinimalRealizedState {
current_ps: CentsSats,
prev_ps: CentsSats,
_ath_ps: CentsSats,
_prev_investor_cap: CentsSquaredSats,
_prev_capitalized_cap: CentsSquaredSats,
) {
match current_ps.cmp(&prev_ps) {
Ordering::Greater => {
@@ -184,7 +184,7 @@ impl RealizedOps for CoreRealizedState {
}
#[inline]
fn set_investor_cap_raw(&mut self, _investor_cap_raw: CentsSquaredSats) {}
fn set_capitalized_cap_raw(&mut self, _capitalized_cap_raw: CentsSquaredSats) {}
#[inline]
fn reset_single_iteration_values(&mut self) {
@@ -199,13 +199,13 @@ impl RealizedOps for CoreRealizedState {
}
#[inline]
fn increment_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) {
self.minimal.increment_snapshot(price_sats, _investor_cap);
fn increment_snapshot(&mut self, price_sats: CentsSats, _capitalized_cap: CentsSquaredSats) {
self.minimal.increment_snapshot(price_sats, _capitalized_cap);
}
#[inline]
fn decrement_snapshot(&mut self, price_sats: CentsSats, _investor_cap: CentsSquaredSats) {
self.minimal.decrement_snapshot(price_sats, _investor_cap);
fn decrement_snapshot(&mut self, price_sats: CentsSats, _capitalized_cap: CentsSquaredSats) {
self.minimal.decrement_snapshot(price_sats, _capitalized_cap);
}
#[inline]
@@ -215,10 +215,10 @@ impl RealizedOps for CoreRealizedState {
current_ps: CentsSats,
prev_ps: CentsSats,
ath_ps: CentsSats,
prev_investor_cap: CentsSquaredSats,
prev_capitalized_cap: CentsSquaredSats,
) {
self.minimal
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
.send(sats, current_ps, prev_ps, ath_ps, prev_capitalized_cap);
match current_ps.cmp(&prev_ps) {
Ordering::Greater | Ordering::Equal => {
self.sent_in_profit += sats;
@@ -242,8 +242,8 @@ impl CoreRealizedState {
#[derive(Debug, Default, Clone)]
pub struct RealizedState {
core: CoreRealizedState,
/// Raw investor cap: Σ(price² × sats)
investor_cap_raw: CentsSquaredSats,
/// Raw capitalized cap: Σ(price² × sats)
capitalized_cap_raw: CentsSquaredSats,
/// Raw realized peak regret: Σ((peak - sell_price) × sats)
peak_regret_raw: u128,
}
@@ -287,8 +287,8 @@ impl RealizedOps for RealizedState {
}
#[inline]
fn set_investor_cap_raw(&mut self, investor_cap_raw: CentsSquaredSats) {
self.investor_cap_raw = investor_cap_raw;
fn set_capitalized_cap_raw(&mut self, capitalized_cap_raw: CentsSquaredSats) {
self.capitalized_cap_raw = capitalized_cap_raw;
}
#[inline]
@@ -301,20 +301,20 @@ impl RealizedOps for RealizedState {
fn increment(&mut self, price: Cents, sats: Sats) {
self.core.increment(price, sats);
if sats.is_not_zero() {
self.investor_cap_raw += CentsSats::from_price_sats(price, sats).to_investor_cap(price);
self.capitalized_cap_raw += CentsSats::from_price_sats(price, sats).to_capitalized_cap(price);
}
}
#[inline]
fn increment_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
self.core.increment_snapshot(price_sats, investor_cap);
self.investor_cap_raw += investor_cap;
fn increment_snapshot(&mut self, price_sats: CentsSats, capitalized_cap: CentsSquaredSats) {
self.core.increment_snapshot(price_sats, capitalized_cap);
self.capitalized_cap_raw += capitalized_cap;
}
#[inline]
fn decrement_snapshot(&mut self, price_sats: CentsSats, investor_cap: CentsSquaredSats) {
self.core.decrement_snapshot(price_sats, investor_cap);
self.investor_cap_raw -= investor_cap;
fn decrement_snapshot(&mut self, price_sats: CentsSats, capitalized_cap: CentsSquaredSats) {
self.core.decrement_snapshot(price_sats, capitalized_cap);
self.capitalized_cap_raw -= capitalized_cap;
}
#[inline]
@@ -324,26 +324,26 @@ impl RealizedOps for RealizedState {
current_ps: CentsSats,
prev_ps: CentsSats,
ath_ps: CentsSats,
prev_investor_cap: CentsSquaredSats,
prev_capitalized_cap: CentsSquaredSats,
) {
self.core
.send(sats, current_ps, prev_ps, ath_ps, prev_investor_cap);
.send(sats, current_ps, prev_ps, ath_ps, prev_capitalized_cap);
self.peak_regret_raw += (ath_ps - current_ps).as_u128();
self.investor_cap_raw -= prev_investor_cap;
self.capitalized_cap_raw -= prev_capitalized_cap;
}
}
impl RealizedState {
/// Get investor price as CentsUnsigned.
/// investor_price = Σ(price² × sats) / Σ(price × sats)
/// Get capitalized price as CentsUnsigned.
/// capitalized_price = Σ(price² × sats) / Σ(price × sats)
#[inline]
pub(crate) fn investor_price(&self) -> Cents {
pub(crate) fn capitalized_price(&self) -> Cents {
let cap_raw = self.core.cap_raw_u128();
if cap_raw == 0 {
return Cents::ZERO;
}
Cents::new((self.investor_cap_raw / cap_raw) as u64)
Cents::new((self.capitalized_cap_raw / cap_raw) as u64)
}
/// Get raw realized cap for aggregation.
@@ -352,10 +352,10 @@ impl RealizedState {
CentsSats::new(self.core.cap_raw_u128())
}
/// Get raw investor cap for aggregation.
/// Get raw capitalized cap for aggregation.
#[inline]
pub(crate) fn investor_cap_raw(&self) -> CentsSquaredSats {
self.investor_cap_raw
pub(crate) fn capitalized_cap_raw(&self) -> CentsSquaredSats {
self.capitalized_cap_raw
}
/// Get realized peak regret as CentsUnsigned.
@@ -10,8 +10,8 @@ pub struct UnrealizedState {
pub supply_in_loss: Sats,
pub unrealized_profit: Cents,
pub unrealized_loss: Cents,
pub investor_cap_in_profit_raw: u128,
pub investor_cap_in_loss_raw: u128,
pub capitalized_cap_in_profit_raw: u128,
pub capitalized_cap_in_loss_raw: u128,
}
impl UnrealizedState {
@@ -20,8 +20,8 @@ impl UnrealizedState {
supply_in_loss: Sats::ZERO,
unrealized_profit: Cents::ZERO,
unrealized_loss: Cents::ZERO,
investor_cap_in_profit_raw: 0,
investor_cap_in_loss_raw: 0,
capitalized_cap_in_profit_raw: 0,
capitalized_cap_in_loss_raw: 0,
};
}
@@ -34,12 +34,12 @@ pub struct WithoutCapital {
pub(crate) unrealized_loss: u128,
}
/// Full cache state: core + investor cap (for sentiment computation).
/// Full cache state: core + capitalized cap (for sentiment computation).
#[derive(Debug, Default, Clone)]
pub struct WithCapital {
core: WithoutCapital,
investor_cap_in_profit: u128,
investor_cap_in_loss: u128,
capitalized_cap_in_profit: u128,
capitalized_cap_in_loss: u128,
}
#[inline(always)]
@@ -116,8 +116,8 @@ impl Accumulate for WithoutCapital {
impl Accumulate for WithCapital {
fn to_output(&self) -> UnrealizedState {
UnrealizedState {
investor_cap_in_profit_raw: self.investor_cap_in_profit,
investor_cap_in_loss_raw: self.investor_cap_in_loss,
capitalized_cap_in_profit_raw: self.capitalized_cap_in_profit,
capitalized_cap_in_loss_raw: self.capitalized_cap_in_loss,
..Accumulate::to_output(&self.core)
}
}
@@ -133,25 +133,25 @@ impl Accumulate for WithCapital {
fn accumulate_profit(&mut self, price_u128: u128, sats: Sats) {
self.core.supply_in_profit += sats;
let invested = price_u128 * sats.as_u128();
self.investor_cap_in_profit += price_u128 * invested;
self.capitalized_cap_in_profit += price_u128 * invested;
}
#[inline(always)]
fn accumulate_loss(&mut self, price_u128: u128, sats: Sats) {
self.core.supply_in_loss += sats;
let invested = price_u128 * sats.as_u128();
self.investor_cap_in_loss += price_u128 * invested;
self.capitalized_cap_in_loss += price_u128 * invested;
}
#[inline(always)]
fn deaccumulate_profit(&mut self, price_u128: u128, sats: Sats) {
self.core.supply_in_profit -= sats;
let invested = price_u128 * sats.as_u128();
self.investor_cap_in_profit -= price_u128 * invested;
self.capitalized_cap_in_profit -= price_u128 * invested;
}
#[inline(always)]
fn deaccumulate_loss(&mut self, price_u128: u128, sats: Sats) {
self.core.supply_in_loss -= sats;
let invested = price_u128 * sats.as_u128();
self.investor_cap_in_loss -= price_u128 * invested;
self.capitalized_cap_in_loss -= price_u128 * invested;
}
}
@@ -13,7 +13,7 @@ impl PendingCapDelta {
}
#[derive(Clone, Debug, Default)]
pub(crate) struct PendingInvestorCapDelta {
pub(crate) struct PendingCapitalizedCapRawDelta {
pub inc: CentsSquaredSats,
pub dec: CentsSquaredSats,
}
+177 -14
View File
@@ -1,5 +1,6 @@
use std::path::{Path, PathBuf};
use brk_cohort::{ByAddrType, Filter};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_traversable::Traversable;
@@ -7,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,
};
@@ -24,7 +26,7 @@ use crate::{
},
indexes, inputs,
internal::{
CachedWindowStarts, PerBlockCumulativeRolling,
PerBlockCumulativeRolling, WindowStartVec, Windows, WithAddrTypes,
db_utils::{finalize_db, open_db},
},
outputs, prices, transactions,
@@ -32,10 +34,14 @@ use crate::{
use super::{
AddrCohorts, AddrsDataVecs, AnyAddrIndexesVecs, RangeMap, UTXOCohorts,
addr::{AddrActivityVecs, AddrCountsVecs, DeltaVecs, NewAddrCountVecs, TotalAddrCountVecs},
addr::{
AddrActivityVecs, AddrCountsVecs, AddrMetricsState, DeltaVecs, ExposedAddrVecs,
NewAddrCountVecs, ReusedAddrVecs, TotalAddrCountVecs,
},
metrics::AvgAmountMetrics,
};
const VERSION: Version = Version::new(22);
const VERSION: Version = Version::new(23);
#[derive(Traversable)]
pub struct AddrMetricsVecs<M: StorageMode = Rw> {
@@ -44,7 +50,11 @@ pub struct AddrMetricsVecs<M: StorageMode = Rw> {
pub activity: AddrActivityVecs<M>,
pub total: TotalAddrCountVecs<M>,
pub new: NewAddrCountVecs<M>,
pub reused: ReusedAddrVecs<M>,
pub respent: ReusedAddrVecs<M>,
pub exposed: ExposedAddrVecs<M>,
pub delta: DeltaVecs,
pub avg_amount: WithAddrTypes<AvgAmountMetrics<M>>,
#[traversable(wrap = "indexes", rename = "funded")]
pub funded_index:
LazyVecFrom1<FundedAddrIndex, FundedAddrIndex, FundedAddrIndex, FundedAddrData>,
@@ -52,6 +62,71 @@ pub struct AddrMetricsVecs<M: StorageMode = Rw> {
pub empty_index: LazyVecFrom1<EmptyAddrIndex, EmptyAddrIndex, EmptyAddrIndex, EmptyAddrData>,
}
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<Item = &mut dyn AnyStoredVec> {
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<Item = &mut dyn AnyStoredVec> {
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<M: StorageMode = Rw> {
#[traversable(skip)]
@@ -102,7 +177,7 @@ impl Vecs {
parent: &Path,
parent_version: Version,
indexes: &indexes::Vecs,
cached_starts: &CachedWindowStarts,
cached_starts: &Windows<&WindowStartVec>,
) -> Result<Self> {
let db_path = parent.join(super::DB_NAME);
let states_path = db_path.join("states");
@@ -146,7 +221,7 @@ impl Vecs {
let empty_addr_count =
AddrCountsVecs::forced_import(&db, "empty_addr_count", version, indexes)?;
let addr_activity =
AddrActivityVecs::forced_import(&db, "addr_activity", version, indexes, cached_starts)?;
AddrActivityVecs::forced_import(&db, version, indexes, cached_starts)?;
// Stored total = addr_count + empty_addr_count (global + per-type, with all derived indexes)
let total_addr_count = TotalAddrCountVecs::forced_import(&db, version, indexes)?;
@@ -154,9 +229,24 @@ 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_*` 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, "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)?;
// Growth rate: delta change + rate (global + per-type)
let delta = DeltaVecs::new(version, &addr_count, cached_starts, indexes);
// Average amount (supply / utxo_count, supply / funded_addr_count) for `all` and per addr type.
let avg_amount = WithAddrTypes::<AvgAmountMetrics>::forced_import(&db, version, indexes)?;
let this = Self {
supply_state: BytesVec::forced_import_with(
vecdb::ImportOptions::new(&db, "supply_state", version)
@@ -169,7 +259,11 @@ impl Vecs {
activity: addr_activity,
total: total_addr_count,
new: new_addr_count,
reused: reused_addr_count,
respent: respent_addr_count,
exposed: exposed_addr_vecs,
delta,
avg_amount,
funded_index: funded_addr_index,
empty_index: empty_addr_index,
},
@@ -282,9 +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.reset_height()?;
reset_state(
&mut self.any_addr_indexes,
&mut self.addrs_data,
@@ -454,6 +546,65 @@ 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)?;
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,
all_supply_sats,
&type_supply_sats,
exit,
)?;
// Average amount (supply / utxo_count, supply / funded_addr_count) for `all` and per addr type.
let all_m = &self.utxo_cohorts.all.metrics;
self.addrs.avg_amount.all.compute(
prices,
&all_m.supply.total.sats.height,
&all_m.outputs.unspent_count.height,
&self.addrs.funded.all.height,
starting_indexes.height,
exit,
)?;
for ((ot, avg), (_, funded)) in self
.addrs
.avg_amount
.by_addr_type
.iter_mut()
.zip(self.addrs.funded.by_addr_type.iter())
{
let type_m = &t.get(ot).metrics;
avg.compute(
prices,
&type_m.supply.total.sats.height,
&type_m.outputs.unspent_count.height,
&funded.height,
starting_indexes.height,
exit,
)?;
}
// 6c. Compute total_addr_count = addr_count + empty_addr_count
self.addrs.total.compute(
@@ -490,6 +641,15 @@ impl Vecs {
exit,
)?;
let all_supply_sats = self
.utxo_cohorts
.all
.metrics
.supply
.total
.sats
.height
.read_only_clone();
let all_utxo_count = self
.utxo_cohorts
.all
@@ -498,8 +658,13 @@ impl Vecs {
.unspent_count
.height
.read_only_clone();
self.addr_cohorts
.compute_rest_part2(prices, starting_indexes, &all_utxo_count, exit)?;
self.addr_cohorts.compute_rest_part2(
prices,
starting_indexes,
&all_supply_sats,
&all_utxo_count,
exit,
)?;
let exit = exit.clone();
self.db.run_bg(move |db| {
@@ -521,9 +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.min_stateful_len()))
.min(Height::from(self.coinblocks_destroyed.block.len()))
}
}

Some files were not shown because too many files have changed in this diff Show More