blockyard 0.0.1 → 0.0.9
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 +679 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +172 -4
- package/SECURITY.md +38 -0
- package/bin/blockyard.js +40 -0
- package/config/pool-map.json +2620 -0
- package/docs/API.md +1575 -0
- package/docs/ARCHITECTURE.md +1307 -0
- package/docs/AUTO-UPDATE.md +269 -0
- package/docs/CONFIGURATION.md +840 -0
- package/docs/DEFECTS.md +813 -0
- package/docs/EFFECTS-AGENTS.md +448 -0
- package/docs/GETTING-STARTED.md +202 -0
- package/docs/INSTALL.md +490 -0
- package/docs/MEASUREMENTS.md +1254 -0
- package/docs/PRIVATE-LEADERBOARD.md +230 -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 +195 -0
- package/docs/STATE-2026-09-09.md +200 -0
- package/docs/TROUBLESHOOTING.md +298 -0
- package/docs/USER-GUIDE.md +1022 -0
- package/package.json +53 -5
- package/public/404.html +9 -0
- package/public/css/app.css +1785 -0
- package/public/index.html +893 -0
- package/public/js/about.js +112 -0
- package/public/js/agents.js +964 -0
- package/public/js/app.js +1312 -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 +2678 -0
- package/public/js/breakout.js +224 -0
- package/public/js/charts.js +635 -0
- package/public/js/depthchart.js +311 -0
- package/public/js/details3d.js +2957 -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 +83 -0
- package/public/js/markets.js +357 -0
- package/public/js/mining.js +1138 -0
- package/public/js/panels.js +966 -0
- package/public/js/pricechart.js +188 -0
- package/public/js/settings.js +1014 -0
- package/public/js/tetris.js +226 -0
- package/public/js/tetrust.js +356 -0
- package/public/js/tetsound.js +175 -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 +20 -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 +410 -0
- package/scripts/shots.mjs +272 -0
- package/scripts/smoke.sh +327 -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 +193 -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 +2516 -0
- package/server/collect/nextblock.js +275 -0
- package/server/collect/sync.js +386 -0
- package/server/config.js +620 -0
- package/server/http/api.js +1275 -0
- package/server/http/explorer.js +418 -0
- package/server/http/server.js +412 -0
- package/server/http/sse.js +176 -0
- package/server/http/static.js +212 -0
- package/server/main.js +628 -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/util/fmt.js +29 -0
- package/systemd/blockyard.service +100 -0
package/scripts/check.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CHECK A NODE BEFORE RUNNING AGAINST IT: the RPC server answers, the credentials work, the
|
|
3
|
+
// chain is the one configured, the indexes the explorer needs are there, the block files can be
|
|
4
|
+
// read (the address index is built from them), the node's log is where it should be, and an
|
|
5
|
+
// address index, if configured, is readable, writable and not far behind.
|
|
6
|
+
//
|
|
7
|
+
// npm run check # every node in config/local.json
|
|
8
|
+
// node scripts/check.js --node <id>
|
|
9
|
+
//
|
|
10
|
+
// Prints one line per check and exits 1 if anything FAILED. `npm run setup` runs the same checks
|
|
11
|
+
// on the answers it is given before it writes config/local.json. The checks are a function
|
|
12
|
+
// (runChecks) so they can be tested against a stub node, with the printing kept out here.
|
|
13
|
+
import { existsSync, statSync, readdirSync, accessSync, readFileSync, realpathSync, constants as FS } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { loadConfig, configProblems, resolveCookie } from '../server/config.js';
|
|
16
|
+
import { RpcClient } from '../server/rpc/client.js';
|
|
17
|
+
import { MAGIC, xorKey, readChainFile, records } from '../server/chain/blockfile.js';
|
|
18
|
+
import { c, checkLine } from './ui.js';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const QUIET = { info() {}, warn() {}, error() {}, debug() {} };
|
|
22
|
+
|
|
23
|
+
/** The RPC client a check talks through; setup and the tests pass their own. */
|
|
24
|
+
export function clientFor(node, cfg) {
|
|
25
|
+
return new RpcClient(node, { ...(cfg?.rpc ?? {}), ...(node.rpc ?? {}) }, { log: QUIET });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CORE_VERSION_MIN = 250000; // getblock verbosity 3 (the follower's one call) arrived in 25.0
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Every check for one node. `rpc` is anything with batch(calls) -> [{ok, result, error}].
|
|
32
|
+
* Returns { ok, checks: [{ name, status: 'ok'|'warn'|'fail'|'info', detail }], facts }.
|
|
33
|
+
*/
|
|
34
|
+
export async function runChecks(node, { rpc, fs = { existsSync, statSync, readdirSync, accessSync, readFileSync }, readBlockFile = readChainFile } = {}) {
|
|
35
|
+
const checks = [];
|
|
36
|
+
const facts = {};
|
|
37
|
+
const add = (name, status, detail) => { checks.push({ name, status, detail }); return status; };
|
|
38
|
+
|
|
39
|
+
// credentials
|
|
40
|
+
const cred = resolveCookie(node);
|
|
41
|
+
if (!cred) add('credentials', 'fail', `no cookie file found under ${node.datadir ?? '(no datadir)'} and no rpcUser/rpcPassword`);
|
|
42
|
+
else add('credentials', 'ok', cred.source === 'config' ? `rpcUser "${cred.user}" from the config` : `cookie ${cred.source}`);
|
|
43
|
+
|
|
44
|
+
// the RPC server
|
|
45
|
+
// every call is timed: how fast the node answers is half of what an install needs to know
|
|
46
|
+
const ms = (t0) => `${Date.now() - t0 >= 1000 ? `${((Date.now() - t0) / 1000).toFixed(1)} s` : `${Date.now() - t0} ms`}`;
|
|
47
|
+
const one = async (method, params = [], timeoutMs = 20000) => {
|
|
48
|
+
const t0 = Date.now();
|
|
49
|
+
try { const [r] = await rpc.batch([{ method, params }], { timeoutMs }); r.ms = ms(t0); return r; }
|
|
50
|
+
catch (err) { return { ok: false, ms: ms(t0), error: { message: err.message, kind: err.kind } }; }
|
|
51
|
+
};
|
|
52
|
+
const info = await one('getblockchaininfo');
|
|
53
|
+
if (!info.ok) {
|
|
54
|
+
add('rpc', 'fail', `${node.rpcUrl}: ${info.error.message}`);
|
|
55
|
+
return { ok: false, checks, facts };
|
|
56
|
+
}
|
|
57
|
+
const chain = info.result.chain;
|
|
58
|
+
facts.chain = chain; facts.blocks = info.result.blocks; facts.headers = info.result.headers;
|
|
59
|
+
add('rpc', 'ok', `${node.rpcUrl} answers in ${info.ms}: chain ${chain}, block ${info.result.blocks.toLocaleString()} of ${info.result.headers.toLocaleString()} headers${info.result.initialblockdownload ? ', still in initial block download' : ''}${info.result.pruned ? ', PRUNED' : ''}`);
|
|
60
|
+
if (node.chainHint && node.chainHint !== chain) add('chain', 'fail', `config says chainHint "${node.chainHint}" but the node is on "${chain}"`);
|
|
61
|
+
if (info.result.pruned) add('pruned', 'fail', 'a pruned node has discarded old block files; the address index needs every one of them');
|
|
62
|
+
|
|
63
|
+
const net = await one('getnetworkinfo');
|
|
64
|
+
if (net.ok) {
|
|
65
|
+
facts.version = net.result.version; facts.subversion = net.result.subversion;
|
|
66
|
+
const v = net.result.version;
|
|
67
|
+
add('version', v >= CORE_VERSION_MIN ? 'ok' : 'fail', `${net.result.subversion} (${v})${v < CORE_VERSION_MIN ? ` -- the address index follower needs getblock verbosity 3, Bitcoin Core 25.0 or later` : ''}`);
|
|
68
|
+
} else add('version', 'warn', `getnetworkinfo: ${net.error.message}`);
|
|
69
|
+
|
|
70
|
+
const idx = await one('getindexinfo');
|
|
71
|
+
if (idx.ok) {
|
|
72
|
+
const tx = idx.result.txindex;
|
|
73
|
+
if (!tx) add('txindex', 'fail', 'txindex is off: the explorer cannot look a confirmed transaction up by id (set txindex=1 in bitcoin.conf)');
|
|
74
|
+
else add('txindex', tx.synced ? 'ok' : 'warn', tx.synced ? `synced to ${tx.best_block_height.toLocaleString()}` : `still building (${tx.best_block_height.toLocaleString()} of ${info.result.blocks.toLocaleString()})`);
|
|
75
|
+
const cs = idx.result.coinstatsindex;
|
|
76
|
+
add('coinstatsindex', cs ? (cs.synced ? 'ok' : 'warn') : 'info', cs ? (cs.synced ? 'synced: the Chain page has UTXO figures' : 'still building') : 'off: the Chain page marks UTXO figures unindexed (optional)');
|
|
77
|
+
} else add('txindex', 'warn', `getindexinfo: ${idx.error.message}`);
|
|
78
|
+
|
|
79
|
+
// the explorer's one heavy need from the node: a block with every input's prevout
|
|
80
|
+
const best = await one('getbestblockhash');
|
|
81
|
+
if (best.ok) {
|
|
82
|
+
const blk = await one('getblock', [best.result, 3], 120_000);
|
|
83
|
+
if (blk.ok) {
|
|
84
|
+
const withPrevout = blk.result.tx.slice(1, 4).every((t) => t.vin.every((v) => v.prevout));
|
|
85
|
+
const slow = blk.ms.endsWith(' s') && parseFloat(blk.ms) >= 5;
|
|
86
|
+
add('getblock 3', withPrevout ? (slow ? 'warn' : 'ok') : 'fail', withPrevout ? `the tip block decodes with prevouts (${blk.result.tx.length.toLocaleString()} transactions) in ${blk.ms}${slow ? ' -- slow: the block files are on a slow disk, or the node is busy' : ''}` : 'the node answered verbosity 3 without prevouts');
|
|
87
|
+
} else add('getblock 3', 'fail', `getblock <tip> 3: ${blk.error.message} -- the address index follower cannot run`);
|
|
88
|
+
}
|
|
89
|
+
// the mempool, verbose: the monitor's heaviest regular read, every 20 s; how long the node takes
|
|
90
|
+
// over it is what decides whether the block-space board fills
|
|
91
|
+
const mp = await one('getrawmempool', [true], 120_000);
|
|
92
|
+
if (mp.ok) {
|
|
93
|
+
const n = Object.keys(mp.result).length;
|
|
94
|
+
const slow = mp.ms.endsWith(' s') && parseFloat(mp.ms) >= 10;
|
|
95
|
+
add('mempool', slow ? 'warn' : 'ok', `${n.toLocaleString()} transactions, verbose, in ${mp.ms}${slow ? ' -- slow: the board and the block being built will lag behind this' : ''}`);
|
|
96
|
+
} else add('mempool', 'warn', `getrawmempool verbose: ${mp.error.message} after ${mp.ms}`);
|
|
97
|
+
const addr = await one('getaddresstxids', [{ addresses: [] }]);
|
|
98
|
+
add('address index rpc', 'info', addr.ok ? 'the node has insight-style address RPCs (unused: BlockYard keeps its own index)' : 'the node has no address index, as expected of Bitcoin Core; BlockYard builds its own');
|
|
99
|
+
|
|
100
|
+
// the data directory: block files and the log
|
|
101
|
+
if (!node.datadir) add('datadir', 'fail', 'no datadir configured: the address index is built from the node\'s block files');
|
|
102
|
+
else if (!fs.existsSync(node.datadir)) add('datadir', 'fail', `${node.datadir} does not exist on this machine`);
|
|
103
|
+
else {
|
|
104
|
+
const blocksDir = path.join(node.datadir, 'blocks');
|
|
105
|
+
let names = null;
|
|
106
|
+
try { names = fs.readdirSync(blocksDir); } catch (err) { add('block files', 'fail', `${blocksDir}: ${err.message}`); }
|
|
107
|
+
if (names) {
|
|
108
|
+
const blk = names.filter((f) => /^blk\d{5}\.dat$/.test(f)).sort();
|
|
109
|
+
const rev = names.filter((f) => /^rev\d{5}\.dat$/.test(f)).sort();
|
|
110
|
+
const xor = names.includes('xor.dat');
|
|
111
|
+
facts.blockFiles = blk.length; facts.undoFiles = rev.length;
|
|
112
|
+
if (!blk.length) add('block files', 'fail', `${blocksDir} holds no blk*.dat`);
|
|
113
|
+
else {
|
|
114
|
+
let bytes = 0;
|
|
115
|
+
for (const f of [...blk, ...rev]) { try { bytes += fs.statSync(path.join(blocksDir, f)).size; } catch { /* counted as far as readable */ } }
|
|
116
|
+
facts.blockBytes = bytes;
|
|
117
|
+
add('block files', rev.length === blk.length ? 'ok' : 'warn', `${blk.length} block files and ${rev.length} undo files, ${(bytes / 1e9).toFixed(1)} GB${xor ? ', XOR-obfuscated (xor.dat present)' : ''}${rev.length !== blk.length ? ' -- every block file needs its undo file' : ''}`);
|
|
118
|
+
// prove they can be read: the first file's first record is the genesis block
|
|
119
|
+
try {
|
|
120
|
+
const key = xorKey(blocksDir);
|
|
121
|
+
const buf = readBlockFile(path.join(blocksDir, blk[0]), key);
|
|
122
|
+
const first = records(buf, MAGIC[chain] ?? MAGIC.main, 0, key).next().value;
|
|
123
|
+
const genesis = first && first.body.subarray(4, 36).every((b) => b === 0);
|
|
124
|
+
add('read a block', genesis ? 'ok' : 'fail', genesis ? `${blk[0]} opens and its first record is the genesis block` : `${blk[0]} opens but its first record is not a ${chain} genesis block: wrong chain, or a key this reader does not know`);
|
|
125
|
+
} catch (err) { add('read a block', 'fail', `${blk[0]}: ${err.message}`); }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const logs = [path.join(node.datadir, chain === 'main' ? '' : ({ test: 'testnet3', testnet4: 'testnet4', signet: 'signet', regtest: 'regtest' }[chain] ?? chain), 'debug.log')];
|
|
129
|
+
const log = logs.find((f) => fs.existsSync(f));
|
|
130
|
+
if (log) { const st = fs.statSync(log); facts.logFile = log; add('node log', 'info', `${log} (${(st.size / 1e6).toFixed(1)} MB) -- found; not parsed on Bitcoin Core, and not needed`); }
|
|
131
|
+
else add('node log', 'info', `no debug.log under ${node.datadir} -- not needed`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// an address index, if there is one
|
|
135
|
+
if (node.addressIndex) {
|
|
136
|
+
const dir = node.addressIndex;
|
|
137
|
+
const manifest = path.join(dir, 'manifest.json');
|
|
138
|
+
if (!fs.existsSync(manifest)) add('address index', node.addressIndexBuild === 'manual' ? 'warn' : 'info', `${dir}: not built yet${node.addressIndexBuild === 'manual' ? ` (node scripts/index-build.js --out ${dir})` : ' -- BlockYard builds it when it starts'}`);
|
|
139
|
+
else {
|
|
140
|
+
try {
|
|
141
|
+
const m = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
142
|
+
const behind = info.result.blocks - m.tip.height;
|
|
143
|
+
facts.indexTip = m.tip.height;
|
|
144
|
+
add('address index', behind > 200 ? 'warn' : 'ok', `${dir}: built at ${m.builtAt ?? '?'} to block ${m.tip.height.toLocaleString()}, ${behind.toLocaleString()} behind the node${behind > 200 ? ' (the follower catches up 50 blocks a poll)' : ''}`);
|
|
145
|
+
} catch (err) { add('address index', 'fail', `${manifest}: ${err.message}`); }
|
|
146
|
+
try { fs.accessSync(dir, FS.W_OK); add('index writable', 'ok', 'the follower can write live.log and layers/ there'); }
|
|
147
|
+
catch { add('index writable', 'fail', `${dir} is not writable by this user; the follower writes live.log and layers/ inside it`); }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { ok: !checks.some((c) => c.status === 'fail'), checks, facts };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function printChecks(label, { ok, checks }) {
|
|
155
|
+
console.log(`\n ${c.bold(label)}`);
|
|
156
|
+
for (const ch of checks) console.log(checkLine(ch.status, ch.name, ch.detail));
|
|
157
|
+
console.log(ok ? ` ${c.ok(c.bold('everything this needs is there'))}` : ` ${c.bad(c.bold('something this needs is missing'))}${c.dim(' (the ✗ lines say what)')}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(process.argv[1])) {
|
|
161
|
+
const arg = (name) => { const i = process.argv.indexOf(`--${name}`); return i > 0 ? process.argv[i + 1] : null; };
|
|
162
|
+
const cfg = loadConfig();
|
|
163
|
+
for (const p of configProblems()) console.log(checkLine('fail', 'config', p));
|
|
164
|
+
const nodes = arg('node') ? cfg.nodes.filter((n) => n.id === arg('node')) : cfg.nodes;
|
|
165
|
+
if (!nodes.length) { console.log(`no node ${arg('node') ?? ''} in ${cfg.__configFile ?? 'the configuration'}`); process.exit(2); }
|
|
166
|
+
let allOk = configProblems().length === 0;
|
|
167
|
+
for (const node of nodes) {
|
|
168
|
+
const r = await runChecks(node, { rpc: clientFor(node, cfg) });
|
|
169
|
+
printChecks(`node "${node.id}" (${node.label ?? ''})`, r);
|
|
170
|
+
allOk &&= r.ok;
|
|
171
|
+
}
|
|
172
|
+
process.exit(allOk ? 0 : 1);
|
|
173
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Replay whole blocks from a live node through server/chain/tx.js and compare every field against the
|
|
3
|
+
// node's own verbose decoding. A decoder is only as good as the last time it was checked against Core.
|
|
4
|
+
//
|
|
5
|
+
// node scripts/decode-check.js [--node <id>] [--blocks N] [--from <height>] [--heights h1,h2,...]
|
|
6
|
+
//
|
|
7
|
+
// Fetches each block twice, as raw bytes (getblock <hash> 0) and as Core's JSON (getblock <hash> 2),
|
|
8
|
+
// decodes the bytes, and reports every disagreement plus the count of each script type seen. Read-only
|
|
9
|
+
// RPC, through the same client and lane the monitor uses.
|
|
10
|
+
import { loadConfig } from '../server/config.js';
|
|
11
|
+
import { RpcClient } from '../server/rpc/client.js';
|
|
12
|
+
import { decodeBlock } from '../server/chain/tx.js';
|
|
13
|
+
|
|
14
|
+
const arg = (name, def) => { const i = process.argv.indexOf(`--${name}`); return i > 0 ? process.argv[i + 1] : def; };
|
|
15
|
+
const cfg = loadConfig();
|
|
16
|
+
const node = cfg.nodes.find((n) => n.id === arg('node', null)) ?? cfg.nodes[0];
|
|
17
|
+
const quiet = { info() {}, warn() {}, error() {}, debug() {} };
|
|
18
|
+
const rpc = new RpcClient(node, { ...(cfg.rpc ?? {}), ...(node.rpc ?? {}) }, { log: quiet });
|
|
19
|
+
const call = async (method, params, timeoutMs = 120_000) => {
|
|
20
|
+
const [r] = await rpc.batch([{ method, params }], { key: `decode-check:${method}:${params[0]}`, priority: 5, maxWaitMs: 120_000, timeoutMs });
|
|
21
|
+
if (!r.ok) throw new Error(`${method}: ${r.error?.message}`);
|
|
22
|
+
return r.result;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const info = await call('getblockchaininfo', []);
|
|
26
|
+
const count = Math.max(1, Number(arg('blocks', 3)));
|
|
27
|
+
const from = Number(arg('from', info.blocks - count + 1));
|
|
28
|
+
const sat = (btc) => Math.round(btc * 1e8);
|
|
29
|
+
const mismatches = [];
|
|
30
|
+
const types = {};
|
|
31
|
+
let txs = 0, inputs = 0, outputs = 0, bytes = 0, decodeMs = 0;
|
|
32
|
+
const differ = (where, got, want) => { if (got !== want && mismatches.length < 50) mismatches.push(`${where}: decoded ${JSON.stringify(got)}, node ${JSON.stringify(want)}`); return got !== want; };
|
|
33
|
+
|
|
34
|
+
const heights = arg('heights', null) ? arg('heights').split(',').map(Number) : Array.from({ length: count }, (_, i) => from + i);
|
|
35
|
+
for (const h of heights) {
|
|
36
|
+
const hash = await call('getblockhash', [h]);
|
|
37
|
+
const raw = await call('getblock', [hash, 0]);
|
|
38
|
+
const want = await call('getblock', [hash, 2], 300_000);
|
|
39
|
+
const buf = Buffer.from(raw, 'hex');
|
|
40
|
+
const t0 = performance.now();
|
|
41
|
+
const got = decodeBlock(buf, info.chain);
|
|
42
|
+
decodeMs += performance.now() - t0;
|
|
43
|
+
bytes += buf.length;
|
|
44
|
+
for (const k of ['hash', 'version', 'previousblockhash', 'merkleroot', 'time', 'bits', 'nonce', 'size', 'nTx']) differ(`block ${h} ${k}`, got[k], want[k]);
|
|
45
|
+
want.tx.forEach((w, i) => {
|
|
46
|
+
const g = got.tx[i];
|
|
47
|
+
const at = `block ${h} tx ${i} (${w.txid.slice(0, 12)})`;
|
|
48
|
+
txs++;
|
|
49
|
+
for (const k of ['txid', 'hash', 'version', 'size', 'vsize', 'weight', 'locktime']) differ(`${at} ${k}`, g[k], w[k]);
|
|
50
|
+
differ(`${at} vin count`, g.vin.length, w.vin.length);
|
|
51
|
+
w.vin.forEach((wi, j) => {
|
|
52
|
+
const gi = g.vin[j] ?? {};
|
|
53
|
+
inputs++;
|
|
54
|
+
if (wi.coinbase != null) differ(`${at} vin ${j} coinbase`, gi.coinbase, wi.coinbase);
|
|
55
|
+
else {
|
|
56
|
+
differ(`${at} vin ${j} txid`, gi.txid, wi.txid);
|
|
57
|
+
differ(`${at} vin ${j} vout`, gi.vout, wi.vout);
|
|
58
|
+
differ(`${at} vin ${j} scriptSig`, gi.scriptSig?.hex, wi.scriptSig?.hex);
|
|
59
|
+
}
|
|
60
|
+
differ(`${at} vin ${j} sequence`, gi.sequence, wi.sequence);
|
|
61
|
+
differ(`${at} vin ${j} witness`, JSON.stringify(gi.txinwitness ?? null), JSON.stringify(wi.txinwitness ?? null));
|
|
62
|
+
});
|
|
63
|
+
differ(`${at} vout count`, g.vout.length, w.vout.length);
|
|
64
|
+
w.vout.forEach((wo, j) => {
|
|
65
|
+
const go = g.vout[j] ?? { scriptPubKey: {} };
|
|
66
|
+
outputs++;
|
|
67
|
+
types[wo.scriptPubKey.type] = (types[wo.scriptPubKey.type] ?? 0) + 1;
|
|
68
|
+
differ(`${at} vout ${j} value`, go.value_sat, sat(wo.value));
|
|
69
|
+
differ(`${at} vout ${j} n`, go.n, wo.n);
|
|
70
|
+
differ(`${at} vout ${j} hex`, go.scriptPubKey.hex, wo.scriptPubKey.hex);
|
|
71
|
+
differ(`${at} vout ${j} type`, go.scriptPubKey.type, wo.scriptPubKey.type);
|
|
72
|
+
differ(`${at} vout ${j} address`, go.scriptPubKey.address ?? null, wo.scriptPubKey.address ?? null);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`${node.id} (${info.chain}): ${heights.length} blocks (${heights[0]}..${heights[heights.length - 1]}), ${txs} transactions, ${inputs} inputs, ${outputs} outputs, ${(bytes / 1e6).toFixed(1)} MB`);
|
|
78
|
+
console.log(`decoded in ${decodeMs.toFixed(0)} ms (${(bytes / 1e6 / (decodeMs / 1000)).toFixed(1)} MB/s, ${(txs / (decodeMs / 1000)).toFixed(0)} tx/s)`);
|
|
79
|
+
console.log('script types:', JSON.stringify(types));
|
|
80
|
+
console.log(mismatches.length ? `MISMATCHES (${mismatches.length}${mismatches.length >= 50 ? '+' : ''}):\n ${mismatches.join('\n ')}` : 'every field matches the node');
|
|
81
|
+
process.exit(mismatches.length ? 1 : 0);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One source of truth for the test count the docs quote.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists as a script rather than as a grep in a test: the documented
|
|
5
|
+
// count drifted out of sync three times on 2026-09-08, and each time the fix was
|
|
6
|
+
// a sed against a string a previous sed had already rewritten, so the replacement
|
|
7
|
+
// matched nothing and the docs went on asserting a stale number. The previous
|
|
8
|
+
// guard compared README against AGENTS.md, which only proves two documents are
|
|
9
|
+
// wrong *together* — it never looked at the suite. 183 vs 180 is exactly that
|
|
10
|
+
// failure, found by hand on 2026-09-09.
|
|
11
|
+
//
|
|
12
|
+
// So: count what the suite declares, and make the docs match it.
|
|
13
|
+
//
|
|
14
|
+
// How the count is derived, and why it is the same number `node --test` prints:
|
|
15
|
+
// every test in this repo is declared at module top level with `test('name', …)`.
|
|
16
|
+
// A nested declaration (`t.test(…)`, or an indented `test(`) would make the two
|
|
17
|
+
// numbers disagree, so the scanner reports nested declarations separately and
|
|
18
|
+
// test/doc-counts.test.js fails loudly if any appear — a scanner that silently
|
|
19
|
+
// undercounts is the same class of bug as the sed that matched nothing.
|
|
20
|
+
//
|
|
21
|
+
// node scripts/doc-counts.js # print the number
|
|
22
|
+
// node scripts/doc-counts.js --check # exit 1 if the docs disagree
|
|
23
|
+
// node scripts/doc-counts.js --fix # rewrite the docs' number
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
|
|
30
|
+
const DECL = /^(?:test|test\.skip|test\.todo|test\.only)\s*\(/;
|
|
31
|
+
|
|
32
|
+
/** Test declarations per file: top-level (counted) and nested (a scanner gap). */
|
|
33
|
+
export function scanTests({ root = ROOT, dir = 'test' } = {}) {
|
|
34
|
+
const base = path.join(root, dir);
|
|
35
|
+
const rel = (f) => path.relative(root, f).split(path.sep).join('/'); // keyed with '/', on every platform
|
|
36
|
+
const files = fs.readdirSync(base, { recursive: true, withFileTypes: true })
|
|
37
|
+
.filter((e) => e.isFile() && e.name.endsWith('.test.js'))
|
|
38
|
+
.map((e) => path.join(e.parentPath ?? e.path, e.name))
|
|
39
|
+
.sort();
|
|
40
|
+
const out = { total: 0, nested: [], perFile: {}, files: files.length };
|
|
41
|
+
for (const f of files) {
|
|
42
|
+
let top = 0;
|
|
43
|
+
for (const line of fs.readFileSync(f, 'utf8').split('\n')) {
|
|
44
|
+
const trimmed = line.trimStart();
|
|
45
|
+
if (!DECL.test(trimmed)) continue;
|
|
46
|
+
// Indented means it sits inside another test's body or a helper: a subtest.
|
|
47
|
+
if (/^\s/.test(line)) out.nested.push(`${rel(f)}: ${trimmed.slice(0, 60)}`);
|
|
48
|
+
else top += 1;
|
|
49
|
+
}
|
|
50
|
+
out.perFile[rel(f)] = top;
|
|
51
|
+
out.total += top;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Every place the docs quote a test count. Deliberately narrow ("N tests" /
|
|
57
|
+
// "N unit tests") so a measurement like "1 test failed" in prose is not edited.
|
|
58
|
+
export const DOC_FILES = ['README.md', 'AGENTS.md'];
|
|
59
|
+
|
|
60
|
+
export function docCounts({ root = ROOT } = {}) {
|
|
61
|
+
const found = [];
|
|
62
|
+
for (const rel of DOC_FILES) {
|
|
63
|
+
const text = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
64
|
+
for (const m of text.matchAll(/(\d+)\s+(unit tests|tests)\b/g)) {
|
|
65
|
+
found.push({ file: rel, count: Number(m[1]), match: m[0], index: m.index });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function rewriteDocs(count, { root = ROOT, dry = false } = {}) {
|
|
72
|
+
const changes = [];
|
|
73
|
+
for (const rel of DOC_FILES) {
|
|
74
|
+
const file = path.join(root, rel);
|
|
75
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
76
|
+
const next = text.replace(/(\d+)(\s+(?:unit tests|tests)\b)/g, `${count}$2`);
|
|
77
|
+
if (next !== text) {
|
|
78
|
+
changes.push(rel);
|
|
79
|
+
if (!dry) fs.writeFileSync(file, next);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return changes;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === fs.realpathSync(process.argv[1]);
|
|
86
|
+
if (isMain) {
|
|
87
|
+
const scan = scanTests();
|
|
88
|
+
const mode = process.argv.includes('--check') ? 'check' : process.argv.includes('--fix') ? 'fix' : 'print';
|
|
89
|
+
if (mode === 'print') {
|
|
90
|
+
process.stdout.write(`${scan.total}\n`);
|
|
91
|
+
if (scan.nested.length) process.stderr.write(`warning: ${scan.nested.length} nested test declaration(s) the count cannot see:\n ${scan.nested.join('\n ')}\n`);
|
|
92
|
+
} else {
|
|
93
|
+
const docs = docCounts();
|
|
94
|
+
const wrong = docs.filter((d) => d.count !== scan.total);
|
|
95
|
+
if (mode === 'fix') {
|
|
96
|
+
const touched = rewriteDocs(scan.total);
|
|
97
|
+
process.stdout.write(`${scan.total} declared tests; ${touched.length ? `rewrote ${touched.join(', ')}` : 'docs already correct'}\n`);
|
|
98
|
+
} else if (wrong.length) {
|
|
99
|
+
process.stderr.write(
|
|
100
|
+
`documented test count is wrong. The suite declares ${scan.total} tests; the docs say `
|
|
101
|
+
+ `${wrong.map((w) => `${w.file}: "${w.match}"`).join(', ')}.\n`
|
|
102
|
+
+ 'Fix it with: node scripts/doc-counts.js --fix\n',
|
|
103
|
+
);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
} else {
|
|
106
|
+
process.stdout.write(`docs agree with the suite at ${scan.total}\n`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Regenerate the donation QR in public/index.html (the <svg class="abqr">) from the address below,
|
|
3
|
+
# with OpenCV's encoder, and check the modules written decode back to the address. Run by hand when
|
|
4
|
+
# the address changes; the SVG is committed, so nothing is generated at run time or in the browser.
|
|
5
|
+
import cv2, numpy as np, re, sys
|
|
6
|
+
ADDR = 'bc1q249cv27lc2q7y0x53vkczgfvvgsjzhwxwv42gc'
|
|
7
|
+
img = cv2.QRCodeEncoder.create().encode(ADDR)
|
|
8
|
+
n = img.shape[0]; size = n + 8
|
|
9
|
+
d = ''.join(f'M{x+4} {y+4}h1v1h-1z' for y in range(n) for x in range(n) if img[y, x] == 0)
|
|
10
|
+
m = np.full((size * 8, size * 8), 255, np.uint8)
|
|
11
|
+
for mm in re.finditer(r'M(\d+) (\d+)h1v1h-1z', d):
|
|
12
|
+
x, y = int(mm.group(1)), int(mm.group(2)); m[y*8:(y+1)*8, x*8:(x+1)*8] = 0
|
|
13
|
+
data, _, _ = cv2.QRCodeDetector().detectAndDecode(m)
|
|
14
|
+
if data != ADDR: sys.exit(f'the QR does not decode to the address: {data!r}')
|
|
15
|
+
svg = f'<svg class="abqr" viewBox="0 0 {size} {size}" width="132" height="132" role="img" aria-label="QR code of the donation address" shape-rendering="crispEdges"><rect width="{size}" height="{size}" fill="#fff"/><path fill="#000" d="{d}"/></svg>'
|
|
16
|
+
p = 'public/index.html'; s = open(p).read()
|
|
17
|
+
s2 = re.sub(r'<svg class="abqr".*?</svg>', svg, s, count=1, flags=re.S)
|
|
18
|
+
if s2 == s and svg not in s: sys.exit('no <svg class="abqr"> in public/index.html to replace')
|
|
19
|
+
open(p, 'w').write(s2)
|
|
20
|
+
print(f'{n}x{n} modules, decodes to {data}')
|