blockyard 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +929 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +191 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +41 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1577 -0
- package/docs/ARCHITECTURE.md +1394 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +847 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +205 -0
- package/docs/INSTALL.md +547 -0
- package/docs/MEASUREMENTS.md +1401 -0
- package/docs/RULES.md +681 -0
- package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
- package/docs/SECURITY-AUDIT.md +258 -0
- package/docs/SECURITY.md +212 -0
- package/docs/TROUBLESHOOTING.md +332 -0
- package/docs/USER-GUIDE.md +1262 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +2009 -0
- package/public/donate-qr.png +0 -0
- package/public/index.html +1085 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +1141 -0
- package/public/js/app.js +1386 -0
- package/public/js/arkanoid.js +806 -0
- package/public/js/blockanoid.js +347 -0
- package/public/js/blockout.js +347 -0
- package/public/js/blockpack.js +428 -0
- package/public/js/blockscene3d.js +2830 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +315 -0
- package/public/js/details3d.js +4342 -0
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/explorer.js +405 -0
- package/public/js/feepalette.js +149 -0
- package/public/js/fmt.js +162 -0
- package/public/js/goggles.js +886 -0
- package/public/js/kiosk.js +41 -0
- package/public/js/login.js +88 -0
- package/public/js/markets.js +395 -0
- package/public/js/mining.js +1416 -0
- package/public/js/panels.js +970 -0
- package/public/js/pricechart.js +189 -0
- package/public/js/quake.js +20 -0
- package/public/js/settings.js +1096 -0
- package/public/js/soundcard.js +459 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -0
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/public/login.html +33 -0
- package/scripts/blockfile-measure.js +156 -0
- package/scripts/browser-check.mjs +286 -0
- package/scripts/check.js +173 -0
- package/scripts/decode-check.js +81 -0
- package/scripts/doc-counts.js +109 -0
- package/scripts/donate-qr.py +23 -0
- package/scripts/dos-bench.js +56 -0
- package/scripts/fake-node.js +534 -0
- package/scripts/index-bench.js +216 -0
- package/scripts/index-benchmark.js +117 -0
- package/scripts/index-build.js +40 -0
- package/scripts/live-render-check.mjs +89 -0
- package/scripts/manage-users.js +132 -0
- package/scripts/motion-check.mjs +138 -0
- package/scripts/pool-map.js +157 -0
- package/scripts/setup.js +432 -0
- package/scripts/shots.mjs +278 -0
- package/scripts/smoke.sh +327 -0
- package/scripts/tls.js +31 -0
- package/scripts/ui.js +174 -0
- package/server/auth/sessions.js +221 -0
- package/server/auth/users.js +243 -0
- package/server/chain/blockfile.js +234 -0
- package/server/chain/index/build.js +210 -0
- package/server/chain/index/heights.js +36 -0
- package/server/chain/index/live.js +276 -0
- package/server/chain/index/rows.js +145 -0
- package/server/chain/index/store.js +154 -0
- package/server/chain/index/worker.js +109 -0
- package/server/chain/tx.js +310 -0
- package/server/collect/gbt.js +229 -0
- package/server/collect/logparse.js +765 -0
- package/server/collect/logtail.js +189 -0
- package/server/collect/markets.js +333 -0
- package/server/collect/mining.js +333 -0
- package/server/collect/monitor.js +2545 -0
- package/server/collect/network.js +295 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +644 -0
- package/server/http/api.js +1319 -0
- package/server/http/explorer.js +418 -0
- package/server/http/games.js +77 -0
- package/server/http/server.js +420 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +673 -0
- package/server/netinfo.js +253 -0
- package/server/rpc/allowlist.js +130 -0
- package/server/rpc/client.js +414 -0
- package/server/store/audit.js +148 -0
- package/server/store/history.js +220 -0
- package/server/store/ledger.js +290 -0
- package/server/store/ring.js +173 -0
- package/server/tls/selfsigned.js +160 -0
- package/server/util/fmt.js +29 -0
- package/systemd/blockyard.service +102 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// THE NETWORK OVER A WEEK AND A YEAR, from this node alone (operator, 2026-09-15, with a
|
|
2
|
+
// mempool.space screenshot of its mining dashboard: "Why don't we have this view in our mining
|
|
3
|
+
// tab?" -- "Do it", then the adjustments table: "add this too"). Nothing here is fetched from
|
|
4
|
+
// anyone but the node:
|
|
5
|
+
//
|
|
6
|
+
// rewards the last 144 blocks' subsidy, fees and transaction counts -- getblockstats, one
|
|
7
|
+
// call per block, batched, rolled forward a block at a time
|
|
8
|
+
// epochs the start of each difficulty period back a year: its height, time and difficulty
|
|
9
|
+
// (getblockhash + getblockheader, two in-memory lookups per epoch) -- the
|
|
10
|
+
// adjustment table, the estimate for the current period, the chart's steps
|
|
11
|
+
// samples a block every 144 back a year, with its time and difficulty: the daily hashrate
|
|
12
|
+
// estimate is difficulty * 2^32 / the mean interval across the day
|
|
13
|
+
// pools the coinbase of every block of the last week, attributed the way the ledger
|
|
14
|
+
// does it (decodeCoinbase + the curated pool map) -- a week is ~1000 blocks at
|
|
15
|
+
// two calls each, so it fills in the background at eight blocks every few seconds
|
|
16
|
+
// and the view says how far it has got
|
|
17
|
+
//
|
|
18
|
+
// The arithmetic is in pure functions above the class, which is what the tests hold.
|
|
19
|
+
import { decodeCoinbase, matchPool, aliasFor, tagFingerprint } from './mining.js';
|
|
20
|
+
|
|
21
|
+
export const EPOCH = 2016;
|
|
22
|
+
export const TARGET_SPACING = 600;
|
|
23
|
+
export const HALVING_INTERVAL = 210_000;
|
|
24
|
+
const DAY_BLOCKS = 144;
|
|
25
|
+
|
|
26
|
+
/** Sum a window of getblockstats rows into the reward figures. Satoshis in, satoshis out. */
|
|
27
|
+
export function rewardStats(rows) {
|
|
28
|
+
const r = rows.filter((x) => x && Number.isFinite(x.totalfee) && Number.isFinite(x.subsidy));
|
|
29
|
+
if (!r.length) return { blocks: 0, minersRewardSat: 0, avgBlockFeeSat: null, avgTxFeeSat: null, txs: 0, from: null, to: null };
|
|
30
|
+
const fees = r.reduce((a, x) => a + x.totalfee, 0);
|
|
31
|
+
const subsidy = r.reduce((a, x) => a + x.subsidy, 0);
|
|
32
|
+
const txs = r.reduce((a, x) => a + Math.max(0, (x.txs ?? 1) - 1), 0); // the coinbase pays no fee
|
|
33
|
+
const hs = r.map((x) => x.height);
|
|
34
|
+
return {
|
|
35
|
+
blocks: r.length,
|
|
36
|
+
minersRewardSat: fees + subsidy,
|
|
37
|
+
avgBlockFeeSat: Math.round(fees / r.length),
|
|
38
|
+
avgTxFeeSat: txs ? Math.round(fees / txs) : null,
|
|
39
|
+
txs,
|
|
40
|
+
from: Math.min(...hs), to: Math.max(...hs),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Where the current difficulty period stands and what the next adjustment looks like, from the
|
|
46
|
+
* tip's height and time, the period's first block time, and the two difficulties.
|
|
47
|
+
* The estimate is what Core would compute if the rest of the period kept the pace so far,
|
|
48
|
+
* clamped to the protocol's factor-of-four bounds. Before ten blocks there is no pace to speak
|
|
49
|
+
* of, so the estimate is null.
|
|
50
|
+
*/
|
|
51
|
+
export function difficultyEstimate({ height, tipTime, epochStartTime, difficulty, prevDifficulty = null, now = Date.now() }) {
|
|
52
|
+
if (!Number.isFinite(height) || !Number.isFinite(tipTime) || !Number.isFinite(epochStartTime)) return null;
|
|
53
|
+
const epochStart = height - (height % EPOCH);
|
|
54
|
+
const into = height - epochStart; // blocks mined since the period's first
|
|
55
|
+
const remaining = EPOCH - into;
|
|
56
|
+
const elapsed = Math.max(1, tipTime - epochStartTime); // seconds over `into` intervals
|
|
57
|
+
const pace = into >= 10 ? elapsed / into : null;
|
|
58
|
+
let estimatePct = null;
|
|
59
|
+
if (pace != null) {
|
|
60
|
+
const projected = pace * EPOCH;
|
|
61
|
+
const factor = Math.max(0.25, Math.min(4, (EPOCH * TARGET_SPACING) / projected));
|
|
62
|
+
estimatePct = (factor - 1) * 100;
|
|
63
|
+
}
|
|
64
|
+
const previousPct = Number.isFinite(prevDifficulty) && prevDifficulty > 0 && Number.isFinite(difficulty) ? (difficulty / prevDifficulty - 1) * 100 : null;
|
|
65
|
+
const etaSec = remaining * (pace ?? TARGET_SPACING);
|
|
66
|
+
return { epochStart, into, remaining, elapsedSec: elapsed, paceSec: pace, estimatePct, previousPct, etaSec, at: now + etaSec * 1000 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The next halving: its height, the blocks to go, and when at the target spacing. */
|
|
70
|
+
export function halvingInfo(height, { now = Date.now() } = {}) {
|
|
71
|
+
if (!Number.isFinite(height)) return null;
|
|
72
|
+
const nextHeight = (Math.floor(height / HALVING_INTERVAL) + 1) * HALVING_INTERVAL;
|
|
73
|
+
const blocksLeft = nextHeight - height;
|
|
74
|
+
const etaSec = blocksLeft * TARGET_SPACING;
|
|
75
|
+
return { nextHeight, blocksLeft, etaSec, at: now + etaSec * 1000, era: Math.floor(height / HALVING_INTERVAL) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Hashrate estimates from block samples ({ height, time, difficulty }), any spacing: between two
|
|
80
|
+
* consecutive samples the mean interval is (t1 - t0) / (h1 - h0), and the hashrate that produces
|
|
81
|
+
* one block per that interval at the later sample's difficulty is difficulty * 2^32 / interval.
|
|
82
|
+
* Ascending by height; a sample with a non-positive interval (clock skew) is skipped.
|
|
83
|
+
*/
|
|
84
|
+
export function hashrateSeries(samples) {
|
|
85
|
+
const s = [...samples].filter((x) => x && Number.isFinite(x.height) && Number.isFinite(x.time) && Number.isFinite(x.difficulty)).sort((a, b) => a.height - b.height);
|
|
86
|
+
const out = [];
|
|
87
|
+
for (let i = 1; i < s.length; i++) {
|
|
88
|
+
const dh = s[i].height - s[i - 1].height, dt = s[i].time - s[i - 1].time;
|
|
89
|
+
if (dh <= 0 || dt <= 0) continue;
|
|
90
|
+
const interval = dt / dh;
|
|
91
|
+
out.push({ t: s[i].time * 1000, height: s[i].height, hashrate: s[i].difficulty * 4294967296 / interval, difficulty: s[i].difficulty });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The adjustment table: each epoch start against the one before it. Newest first. */
|
|
97
|
+
export function adjustments(epochs) {
|
|
98
|
+
const e = [...epochs].filter((x) => x && Number.isFinite(x.height) && Number.isFinite(x.difficulty)).sort((a, b) => a.height - b.height);
|
|
99
|
+
const out = [];
|
|
100
|
+
for (let i = 1; i < e.length; i++) {
|
|
101
|
+
out.push({ height: e[i].height, time: e[i].time, difficulty: e[i].difficulty, changePct: e[i - 1].difficulty > 0 ? (e[i].difficulty / e[i - 1].difficulty - 1) * 100 : null });
|
|
102
|
+
}
|
|
103
|
+
return out.reverse();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Pool shares over the window: blocks inside it grouped by pool, share of the window, and the
|
|
108
|
+
* window's luck -- the blocks found against the number the target spacing would give.
|
|
109
|
+
* rows: { height, time, poolKey, name, labelled }.
|
|
110
|
+
*/
|
|
111
|
+
export function poolShares(rows, { now = Date.now(), windowSec = 7 * 86_400 } = {}) {
|
|
112
|
+
const since = now / 1000 - windowSec;
|
|
113
|
+
const inWin = rows.filter((r) => r && Number.isFinite(r.time) && r.time >= since);
|
|
114
|
+
const by = new Map();
|
|
115
|
+
for (const r of inWin) {
|
|
116
|
+
const k = r.poolKey ?? 'unknown';
|
|
117
|
+
const e = by.get(k) ?? { key: k, name: r.name ?? k, labelled: !!r.labelled, blocks: 0 };
|
|
118
|
+
e.blocks += 1;
|
|
119
|
+
by.set(k, e);
|
|
120
|
+
}
|
|
121
|
+
const pools = [...by.values()].sort((a, b) => b.blocks - a.blocks || a.name.localeCompare(b.name));
|
|
122
|
+
for (const p of pools) p.sharePct = inWin.length ? (p.blocks / inWin.length) * 100 : 0;
|
|
123
|
+
// luck against the span actually covered: while the week is still being read, the oldest
|
|
124
|
+
// block read bounds it, so a half-read week is not reported as half the luck
|
|
125
|
+
const oldest = inWin.length ? Math.min(...inWin.map((r) => r.time)) : null;
|
|
126
|
+
const spanSec = oldest == null ? windowSec : Math.min(windowSec, Math.max(TARGET_SPACING, now / 1000 - oldest));
|
|
127
|
+
const expected = spanSec / TARGET_SPACING;
|
|
128
|
+
return { blocks: inWin.length, expected, spanSec, luckPct: inWin.length ? (inWin.length / expected) * 100 : null, pools, count: pools.length, windowSec };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------- the collector
|
|
132
|
+
export class NetworkStats {
|
|
133
|
+
/**
|
|
134
|
+
* @param {object} o
|
|
135
|
+
* @param {{batch: Function}} o.rpc the node's RPC client (batch(calls, opts))
|
|
136
|
+
* @param {Function} [o.log]
|
|
137
|
+
* @param {Function} [o.poolMap] () => the curated coinbase map, or null
|
|
138
|
+
* @param {Function} [o.aliases] () => the operator's alias table, or null
|
|
139
|
+
* @param {Function} [o.now]
|
|
140
|
+
*/
|
|
141
|
+
constructor({ rpc, log = () => {}, poolMap = () => null, aliases = () => null, now = Date.now, windowDays = 7, sampleDays = 366, rewardBlocks = 144 } = {}) {
|
|
142
|
+
this.rpc = rpc; this.log = log; this.poolMapOf = poolMap; this.aliasesOf = aliases; this.now = now;
|
|
143
|
+
this.windowDays = windowDays; this.sampleDays = sampleDays; this.rewardBlocks = rewardBlocks;
|
|
144
|
+
this.rewards = new Map(); // height -> { height, time, totalfee, subsidy, txs }
|
|
145
|
+
this.epochs = new Map(); // epoch start height -> { height, time, difficulty }
|
|
146
|
+
this.samples = new Map(); // sample height -> { height, time, difficulty }
|
|
147
|
+
this.pools = new Map(); // height -> { height, time, poolKey, name, labelled, tagText }
|
|
148
|
+
this.tip = null; this.tipTime = null; this.difficulty = null; this.networkHashPs = null;
|
|
149
|
+
this.busy = false; this.stopped = false; this.lastError = null; this.at = null;
|
|
150
|
+
this.poolTimer = null; this.poolFilling = false; this.poolTodo = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
stop() { this.stopped = true; if (this.poolTimer) clearTimeout(this.poolTimer); }
|
|
154
|
+
|
|
155
|
+
/** Called by the monitor whenever fresh chain info lands; cheap when nothing changed. */
|
|
156
|
+
async refresh(chainInfo) {
|
|
157
|
+
if (this.stopped || this.busy || !chainInfo || !Number.isFinite(chainInfo.blocks)) return;
|
|
158
|
+
if (chainInfo.initialblockdownload === true) return;
|
|
159
|
+
const tip = chainInfo.blocks;
|
|
160
|
+
this.busy = true;
|
|
161
|
+
try {
|
|
162
|
+
const tipChanged = tip !== this.tip;
|
|
163
|
+
this.tip = tip; this.difficulty = chainInfo.difficulty ?? this.difficulty;
|
|
164
|
+
// getblockchaininfo carries the tip's time on Core 24+; older builds get one header read
|
|
165
|
+
if (Number.isFinite(chainInfo.time)) this.tipTime = chainInfo.time;
|
|
166
|
+
else if (tipChanged || this.tipTime == null) { const h = await this.headers([tip]); this.tipTime = h.get(tip)?.time ?? this.tipTime; }
|
|
167
|
+
if (tipChanged || this.networkHashPs == null) {
|
|
168
|
+
const [hr] = await this.rpc.batch([{ method: 'getnetworkhashps', params: [DAY_BLOCKS * this.windowDays] }], { priority: 6 });
|
|
169
|
+
if (hr?.ok) this.networkHashPs = hr.result;
|
|
170
|
+
}
|
|
171
|
+
await this.ensureRewards(tip);
|
|
172
|
+
await this.ensureEpochs(tip);
|
|
173
|
+
await this.ensureSamples(tip);
|
|
174
|
+
this.queuePools(tip);
|
|
175
|
+
this.at = this.now();
|
|
176
|
+
this.lastError = null;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
this.lastError = err?.message ?? String(err);
|
|
179
|
+
} finally {
|
|
180
|
+
this.busy = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async ensureRewards(tip) {
|
|
185
|
+
const want = [];
|
|
186
|
+
for (let h = tip; h > tip - this.rewardBlocks && h > 0; h--) if (!this.rewards.has(h)) want.push(h);
|
|
187
|
+
for (let i = 0; i < want.length; i += 12) { // twelve block reads a batch: a step the live polls can slip between
|
|
188
|
+
const chunk = want.slice(i, i + 12);
|
|
189
|
+
const res = await this.rpc.batch(chunk.map((h) => ({ method: 'getblockstats', params: [h, ['height', 'time', 'totalfee', 'subsidy', 'txs']] })), { priority: 6, heavy: true });
|
|
190
|
+
res.forEach((r, k) => { if (r?.ok && r.result) this.rewards.set(chunk[k], r.result); });
|
|
191
|
+
}
|
|
192
|
+
for (const h of [...this.rewards.keys()]) if (h <= tip - this.rewardBlocks - 10) this.rewards.delete(h);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** height -> { height, time, difficulty } for a list of heights, two batched in-memory lookups each. */
|
|
196
|
+
async headers(heights) {
|
|
197
|
+
const out = new Map();
|
|
198
|
+
for (let i = 0; i < heights.length; i += 50) {
|
|
199
|
+
const chunk = heights.slice(i, i + 50);
|
|
200
|
+
const hashes = await this.rpc.batch(chunk.map((h) => ({ method: 'getblockhash', params: [h] })), { priority: 6 });
|
|
201
|
+
const withHash = chunk.map((h, k) => ({ h, hash: hashes[k]?.ok ? hashes[k].result : null })).filter((x) => x.hash);
|
|
202
|
+
if (!withHash.length) continue;
|
|
203
|
+
const hdrs = await this.rpc.batch(withHash.map((x) => ({ method: 'getblockheader', params: [x.hash] })), { priority: 6 });
|
|
204
|
+
hdrs.forEach((r, k) => { if (r?.ok && r.result) out.set(withHash[k].h, { height: withHash[k].h, time: r.result.time, difficulty: r.result.difficulty }); });
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async ensureEpochs(tip) {
|
|
210
|
+
const epochStart = tip - (tip % EPOCH);
|
|
211
|
+
const perYear = Math.ceil((this.sampleDays * DAY_BLOCKS) / EPOCH) + 1;
|
|
212
|
+
const want = [];
|
|
213
|
+
for (let i = 0; i <= perYear; i++) { const h = epochStart - i * EPOCH; if (h >= 0 && !this.epochs.has(h)) want.push(h); }
|
|
214
|
+
// the block before the current period too, for the previous adjustment's base difficulty
|
|
215
|
+
if (epochStart > 0 && !this.epochs.has(epochStart - 1)) want.push(epochStart - 1);
|
|
216
|
+
if (!want.length) return;
|
|
217
|
+
for (const [h, v] of await this.headers(want)) this.epochs.set(h, v);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async ensureSamples(tip) {
|
|
221
|
+
const anchor = tip - (tip % DAY_BLOCKS);
|
|
222
|
+
const want = [];
|
|
223
|
+
for (let i = 0; i <= this.sampleDays; i++) { const h = anchor - i * DAY_BLOCKS; if (h >= 0 && !this.samples.has(h)) want.push(h); }
|
|
224
|
+
if (want.length) for (const [h, v] of await this.headers(want)) this.samples.set(h, v);
|
|
225
|
+
for (const h of [...this.samples.keys()]) if (h < anchor - (this.sampleDays + 2) * DAY_BLOCKS) this.samples.delete(h);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- the week of coinbases, in the background
|
|
229
|
+
queuePools(tip) {
|
|
230
|
+
const floor = Math.max(1, tip - Math.ceil(this.windowDays * DAY_BLOCKS * 1.15)); // a little past a week, then the view trims by time
|
|
231
|
+
const todo = [];
|
|
232
|
+
for (let h = tip; h >= floor; h--) if (!this.pools.has(h)) todo.push(h);
|
|
233
|
+
this.poolTodo = todo;
|
|
234
|
+
for (const h of [...this.pools.keys()]) if (h < floor - 50) this.pools.delete(h);
|
|
235
|
+
if (todo.length && !this.poolFilling) this.pumpPools();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
pumpPools() {
|
|
239
|
+
if (this.stopped) return;
|
|
240
|
+
this.poolFilling = true;
|
|
241
|
+
const chunk = this.poolTodo.splice(0, 8);
|
|
242
|
+
if (!chunk.length) { this.poolFilling = false; return; }
|
|
243
|
+
this.fetchPools(chunk)
|
|
244
|
+
.catch((err) => { this.lastError = `pools: ${err?.message ?? err}`; this.poolTodo.unshift(...chunk); })
|
|
245
|
+
.finally(() => {
|
|
246
|
+
if (this.stopped) return;
|
|
247
|
+
this.poolTimer = setTimeout(() => this.pumpPools(), this.lastError ? 15_000 : 3_000);
|
|
248
|
+
this.poolTimer.unref?.();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async fetchPools(heights) {
|
|
253
|
+
const hashes = await this.rpc.batch(heights.map((h) => ({ method: 'getblockhash', params: [h] })), { priority: 7 });
|
|
254
|
+
const withHash = heights.map((h, k) => ({ h, hash: hashes[k]?.ok ? hashes[k].result : null })).filter((x) => x.hash);
|
|
255
|
+
const blocks = await this.rpc.batch(withHash.map((x) => ({ method: 'getblock', params: [x.hash, 1] })), { priority: 7, heavy: true });
|
|
256
|
+
const withCb = withHash.map((x, k) => ({ ...x, block: blocks[k]?.ok ? blocks[k].result : null })).filter((x) => x.block?.tx?.length);
|
|
257
|
+
const cbs = await this.rpc.batch(withCb.map((x) => ({ method: 'getrawtransaction', params: [x.block.tx[0], 2, x.hash] })), { priority: 7, heavy: true });
|
|
258
|
+
const map = this.poolMapOf(), aliases = this.aliasesOf();
|
|
259
|
+
withCb.forEach((x, k) => {
|
|
260
|
+
const tx = cbs[k]?.ok ? cbs[k].result : null;
|
|
261
|
+
const hex = tx?.vin?.[0]?.coinbase ?? '';
|
|
262
|
+
const decoded = decodeCoinbase(hex);
|
|
263
|
+
const tagText = decoded?.tagText ?? '';
|
|
264
|
+
const matched = matchPool(map, { tagText, rawHex: hex });
|
|
265
|
+
const rawKey = decoded?.tag ? decoded.tag : tagFingerprint(tagText); // the ledger's own key rule (minerRow)
|
|
266
|
+
const key = matched ? matched.key : rawKey;
|
|
267
|
+
const name = matched ? matched.name : (aliasFor(aliases, rawKey) ?? rawKey); // the ledger shows an unlabelled pool by its key
|
|
268
|
+
this.pools.set(x.h, { height: x.h, time: x.block.time, poolKey: key, name, labelled: !!matched, tagText });
|
|
269
|
+
});
|
|
270
|
+
this.at = this.now();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
view() {
|
|
274
|
+
const now = this.now();
|
|
275
|
+
const tip = this.tip;
|
|
276
|
+
const epochStart = tip != null ? tip - (tip % EPOCH) : null;
|
|
277
|
+
const cur = epochStart != null ? this.epochs.get(epochStart) : null;
|
|
278
|
+
const prev = epochStart != null ? this.epochs.get(epochStart - 1) : null;
|
|
279
|
+
const est = tip != null && cur ? difficultyEstimate({ height: tip, tipTime: this.tipTime, epochStartTime: cur.time, difficulty: this.difficulty, prevDifficulty: prev?.difficulty ?? null, now }) : null;
|
|
280
|
+
const series = hashrateSeries([...this.samples.values(), ...(tip != null && this.tipTime != null && this.difficulty != null ? [{ height: tip, time: this.tipTime, difficulty: this.difficulty }] : [])]);
|
|
281
|
+
const shares = poolShares([...this.pools.values()], { now, windowSec: this.windowDays * 86_400 });
|
|
282
|
+
const windowFloor = tip != null ? tip - Math.ceil(this.windowDays * DAY_BLOCKS * 1.15) : null;
|
|
283
|
+
return {
|
|
284
|
+
at: this.at, lastError: this.lastError, tip,
|
|
285
|
+
// exactly the newest 144: the map keeps a few extra rows below the window between prunes
|
|
286
|
+
rewards: rewardStats([...this.rewards.values()].sort((a, b) => b.height - a.height).slice(0, this.rewardBlocks)),
|
|
287
|
+
difficulty: this.difficulty,
|
|
288
|
+
adjustment: est,
|
|
289
|
+
halving: tip != null ? halvingInfo(tip, { now }) : null,
|
|
290
|
+
adjustments: adjustments([...this.epochs.values()].filter((e) => e.height % EPOCH === 0)).slice(0, 27), // a year and a period; the card shows six, View more all
|
|
291
|
+
hashrate: { networkHashPs: this.networkHashPs, windowBlocks: DAY_BLOCKS * this.windowDays, series },
|
|
292
|
+
pools: { ...shares, filled: this.pools.size, todo: this.poolTodo.length, filling: this.poolFilling, floor: windowFloor },
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// The block being built right now, and the packages inside it.
|
|
2
|
+
//
|
|
3
|
+
// This module is pure: it takes the reply of `getblocktemplate` and turns it into the
|
|
4
|
+
// numbers the block-flow's "next block" card and the Goggles charts need. No I/O, so the
|
|
5
|
+
// cluster math is testable against a frozen real reply.
|
|
6
|
+
//
|
|
7
|
+
// WHAT THIS REPLACED, and a claim I got wrong. The Mining page used to say Goggles-style
|
|
8
|
+
// cluster analysis was impossible on this node, because `getrawmempool` verbose on this
|
|
9
|
+
// build returns `vsize` and `fees.base` with no `depends` and no `ancestorcount`. That
|
|
10
|
+
// half was measured; the conclusion drawn from it was not. The dependency graph IS
|
|
11
|
+
// published -- by `getblocktemplate`, where every selected transaction carries
|
|
12
|
+
// `depends: [indices into this same array]`. Measured 2026-09-09 on height 966265:
|
|
13
|
+
// 1,496 transactions selected, 1,475 ancestor packages, 19 of them multi-transaction,
|
|
14
|
+
// 40 transactions (2.7%) inside a package, largest package 3 transactions.
|
|
15
|
+
// One package: child at 38.1 sat/vB, parent at 0.5 sat/vB, package feerate 10.93
|
|
16
|
+
// sat/vB -- a textbook child-pays-for-parent, visible rather than inferred.
|
|
17
|
+
//
|
|
18
|
+
// COST, which is why it is polled once a minute and never during initial download:
|
|
19
|
+
// getblocktemplate(rules:[segwit]) = 1,790,010 bytes and 1.29-1.48 s per call on this
|
|
20
|
+
// node, whose RPC server serves one connection at a time on one thread. That is the
|
|
21
|
+
// node's time, not ours, so the caller owns the cadence and the coalescing; and the
|
|
22
|
+
// `data` field (full transaction hex, the reason for the 1.79 MB) is dropped at the
|
|
23
|
+
// door -- nothing downstream reads it, and keeping it would put megabytes in every
|
|
24
|
+
// snapshot frame.
|
|
25
|
+
|
|
26
|
+
const VBYTES = 4; // weight units per virtual byte
|
|
27
|
+
|
|
28
|
+
/** sat/vB of one selected transaction, or null when it cannot be computed. */
|
|
29
|
+
function feeRate(tx) {
|
|
30
|
+
const w = tx?.weight;
|
|
31
|
+
if (!Number.isFinite(w) || w <= 0 || !Number.isFinite(tx?.fee)) return null;
|
|
32
|
+
return +(tx.fee / (w / VBYTES)).toFixed(2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pctOf = (sorted, p) => (sorted.length ? sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))] : null);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The header numbers for the next-block card, plus a fixed-bucket feerate histogram so
|
|
39
|
+
* 1,496 transactions never have to travel to the browser.
|
|
40
|
+
*/
|
|
41
|
+
export function summarizeTemplate(t, { at = Date.now(), previous = null } = {}) {
|
|
42
|
+
const txs = Array.isArray(t?.transactions) ? t.transactions : [];
|
|
43
|
+
const rates = txs.map(feeRate).filter((v) => v != null).sort((a, b) => a - b);
|
|
44
|
+
const weight = txs.reduce((n, x) => n + (Number.isFinite(x?.weight) ? x.weight : 0), 0);
|
|
45
|
+
const limit = Number.isFinite(t?.weightlimit) && t.weightlimit > 0 ? t.weightlimit : 4_000_000;
|
|
46
|
+
const fees = txs.reduce((n, x) => n + (Number.isFinite(x?.fee) ? x.fee : 0), 0);
|
|
47
|
+
return {
|
|
48
|
+
height: t?.height ?? null,
|
|
49
|
+
previous: t?.previousblockhash ?? previous,
|
|
50
|
+
txCount: txs.length,
|
|
51
|
+
weight,
|
|
52
|
+
weightLimit: limit,
|
|
53
|
+
weightPct: +((100 * weight) / limit).toFixed(1),
|
|
54
|
+
totalFeesSat: fees,
|
|
55
|
+
coinbaseSat: Number.isFinite(t?.coinbasevalue) ? t.coinbasevalue : null,
|
|
56
|
+
feeRate: {
|
|
57
|
+
min: rates[0] ?? null,
|
|
58
|
+
p50: pctOf(rates, 0.5),
|
|
59
|
+
p75: pctOf(rates, 0.75),
|
|
60
|
+
p90: pctOf(rates, 0.9),
|
|
61
|
+
max: rates[rates.length - 1] ?? null,
|
|
62
|
+
median: rates.length % 2 ? rates[(rates.length - 1) / 2] : (rates[rates.length / 2 - 1] + rates[rates.length / 2]) / 2,
|
|
63
|
+
},
|
|
64
|
+
// Log buckets from 0.1 to 1000 sat/vB. They carry the WEIGHT in each bucket, not
|
|
65
|
+
// just the count, because the useful question -- "what feerate still gets into the
|
|
66
|
+
// next block?" -- is about weight, and a histogram of counts cannot answer it.
|
|
67
|
+
feeRateHistogram: bucketize(txs.map((t) => ({ rate: feeRate(t), weight: Number.isFinite(t?.weight) ? t.weight : 0 }))),
|
|
68
|
+
mintime: t?.mintime ?? null,
|
|
69
|
+
bits: t?.bits ?? null,
|
|
70
|
+
sigopLimit: t?.sigoplimit ?? null,
|
|
71
|
+
sizeLimit: t?.sizelimit ?? null,
|
|
72
|
+
version: t?.version ?? null,
|
|
73
|
+
signal: t?.vbavailable && Object.keys(t.vbavailable).length
|
|
74
|
+
? Object.entries(t.vbavailable).map(([k, v]) => ({ bit: k, available: v, required: (t.vbrequired ?? 0) & (1 << Number(k)) ? 1 : 0 }))
|
|
75
|
+
: null,
|
|
76
|
+
at,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const BUCKET_EDGES = [0.1, 0.5, 1, 2, 3, 4, 6, 8, 12, 20, 30, 50, 100, 250, 1000];
|
|
81
|
+
|
|
82
|
+
function bucketize(txs) {
|
|
83
|
+
const out = BUCKET_EDGES.map((hi, i) => ({ hi, lo: i ? BUCKET_EDGES[i - 1] : 0, n: 0, weight: 0 }));
|
|
84
|
+
for (const t of txs) {
|
|
85
|
+
if (t.rate == null) continue;
|
|
86
|
+
const i = BUCKET_EDGES.findIndex((hi) => t.rate <= hi);
|
|
87
|
+
const b = out[i < 0 ? out.length - 1 : i];
|
|
88
|
+
b.n++;
|
|
89
|
+
b.weight += t.weight;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* What the next block can still take, and what spills past it.
|
|
96
|
+
*
|
|
97
|
+
* Walk the selected transactions from the highest feerate down until the remaining
|
|
98
|
+
* weight is gone; the bucket it runs out in is the marginal rate -- the thing a wallet
|
|
99
|
+
* actually needs to know. Anything below that is the spill, and the spill has to fit in
|
|
100
|
+
* a later block, which is where the ghost cards get their content.
|
|
101
|
+
*
|
|
102
|
+
* Everything here is arithmetic on what the node reported. `estimate: true` marks the
|
|
103
|
+
* forward-looking numbers, because "the backlog is about 1.3 blocks deep" is a shape of
|
|
104
|
+
* the queue, not a claim that anyone is building block 3 from here.
|
|
105
|
+
*/
|
|
106
|
+
export function blockEconomy({ template, mempool, avgWeightMined = null, maxAhead = 3 } = {}) {
|
|
107
|
+
const limit = Number.isFinite(template?.weightLimit) && template.weightLimit > 0 ? template.weightLimit : 4_000_000;
|
|
108
|
+
const used = Number.isFinite(template?.weight) ? template.weight : 0;
|
|
109
|
+
const remaining = Math.max(0, limit - used);
|
|
110
|
+
const buckets = (template?.feeRateHistogram ?? []).slice().reverse(); // richest first
|
|
111
|
+
|
|
112
|
+
let marginal = null;
|
|
113
|
+
let spillWeight = 0;
|
|
114
|
+
let spillCount = 0;
|
|
115
|
+
let acc = 0;
|
|
116
|
+
for (const b of buckets) {
|
|
117
|
+
if (!b?.n) continue;
|
|
118
|
+
if (acc + b.weight <= remaining) { acc += b.weight; continue; }
|
|
119
|
+
marginal = { rate: b.hi, lo: b.lo, bandUnresolved: true };
|
|
120
|
+
spillWeight += (b.weight - Math.max(0, remaining - acc)) + 0;
|
|
121
|
+
spillCount += b.n;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
125
|
+
const b = buckets[i];
|
|
126
|
+
if (!b?.n) continue;
|
|
127
|
+
if (marginal && b.hi >= marginal.rate) continue;
|
|
128
|
+
spillWeight += b.weight;
|
|
129
|
+
spillCount += b.n;
|
|
130
|
+
}
|
|
131
|
+
if (marginal == null && remaining > 0) marginal = { rate: null, bandUnresolved: false, note: 'the template did not fill the block' };
|
|
132
|
+
|
|
133
|
+
// The queue, in blocks. getmempoolinfo.bytes is serialized bytes, and a mined block
|
|
134
|
+
// is ~1M vB, so the ratio is honest as an order of magnitude and no more precise than
|
|
135
|
+
// that -- which the card says.
|
|
136
|
+
const poolBytes = Number.isFinite(mempool?.bytes) ? mempool.bytes : null;
|
|
137
|
+
const blockBytes = avgWeightMined != null ? avgWeightMined / 4 : 1_000_000;
|
|
138
|
+
const fitsNext = poolBytes != null ? Math.min(1, (remaining / 4) / Math.max(1, poolBytes)) : null;
|
|
139
|
+
const depth = poolBytes != null ? poolBytes / blockBytes : null;
|
|
140
|
+
|
|
141
|
+
const ahead = [];
|
|
142
|
+
for (let k = 2; k <= maxAhead + 1; k++) {
|
|
143
|
+
const leftAfterNext = poolBytes != null ? Math.max(0, poolBytes - remaining / 4) : null;
|
|
144
|
+
const bytesHere = leftAfterNext == null ? null : Math.min(blockBytes, leftAfterNext - (k - 2) * blockBytes > 0 ? leftAfterNext - (k - 2) * blockBytes : 0);
|
|
145
|
+
ahead.push({
|
|
146
|
+
offset: k,
|
|
147
|
+
estimate: true,
|
|
148
|
+
assembled: false,
|
|
149
|
+
bytes: bytesHere && bytesHere > 0 ? Math.round(bytesHere) : null,
|
|
150
|
+
spillRate: marginal?.rate ?? null,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
remainingWeight: Math.round(remaining),
|
|
156
|
+
remainingPct: +((100 * remaining) / limit).toFixed(1),
|
|
157
|
+
marginal,
|
|
158
|
+
spillCount,
|
|
159
|
+
spillWeight: Math.round(spillWeight),
|
|
160
|
+
poolBytes,
|
|
161
|
+
poolFitsNextPct: fitsNext != null ? +(100 * fitsNext).toFixed(1) : null,
|
|
162
|
+
backlogBlocks: depth != null ? +depth.toFixed(2) : null,
|
|
163
|
+
blockBytesEstimate: Math.round(blockBytes),
|
|
164
|
+
ahead,
|
|
165
|
+
note: 'marginal and spill come from the node\'s own template selection; the queue depth is getmempoolinfo.bytes against ~1M vB per block, which is an order of magnitude, not a schedule.',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Cells for the block visualiser: one entry per transaction, sized by vbytes and
|
|
171
|
+
* coloured by its own feerate, richest first.
|
|
172
|
+
*
|
|
173
|
+
* Bounded, because a template holds ~1,500 transactions and a browser cannot draw that
|
|
174
|
+
* usefully: cells are kept until they cover `coverPct` of the selected weight and the
|
|
175
|
+
* rest becomes ONE aggregate cell. The aggregate is labelled as an aggregate rather than
|
|
176
|
+
* dropped -- a picture that quietly omits a fifth of the weight is a picture of
|
|
177
|
+
* something else. Richest-first is not styling: it is the rule the miner used, and the
|
|
178
|
+
* cut where the block runs out is only visible in that order.
|
|
179
|
+
*/
|
|
180
|
+
export function templateCells(txs, { maxCells = 400, coverPct = 0.97 } = {}) {
|
|
181
|
+
const list = (txs ?? [])
|
|
182
|
+
.map((t) => ({ vbytes: Math.max(1, Math.round((Number(t.weight) || 0) / 4)), rate: feeRate(t), txid: t.txid ?? t.hash ?? null }))
|
|
183
|
+
.filter((c) => Number.isFinite(c.vbytes) && c.rate != null)
|
|
184
|
+
.sort((a, b) => b.rate - a.rate);
|
|
185
|
+
|
|
186
|
+
const total = list.reduce((n, c) => n + c.vbytes, 0);
|
|
187
|
+
const want = total * coverPct;
|
|
188
|
+
const cells = [];
|
|
189
|
+
let acc = 0;
|
|
190
|
+
for (const c of list) {
|
|
191
|
+
if (cells.length >= maxCells || acc >= want) break;
|
|
192
|
+
cells.push(c);
|
|
193
|
+
acc += c.vbytes;
|
|
194
|
+
}
|
|
195
|
+
const tail = list.slice(cells.length);
|
|
196
|
+
if (tail.length) {
|
|
197
|
+
const w = tail.reduce((n, c) => n + c.vbytes, 0);
|
|
198
|
+
const r = tail.reduce((n, c) => n + c.rate * c.vbytes, 0) / Math.max(1, w);
|
|
199
|
+
cells.push({ vbytes: w, rate: +r.toFixed(2), aggregate: tail.length });
|
|
200
|
+
}
|
|
201
|
+
return { cells: cells.sort((a, b) => b.rate - a.rate), totalVbytes: total, tailCount: tail.length };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Ancestor packages -- what Goggles calls clusters -- from `depends`.
|
|
206
|
+
*
|
|
207
|
+
* `depends` entries are indices into the same `transactions` array, measured
|
|
208
|
+
* ('depends': [6] on a transaction whose own position is higher). A package is a root and
|
|
209
|
+
* everything reachable from it through children. Package feerate is the whole package's
|
|
210
|
+
* fees over the whole package's weight, which is the only number that means anything for
|
|
211
|
+
* a child paying for a parent: the child's own 38 sat/vB says nothing about the chain
|
|
212
|
+
* until the parent's 0.5 is folded in and the answer becomes 10.93.
|
|
213
|
+
*/
|
|
214
|
+
export function packagesFromTemplate(txs, { keepTop = 20 } = {}) {
|
|
215
|
+
const list = Array.isArray(txs) ? txs : [];
|
|
216
|
+
const n = list.length;
|
|
217
|
+
const children = new Map();
|
|
218
|
+
for (let i = 0; i < n; i++) {
|
|
219
|
+
for (const d of list[i]?.depends ?? []) {
|
|
220
|
+
if (Number.isInteger(d) && d >= 0 && d < n) {
|
|
221
|
+
if (!children.has(d)) children.set(d, []);
|
|
222
|
+
children.get(d).push(i);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const seen = new Set();
|
|
227
|
+
const packs = [];
|
|
228
|
+
for (let i = 0; i < n; i++) {
|
|
229
|
+
if (seen.has(i)) continue;
|
|
230
|
+
const stack = [i];
|
|
231
|
+
const members = [];
|
|
232
|
+
seen.add(i);
|
|
233
|
+
while (stack.length) {
|
|
234
|
+
const cur = stack.pop();
|
|
235
|
+
members.push(cur);
|
|
236
|
+
for (const c of children.get(cur) ?? []) if (!seen.has(c)) { seen.add(c); stack.push(c); }
|
|
237
|
+
}
|
|
238
|
+
packs.push(members);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const described = packs.map((members) => {
|
|
242
|
+
const txs2 = members.map((i) => list[i]).filter(Boolean);
|
|
243
|
+
const fee = txs2.reduce((x, t) => x + (Number.isFinite(t.fee) ? t.fee : 0), 0);
|
|
244
|
+
const weight = txs2.reduce((x, t) => x + (Number.isFinite(t.weight) ? t.weight : 0), 0);
|
|
245
|
+
const rates = txs2.map(feeRate).filter((v) => v != null);
|
|
246
|
+
return {
|
|
247
|
+
size: members.length,
|
|
248
|
+
feesSat: fee,
|
|
249
|
+
weight,
|
|
250
|
+
packageFeeRate: weight > 0 ? +(fee / (weight / VBYTES)).toFixed(2) : null,
|
|
251
|
+
childRate: rates.length ? Math.max(...rates) : null,
|
|
252
|
+
parentRate: rates.length ? Math.min(...rates) : null,
|
|
253
|
+
// The thing a human looks at first: does the child carry the parent?
|
|
254
|
+
cpfp: members.length > 1 && rates.length > 1 && Math.max(...rates) > Math.min(...rates) * 2,
|
|
255
|
+
txids: txs2.slice(0, 6).map((t) => t.txid ?? t.hash ?? null).filter(Boolean),
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const sizeHistogram = {};
|
|
260
|
+
for (const p of described) sizeHistogram[p.size] = (sizeHistogram[p.size] ?? 0) + 1;
|
|
261
|
+
const multi = described.filter((p) => p.size > 1).sort((a, b) => b.size - a.size || (b.packageFeeRate ?? 0) - (a.packageFeeRate ?? 0));
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
total: described.length,
|
|
265
|
+
multiTx: multi.length,
|
|
266
|
+
txsInPackages: multi.reduce((n2, p) => n2 + p.size, 0),
|
|
267
|
+
largest: multi[0]?.size ?? 1,
|
|
268
|
+
cpfpCandidates: described.filter((p) => p.cpfp).length,
|
|
269
|
+
sizeHistogram,
|
|
270
|
+
top: (keepTop ? multi.slice(0, keepTop) : multi).map((p) => ({ ...p, txids: p.txids.slice(0, 4) })),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The honest note that travels with this data, so cost and cadence are never invisible. */
|
|
275
|
+
export const TEMPLATE_NOTE = 'getblocktemplate(rules:[segwit]) costs this node roughly 1.3-1.5 s of its single RPC thread and 1.79 MB per call; it is polled once a minute, skipped during initial download, and its transaction hex is dropped before the snapshot is built.';
|