- {series.cells.map((cell, column) => {
- const lit = !hover || hover.cell.month === cell?.month;
- return (
-
cell && onHover({ pool: series.pool, cell })}
- style={{
- background: cell
- ? shade(cell.rate, maxRate)
- : "var(--color-ink-900)",
- boxShadow: cell
- ? undefined
- : "inset 0 0 0 1px var(--color-ink-800)",
- opacity: lit ? 1 : 0.28,
- animationDelay: `${column * 6 + row * 14}ms`,
- }}
- />
- );
- })}
-
-
-
- {formatRate(series.rate)}
-
- >
- );
-}
-
-function Readout({
- hover,
- network,
- span,
-}: {
- hover: Hover | null;
- network: Series;
- span: string;
-}) {
- const pool = hover ? hover.pool : "all pools";
- const when = hover ? formatMonth(hover.cell.month) : span;
- const won = hover ? hover.cell.blocksWon : network.blocksWon;
- const flagged = hover ? hover.cell.flaggedBlocks : network.flaggedBlocks;
- const rate = hover ? hover.cell.rate : network.rate;
-
- return (
-
-
-
- {pool} · {when}
-
-
- {formatRate(rate)}
-
-
-
-
{formatBlocks(flagged)} flagged
-
{formatBlocks(won)} won
-
-
- );
-}
-
-function Legend({ maxRate }: { maxRate: number }) {
- const steps = [0, 0.25, 0.5, 0.75, 1];
-
- return (
-
-
-
0%
-
- {steps.map((step) => (
-
- ))}
-
-
{formatRate(maxRate)}
-
-
one cell = one pool-month · hollow = no blocks won
-
- );
-}
diff --git a/src/posts/bitcoin/index.mdx b/src/posts/bitcoin/index.mdx
index 12f3750..70a90b5 100644
--- a/src/posts/bitcoin/index.mdx
+++ b/src/posts/bitcoin/index.mdx
@@ -11,9 +11,6 @@ stageTone: "dark"
import AcceleratorPools from "./diagrams/AcceleratorPools.astro";
import BitcoinTimeline from "./diagrams/BitcoinTimeline.astro";
-import FlagRateMempool from "./diagrams/FlagRateMempool.astro";
-import FlaggedBlocks from "./diagrams/FlaggedBlocks.astro";
-import PoolFlagHeatmap from "./diagrams/PoolFlagHeatmap.astro";
import { OffchainRevenue } from "./diagrams/OffchainRevenue";
@@ -52,16 +49,6 @@ There are more offchain revenues, private business deals, un published data, so
[same charts, but with addition of assumed offchain revenue]
-## Flag rate vs mempool congestion
-
-
-
-
-
-
-
-The numbers here are placeholder data.
-
## What a pool earns off the book
diff --git a/src/posts/bitcoin/lib/flag-rate.ts b/src/posts/bitcoin/lib/flag-rate.ts
deleted file mode 100644
index d7a507b..0000000
--- a/src/posts/bitcoin/lib/flag-rate.ts
+++ /dev/null
@@ -1,277 +0,0 @@
-/**
- * PSEUDO DATA — every number below is synthesised, not measured. Swap
- * `buildDays` for a real loader (mempool.space fee history + whatever marks a
- * block as carrying privately-relayed transactions) and the widget is
- * unchanged. The shape is what a real loader has to produce.
- */
-
-export interface DayBucket {
- /** ISO date, UTC. */
- date: string;
- /** Blocks mined that day. */
- blocks: number;
- /** Blocks that carried at least one flagged (privately included) tx. */
- flagged: number;
- /** Median public fee rate, sat/vB. */
- feeRate: number;
-}
-
-export const FLAG_START = "2023-01-01";
-const DAYS_COUNT = 731;
-
-// Deterministic: the island renders on the server and again in the browser,
-// and a Math.random() series would differ between the two.
-function mulberry32(seed: number) {
- let a = seed >>> 0;
- return () => {
- a = (a + 0x6d2b79f5) >>> 0;
- let t = Math.imul(a ^ (a >>> 15), 1 | a);
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
- };
-}
-
-interface Episode {
- /** Day index the congestion starts. */
- at: number;
- /** Days it lasts. */
- len: number;
- /** Peak median fee rate, sat/vB. */
- peak: number;
-}
-
-const EPISODES: Episode[] = [
- { at: 118, len: 22, peak: 78 },
- { at: 131, len: 16, peak: 168 },
- { at: 199, len: 12, peak: 62 },
- { at: 320, len: 34, peak: 246 },
- { at: 412, len: 14, peak: 71 },
- { at: 468, len: 20, peak: 132 },
- { at: 475, len: 9, peak: 301 },
- { at: 560, len: 18, peak: 96 },
- { at: 651, len: 26, peak: 154 },
-];
-
-/** How many days private inclusion trails the public spike. */
-export const RESPONSE_LAG = 2;
-
-function episodeLift(index: number): number {
- let lift = 0;
- for (const episode of EPISODES) {
- const t = (index - episode.at) / episode.len;
- if (t < 0 || t > 1) continue;
- // Fast in, slow out — congestion arrives quicker than it drains.
- const shape = t < 0.28 ? t / 0.28 : Math.pow(1 - (t - 0.28) / 0.72, 1.7);
- lift += episode.peak * shape;
- }
- return lift;
-}
-
-function buildDays(): DayBucket[] {
- const random = mulberry32(0x5eed_1337);
- const start = Date.UTC(2023, 0, 1);
-
- const feeRates: number[] = [];
- for (let i = 0; i < DAYS_COUNT; i += 1) {
- const base = 9 + 3.5 * Math.sin(i / 47) + 2 * Math.sin(i / 11);
- const noise = (random() - 0.45) * 6;
- feeRates.push(Math.max(1.4, base + noise + episodeLift(i)));
- }
-
- return feeRates.map((feeRate, i) => {
- // Private inclusion answers the *earlier* public spike, not today's — the
- // whole point of the chart is that the two curves are offset, not glued.
- const driver = feeRates[Math.max(0, i - RESPONSE_LAG)];
- const pressure = Math.min(1, Math.log10(1 + driver / 9) / 1.35);
- const share = Math.min(
- 0.62,
- Math.max(0.01, 0.055 + 0.44 * pressure + (random() - 0.5) * 0.045),
- );
- const blocks = 132 + Math.round(random() * 22);
- return {
- date: new Date(start + i * 86_400_000).toISOString().slice(0, 10),
- blocks,
- flagged: Math.round(blocks * share),
- feeRate: Math.round(feeRate * 10) / 10,
- };
- });
-}
-
-export const DAYS: DayBucket[] = buildDays();
-
-export function flagShare(day: DayBucket): number {
- return day.flagged / day.blocks;
-}
-
-/**
- * One day's blocks, flagged or not. Runs rather than a coin flip per block:
- * a pool that just won a private-order-flow deal wins several blocks in a
- * row, so the real ribbon clusters.
- */
-export function blocksOfDay(index: number): boolean[] {
- const day = DAYS[index];
- const random = mulberry32(0xb10c + index * 2654435761);
- const out: boolean[] = [];
- let remaining = day.flagged;
- while (out.length < day.blocks) {
- const left = day.blocks - out.length;
- const gapRoom = left - remaining;
- if (remaining <= 0) {
- out.push(false);
- continue;
- }
- if (gapRoom <= 0) {
- out.push(true);
- remaining -= 1;
- continue;
- }
- const gap = 1 + Math.floor(random() * Math.min(gapRoom, 6));
- for (let i = 0; i < gap; i += 1) out.push(false);
- const run = 1 + Math.floor(random() * Math.min(remaining, 4));
- for (let i = 0; i < run; i += 1) out.push(true);
- remaining -= run;
- }
- return out.slice(0, day.blocks);
-}
-
-export function percentile(values: number[], p: number): number {
- const sorted = [...values].sort((a, b) => a - b);
- const at = (sorted.length - 1) * p;
- const low = Math.floor(at);
- const high = Math.ceil(at);
- return sorted[low] + (sorted[high] - sorted[low]) * (at - low);
-}
-
-export function pearson(a: number[], b: number[], lag = 0): number {
- const x = lag > 0 ? a.slice(0, a.length - lag) : a;
- const y = lag > 0 ? b.slice(lag) : b;
- const n = Math.min(x.length, y.length);
- const mx = x.reduce((s, v) => s + v, 0) / n;
- const my = y.reduce((s, v) => s + v, 0) / n;
- let num = 0;
- let dx = 0;
- let dy = 0;
- for (let i = 0; i < n; i += 1) {
- const a0 = x[i] - mx;
- const b0 = y[i] - my;
- num += a0 * b0;
- dx += a0 * a0;
- dy += b0 * b0;
- }
- return num / Math.sqrt(dx * dy);
-}
-
-/** The lag, in days, at which the two series line up best. */
-export function bestLag(a: number[], b: number[], maxLag = 10): number {
- let best = 0;
- let bestR = -Infinity;
- for (let lag = 0; lag <= maxLag; lag += 1) {
- const r = pearson(a, b, lag);
- if (r > bestR) {
- bestR = r;
- best = lag;
- }
- }
- return best;
-}
-
-export interface Frame {
- width: number;
- /** Where the two mirrored bands meet. */
- axisY: number;
- /** Tallest either band gets. */
- extent: number;
-}
-
-export const FRAME: Frame = { width: 720, axisY: 108, extent: 96 };
-
-export function xAt(index: number, count = DAYS.length): number {
- return (index / (count - 1)) * FRAME.width;
-}
-
-export type Scale = "linear" | "log";
-
-/**
- * Fee rate spans 1.4 → 350 sat/vB, so a linear band pins two years of
- * ordinary days flat against the axis and shows only the spikes. Log keeps
- * the quiet weeks readable — and the band is labelled "log", because a log
- * band's shape is not the growth's shape.
- */
-function norm(value: number, max: number, scale: Scale): number {
- const clamped = Math.min(max, Math.max(0, value));
- if (scale === "linear") return clamped / max;
- return Math.log10(1 + clamped) / Math.log10(1 + max);
-}
-
-/**
- * A mirrored band: `up` draws above the axis, otherwise below. Each band is
- * normalised against its own max — sat/vB and a percentage have nothing to
- * say to each other on one scale.
- */
-export function bandLine(
- values: number[],
- max: number,
- up: boolean,
- scale: Scale = "linear",
-): string {
- return values
- .map(
- (v, i) =>
- `${i === 0 ? "M" : "L"}${xAt(i).toFixed(2)} ${bandY(v, max, up, scale).toFixed(2)}`,
- )
- .join(" ");
-}
-
-/** The same line closed down to the axis, for the fill only — stroking this
- would paint the baseline and the left edge as if they were data. */
-export function bandPath(
- values: number[],
- max: number,
- up: boolean,
- scale: Scale = "linear",
-): string {
- return `${bandLine(values, max, up, scale)} L${FRAME.width} ${FRAME.axisY} L0 ${FRAME.axisY} Z`;
-}
-
-export function bandY(
- value: number,
- max: number,
- up: boolean,
- scale: Scale = "linear",
-): number {
- const { axisY, extent } = FRAME;
- const h = norm(value, max, scale) * extent;
- return up ? axisY - h : axisY + h;
-}
-
-/** First day of each month that starts a quarter, for the axis ticks. */
-export function quarterTicks(): { index: number; label: string }[] {
- const out: { index: number; label: string }[] = [];
- DAYS.forEach((day, index) => {
- const [year, month, dayOfMonth] = day.date.split("-");
- if (dayOfMonth !== "01") return;
- if (!["01", "04", "07", "10"].includes(month)) return;
- const quarter = Math.floor(Number(month) / 3) + 1;
- out.push({ index, label: `${year.slice(2)}·Q${quarter}` });
- });
- return out;
-}
-
-export function formatDate(iso: string): string {
- const [year, month, day] = iso.split("-");
- const names = [
- "jan",
- "feb",
- "mar",
- "apr",
- "may",
- "jun",
- "jul",
- "aug",
- "sep",
- "oct",
- "nov",
- "dec",
- ];
- return `${day} ${names[Number(month) - 1]} ${year}`;
-}
diff --git a/src/posts/bitcoin/lib/flagged-blocks.test.ts b/src/posts/bitcoin/lib/flagged-blocks.test.ts
deleted file mode 100644
index dde3273..0000000
--- a/src/posts/bitcoin/lib/flagged-blocks.test.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import assert from "node:assert/strict";
-import { test } from "node:test";
-import {
- axisMax,
- axisTicks,
- flaggedBlocks,
- FLAGGED_MONTHS,
- formatMonth,
- monthShare,
- POOLS,
- segments,
- yearTicks,
- type FlaggedMonth,
-} from "./flagged-blocks.ts";
-
-const row: FlaggedMonth = {
- month: "2023-01",
- blocks: 1000,
- flagged: { foundry: 100, antpool: 50, viabtc: 30, f2pool: 20, other: 10 },
-};
-
-test("monthShare counts every pool once against the month's blocks", () => {
- assert.equal(flaggedBlocks(row), 210);
- assert.equal(monthShare(row), 21);
-});
-
-test("segments stack without gaps and end at the month's share", () => {
- const stacked = segments(row);
- assert.equal(stacked.length, POOLS.length);
- assert.equal(stacked[0].base, 0);
- stacked.forEach((segment, index) => {
- if (index === 0) return;
- const previous = stacked[index - 1];
- assert.equal(segment.base, previous.base + previous.height);
- });
- const last = stacked[stacked.length - 1];
- assert.equal(last.base + last.height, monthShare(row));
-});
-
-test("a month with no blocks reads as zero rather than NaN", () => {
- const empty: FlaggedMonth = { ...row, blocks: 0 };
- assert.equal(monthShare(empty), 0);
- assert.equal(segments(empty)[0].height, 0);
-});
-
-test("the axis clears the tallest month", () => {
- const max = axisMax(FLAGGED_MONTHS);
- const tallest = Math.max(...FLAGGED_MONTHS.map(monthShare));
- assert.ok(max >= tallest);
- assert.equal(max % 10, 0);
- assert.equal(axisTicks(max)[0], 0);
-});
-
-test("one x label per year, the first one on the opening month", () => {
- const years = yearTicks(FLAGGED_MONTHS);
- assert.equal(years[0].index, 0);
- assert.deepEqual(
- years.map((tick) => tick.year),
- ["2023", "2024", "2025", "2026"],
- );
-});
-
-test("formatMonth reads as a month, not an ISO string", () => {
- assert.equal(formatMonth("2023-01"), "jan 2023");
- assert.equal(formatMonth("2026-07"), "jul 2026");
-});
diff --git a/src/posts/bitcoin/lib/flagged-blocks.ts b/src/posts/bitcoin/lib/flagged-blocks.ts
deleted file mode 100644
index 08e50f0..0000000
--- a/src/posts/bitcoin/lib/flagged-blocks.ts
+++ /dev/null
@@ -1,541 +0,0 @@
-/**
- * PLACEHOLDER DATA — every number in FLAGGED_MONTHS below is generated, not
- * measured. Replace the array with real per-month counts; nothing else in this
- * file or in FlaggedBlocks.tsx needs to change, as long as the shape holds:
- * one row per month, `blocks` = every block mined that month, `flagged` =
- * blocks holding at least one flagged tx, split by the pool that mined them.
- * A block is counted once, under one pool, however many flagged txs it holds.
- */
-
-export type PoolId = "foundry" | "antpool" | "viabtc" | "f2pool" | "other";
-
-export interface FlaggedMonth {
- /** YYYY-MM */
- month: string;
- blocks: number;
- flagged: Record
;
-}
-
-export interface Pool {
- id: PoolId;
- label: string;
- /** Slot in the validated categorical palette — see global.css. */
- color: string;
-}
-
-/** Stack order, floor first. Fixed: a pool keeps its color as the mix moves. */
-export const POOLS: Pool[] = [
- { id: "foundry", label: "Foundry USA", color: "var(--pool-1)" },
- { id: "antpool", label: "AntPool", color: "var(--pool-2)" },
- { id: "viabtc", label: "ViaBTC", color: "var(--pool-3)" },
- { id: "f2pool", label: "F2Pool", color: "var(--pool-4)" },
- { id: "other", label: "Other pools", color: "var(--pool-5)" },
-];
-
-export const FLAGGED_MONTHS: FlaggedMonth[] = [
- {
- month: "2023-01",
- blocks: 4428,
- flagged: { foundry: 114, antpool: 94, viabtc: 57, f2pool: 59, other: 74 },
- },
- {
- month: "2023-02",
- blocks: 4132,
- flagged: { foundry: 125, antpool: 106, viabtc: 50, f2pool: 64, other: 68 },
- },
- {
- month: "2023-03",
- blocks: 4446,
- flagged: { foundry: 155, antpool: 126, viabtc: 74, f2pool: 71, other: 91 },
- },
- {
- month: "2023-04",
- blocks: 4419,
- flagged: { foundry: 160, antpool: 137, viabtc: 85, f2pool: 89, other: 108 },
- },
- {
- month: "2023-05",
- blocks: 4470,
- flagged: { foundry: 184, antpool: 159, viabtc: 92, f2pool: 93, other: 101 },
- },
- {
- month: "2023-06",
- blocks: 4319,
- flagged: { foundry: 167, antpool: 143, viabtc: 82, f2pool: 85, other: 112 },
- },
- {
- month: "2023-07",
- blocks: 4536,
- flagged: {
- foundry: 197,
- antpool: 166,
- viabtc: 92,
- f2pool: 105,
- other: 119,
- },
- },
- {
- month: "2023-08",
- blocks: 4506,
- flagged: {
- foundry: 210,
- antpool: 156,
- viabtc: 88,
- f2pool: 108,
- other: 104,
- },
- },
- {
- month: "2023-09",
- blocks: 4394,
- flagged: {
- foundry: 189,
- antpool: 161,
- viabtc: 98,
- f2pool: 101,
- other: 105,
- },
- },
- {
- month: "2023-10",
- blocks: 4474,
- flagged: {
- foundry: 222,
- antpool: 154,
- viabtc: 91,
- f2pool: 110,
- other: 169,
- },
- },
- {
- month: "2023-11",
- blocks: 4293,
- flagged: { foundry: 214, antpool: 167, viabtc: 99, f2pool: 84, other: 111 },
- },
- {
- month: "2023-12",
- blocks: 4494,
- flagged: {
- foundry: 235,
- antpool: 197,
- viabtc: 111,
- f2pool: 102,
- other: 161,
- },
- },
- {
- month: "2024-01",
- blocks: 4355,
- flagged: {
- foundry: 282,
- antpool: 189,
- viabtc: 122,
- f2pool: 105,
- other: 140,
- },
- },
- {
- month: "2024-02",
- blocks: 4160,
- flagged: {
- foundry: 243,
- antpool: 174,
- viabtc: 105,
- f2pool: 115,
- other: 179,
- },
- },
- {
- month: "2024-03",
- blocks: 4518,
- flagged: {
- foundry: 316,
- antpool: 222,
- viabtc: 138,
- f2pool: 139,
- other: 142,
- },
- },
- {
- month: "2024-04",
- blocks: 4196,
- flagged: {
- foundry: 311,
- antpool: 189,
- viabtc: 116,
- f2pool: 129,
- other: 152,
- },
- },
- {
- month: "2024-05",
- blocks: 4389,
- flagged: {
- foundry: 331,
- antpool: 221,
- viabtc: 153,
- f2pool: 116,
- other: 168,
- },
- },
- {
- month: "2024-06",
- blocks: 4376,
- flagged: {
- foundry: 335,
- antpool: 192,
- viabtc: 134,
- f2pool: 126,
- other: 159,
- },
- },
- {
- month: "2024-07",
- blocks: 4564,
- flagged: {
- foundry: 331,
- antpool: 224,
- viabtc: 141,
- f2pool: 128,
- other: 129,
- },
- },
- {
- month: "2024-08",
- blocks: 4554,
- flagged: {
- foundry: 350,
- antpool: 226,
- viabtc: 139,
- f2pool: 116,
- other: 230,
- },
- },
- {
- month: "2024-09",
- blocks: 4393,
- flagged: {
- foundry: 350,
- antpool: 216,
- viabtc: 162,
- f2pool: 116,
- other: 193,
- },
- },
- {
- month: "2024-10",
- blocks: 4483,
- flagged: {
- foundry: 337,
- antpool: 227,
- viabtc: 141,
- f2pool: 140,
- other: 215,
- },
- },
- {
- month: "2024-11",
- blocks: 4402,
- flagged: {
- foundry: 329,
- antpool: 221,
- viabtc: 136,
- f2pool: 122,
- other: 215,
- },
- },
- {
- month: "2024-12",
- blocks: 4558,
- flagged: {
- foundry: 355,
- antpool: 241,
- viabtc: 153,
- f2pool: 122,
- other: 207,
- },
- },
- {
- month: "2025-01",
- blocks: 4480,
- flagged: {
- foundry: 411,
- antpool: 230,
- viabtc: 161,
- f2pool: 121,
- other: 220,
- },
- },
- {
- month: "2025-02",
- blocks: 3957,
- flagged: {
- foundry: 358,
- antpool: 220,
- viabtc: 151,
- f2pool: 120,
- other: 226,
- },
- },
- {
- month: "2025-03",
- blocks: 4434,
- flagged: {
- foundry: 395,
- antpool: 234,
- viabtc: 143,
- f2pool: 127,
- other: 164,
- },
- },
- {
- month: "2025-04",
- blocks: 4377,
- flagged: {
- foundry: 388,
- antpool: 211,
- viabtc: 158,
- f2pool: 128,
- other: 222,
- },
- },
- {
- month: "2025-05",
- blocks: 4369,
- flagged: {
- foundry: 460,
- antpool: 241,
- viabtc: 186,
- f2pool: 142,
- other: 211,
- },
- },
- {
- month: "2025-06",
- blocks: 4429,
- flagged: {
- foundry: 476,
- antpool: 285,
- viabtc: 203,
- f2pool: 140,
- other: 185,
- },
- },
- {
- month: "2025-07",
- blocks: 4579,
- flagged: {
- foundry: 415,
- antpool: 250,
- viabtc: 194,
- f2pool: 133,
- other: 185,
- },
- },
- {
- month: "2025-08",
- blocks: 4534,
- flagged: {
- foundry: 439,
- antpool: 239,
- viabtc: 207,
- f2pool: 121,
- other: 230,
- },
- },
- {
- month: "2025-09",
- blocks: 4421,
- flagged: {
- foundry: 521,
- antpool: 244,
- viabtc: 208,
- f2pool: 139,
- other: 227,
- },
- },
- {
- month: "2025-10",
- blocks: 4394,
- flagged: {
- foundry: 489,
- antpool: 240,
- viabtc: 198,
- f2pool: 131,
- other: 189,
- },
- },
- {
- month: "2025-11",
- blocks: 4228,
- flagged: {
- foundry: 443,
- antpool: 254,
- viabtc: 217,
- f2pool: 117,
- other: 260,
- },
- },
- {
- month: "2025-12",
- blocks: 4484,
- flagged: {
- foundry: 524,
- antpool: 242,
- viabtc: 200,
- f2pool: 124,
- other: 224,
- },
- },
- {
- month: "2026-01",
- blocks: 4374,
- flagged: {
- foundry: 502,
- antpool: 270,
- viabtc: 187,
- f2pool: 112,
- other: 188,
- },
- },
- {
- month: "2026-02",
- blocks: 4036,
- flagged: {
- foundry: 473,
- antpool: 248,
- viabtc: 203,
- f2pool: 134,
- other: 294,
- },
- },
- {
- month: "2026-03",
- blocks: 4397,
- flagged: {
- foundry: 465,
- antpool: 238,
- viabtc: 219,
- f2pool: 119,
- other: 258,
- },
- },
- {
- month: "2026-04",
- blocks: 4317,
- flagged: {
- foundry: 484,
- antpool: 249,
- viabtc: 186,
- f2pool: 111,
- other: 276,
- },
- },
- {
- month: "2026-05",
- blocks: 4582,
- flagged: {
- foundry: 612,
- antpool: 307,
- viabtc: 240,
- f2pool: 138,
- other: 330,
- },
- },
- {
- month: "2026-06",
- blocks: 4351,
- flagged: {
- foundry: 602,
- antpool: 279,
- viabtc: 214,
- f2pool: 121,
- other: 231,
- },
- },
- {
- month: "2026-07",
- blocks: 4457,
- flagged: {
- foundry: 520,
- antpool: 260,
- viabtc: 201,
- f2pool: 118,
- other: 303,
- },
- },
-];
-
-export function flaggedBlocks(row: FlaggedMonth): number {
- return POOLS.reduce((sum, pool) => sum + (row.flagged[pool.id] ?? 0), 0);
-}
-
-/** Percent of the month's blocks that held a flagged tx, 0–100. */
-export function monthShare(row: FlaggedMonth): number {
- return row.blocks === 0 ? 0 : (flaggedBlocks(row) / row.blocks) * 100;
-}
-
-/** One pool's contribution to that share, in the same percentage points. */
-export function poolShare(row: FlaggedMonth, pool: PoolId): number {
- return row.blocks === 0 ? 0 : ((row.flagged[pool] ?? 0) / row.blocks) * 100;
-}
-
-/** Cumulative floor and height, in percentage points, for each stack segment. */
-export function segments(
- row: FlaggedMonth,
-): { pool: Pool; base: number; height: number }[] {
- let base = 0;
- return POOLS.map((pool) => {
- const height = poolShare(row, pool.id);
- const segment = { pool, base, height };
- base += height;
- return segment;
- });
-}
-
-/** Axis ceiling: the next 10-point step above the tallest month. */
-export function axisMax(rows: FlaggedMonth[]): number {
- const tallest = rows.reduce((max, row) => Math.max(max, monthShare(row)), 0);
- return Math.max(10, Math.ceil(tallest / 10) * 10);
-}
-
-export function axisTicks(max: number): number[] {
- const step = max > 50 ? 20 : 10;
- const ticks: number[] = [];
- for (let value = 0; value <= max; value += step) ticks.push(value);
- return ticks;
-}
-
-/** Index of every January in the series — the only x labels the axis carries. */
-export function yearTicks(
- rows: FlaggedMonth[],
-): { index: number; year: string }[] {
- return rows.flatMap((row, index) => {
- const [year, month] = row.month.split("-");
- return month === "01" || index === 0 ? [{ index, year }] : [];
- });
-}
-
-const MONTH_NAMES = [
- "jan",
- "feb",
- "mar",
- "apr",
- "may",
- "jun",
- "jul",
- "aug",
- "sep",
- "oct",
- "nov",
- "dec",
-];
-
-export function formatMonth(month: string): string {
- const [year, index] = month.split("-");
- return `${MONTH_NAMES[Number(index) - 1]} ${year}`;
-}
-
-export function formatPercent(value: number, digits = 1): string {
- return `${value.toFixed(digits)}%`;
-}
diff --git a/src/posts/bitcoin/lib/pool-flag-rate.ts b/src/posts/bitcoin/lib/pool-flag-rate.ts
deleted file mode 100644
index 62c5a31..0000000
--- a/src/posts/bitcoin/lib/pool-flag-rate.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-export interface FlagRow {
- pool: string;
- month: string;
- blocksWon: number;
- flaggedBlocks: number;
-}
-
-export interface Cell {
- month: string;
- blocksWon: number;
- flaggedBlocks: number;
- rate: number;
-}
-
-export interface Series {
- pool: string;
- cells: (Cell | null)[];
- blocksWon: number;
- flaggedBlocks: number;
- rate: number;
-}
-
-export interface YearSpan {
- year: string;
- start: number;
- span: number;
-}
-
-export interface Heatmap {
- months: string[];
- years: YearSpan[];
- pools: Series[];
- network: Series;
- maxRate: number;
-}
-
-const MONTH_NAMES = [
- "jan",
- "feb",
- "mar",
- "apr",
- "may",
- "jun",
- "jul",
- "aug",
- "sep",
- "oct",
- "nov",
- "dec",
-];
-
-function toCell(month: string, won: number, flagged: number): Cell {
- return {
- month,
- blocksWon: won,
- flaggedBlocks: flagged,
- rate: won === 0 ? 0 : flagged / won,
- };
-}
-
-function summarize(pool: string, cells: (Cell | null)[]): Series {
- let blocksWon = 0;
- let flaggedBlocks = 0;
- for (const cell of cells) {
- if (!cell) continue;
- blocksWon += cell.blocksWon;
- flaggedBlocks += cell.flaggedBlocks;
- }
- return {
- pool,
- cells,
- blocksWon,
- flaggedBlocks,
- rate: blocksWon === 0 ? 0 : flaggedBlocks / blocksWon,
- };
-}
-
-export function buildHeatmap(rows: FlagRow[]): Heatmap {
- const months = [...new Set(rows.map((row) => row.month))].sort();
- const index = new Map(months.map((month, i) => [month, i]));
-
- const byPool = new Map();
- const networkCells: (Cell | null)[] = months.map(() => null);
- const networkWon = months.map(() => 0);
- const networkFlagged = months.map(() => 0);
-
- for (const row of rows) {
- const column = index.get(row.month);
- if (column === undefined) continue;
- let cells = byPool.get(row.pool);
- if (!cells) {
- cells = months.map(() => null);
- byPool.set(row.pool, cells);
- }
- cells[column] = toCell(row.month, row.blocksWon, row.flaggedBlocks);
- networkWon[column] += row.blocksWon;
- networkFlagged[column] += row.flaggedBlocks;
- }
-
- for (const [column, month] of months.entries()) {
- if (networkWon[column] === 0) continue;
- networkCells[column] = toCell(
- month,
- networkWon[column],
- networkFlagged[column],
- );
- }
-
- const pools = [...byPool.entries()]
- .map(([pool, cells]) => summarize(pool, cells))
- .sort((a, b) => b.blocksWon - a.blocksWon);
-
- const years: YearSpan[] = [];
- for (const [column, month] of months.entries()) {
- const year = month.slice(0, 4);
- const last = years[years.length - 1];
- if (last && last.year === year) last.span += 1;
- else years.push({ year, start: column, span: 1 });
- }
-
- let maxRate = 0;
- for (const series of pools) {
- for (const cell of series.cells) {
- if (cell && cell.rate > maxRate) maxRate = cell.rate;
- }
- }
-
- return {
- months,
- years,
- pools,
- network: summarize("network", networkCells),
- maxRate: maxRate || 1,
- };
-}
-
-/** Cell fill: one hue, surface → signal, gamma-eased so the low end stays legible. */
-export function shade(rate: number, maxRate: number): string {
- const t = Math.min(1, Math.max(0, rate / maxRate)) ** 0.78;
- const mix = (5 + 90 * t).toFixed(1);
- return `color-mix(in oklab, var(--color-signal-500) ${mix}%, var(--color-ink-800))`;
-}
-
-export function formatMonth(month: string): string {
- const [year, index] = month.split("-");
- return `${MONTH_NAMES[Number(index) - 1]} ${year}`;
-}
-
-export function formatRate(rate: number): string {
- return `${(rate * 100).toFixed(1)}%`;
-}
-
-export function formatBlocks(count: number): string {
- return count.toLocaleString("en-US");
-}
diff --git a/src/posts/bitcoin/lib/pool-flag-sample.ts b/src/posts/bitcoin/lib/pool-flag-sample.ts
deleted file mode 100644
index bbc99aa..0000000
--- a/src/posts/bitcoin/lib/pool-flag-sample.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import type { FlagRow } from "./pool-flag-rate";
-
-/* PSEUDO DATA — not measured, generated. Replace this whole file's default
- export with the real rows; nothing else reads the generator. */
-
-interface PoolSpec {
- name: string;
- share: number;
- base: number;
- drift: number;
- from?: string;
- until?: string;
-}
-
-const POOL_SPECS: PoolSpec[] = [
- { name: "foundry usa", share: 0.29, base: 0.16, drift: 0.13 },
- { name: "antpool", share: 0.21, base: 0.05, drift: 0.03 },
- { name: "viabtc", share: 0.11, base: 0.04, drift: 0.02 },
- { name: "f2pool", share: 0.1, base: 0.14, drift: -0.11 },
- { name: "binance pool", share: 0.07, base: 0.03, drift: 0.01 },
- { name: "mara pool", share: 0.05, base: 0.21, drift: 0.14 },
- { name: "luxor", share: 0.04, base: 0.15, drift: 0.07 },
- { name: "spiderpool", share: 0.05, base: 0.02, drift: 0.01, from: "2023-10" },
- { name: "braiins", share: 0.03, base: 0.01, drift: 0.0 },
- {
- name: "sbi crypto",
- share: 0.02,
- base: 0.09,
- drift: 0.02,
- until: "2025-08",
- },
- { name: "other", share: 0.06, base: 0.03, drift: 0.02 },
-];
-
-const FIRST_MONTH = "2023-01";
-const LAST_MONTH = "2026-06";
-const BLOCKS_PER_MONTH = 4380;
-
-function mulberry32(seed: number) {
- let a = seed;
- return () => {
- a |= 0;
- a = (a + 0x6d2b79f5) | 0;
- let t = Math.imul(a ^ (a >>> 15), 1 | a);
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
- };
-}
-
-function monthRange(first: string, last: string): string[] {
- const months: string[] = [];
- let [year, month] = first.split("-").map(Number);
- const [lastYear, lastMonth] = last.split("-").map(Number);
- while (year < lastYear || (year === lastYear && month <= lastMonth)) {
- months.push(`${year}-${String(month).padStart(2, "0")}`);
- month += 1;
- if (month > 12) {
- month = 1;
- year += 1;
- }
- }
- return months;
-}
-
-function generate(): FlagRow[] {
- const random = mulberry32(0x9c1f37);
- const months = monthRange(FIRST_MONTH, LAST_MONTH);
- const rows: FlagRow[] = [];
-
- for (const spec of POOL_SPECS) {
- for (const [index, month] of months.entries()) {
- if (spec.from && month < spec.from) continue;
- if (spec.until && month > spec.until) continue;
-
- const t = index / (months.length - 1);
- const share = spec.share * (0.78 + random() * 0.44);
- const blocksWon = Math.round(BLOCKS_PER_MONTH * share);
- if (blocksWon === 0) continue;
-
- // An S-curve, not a straight line: policy changes land over a quarter or
- // two rather than one month.
- const ramp = 1 / (1 + Math.exp(-(t - 0.45) * 7));
- const rate = Math.min(
- 0.94,
- Math.max(0, spec.base + spec.drift * ramp + (random() - 0.5) * 0.045),
- );
- rows.push({
- pool: spec.name,
- month,
- blocksWon,
- flaggedBlocks: Math.round(blocksWon * rate),
- });
- }
- }
-
- return rows;
-}
-
-export const FLAG_ROWS: FlagRow[] = generate();