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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// A build worker (server/chain/index/build.js). Two jobs, chosen per message:
|
|
2
|
+
//
|
|
3
|
+
// scan one blk/rev file pair -> the file's index rows, partitioned by the key's first byte into
|
|
4
|
+
// 256 buffers (a counting sort), so the main thread only appends each to its bucket file
|
|
5
|
+
// sort one bucket file -> sorted, de-duplicated rows plus a sparse index, then the unsorted
|
|
6
|
+
// input is removed
|
|
7
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
8
|
+
import { openSync, readSync, writeSync, closeSync, fstatSync, unlinkSync, renameSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { Reader, readHeader } from '../tx.js';
|
|
11
|
+
import { readChainFile, records, pairBlocksWithUndo, MAGIC } from '../blockfile.js';
|
|
12
|
+
import { blockRows, RowSink, ROW } from './rows.js';
|
|
13
|
+
import { HeightTable } from './heights.js';
|
|
14
|
+
|
|
15
|
+
const { blocksDir, key, heightsBuffer, heightsCapacity, blockRowsPerIndex } = workerData;
|
|
16
|
+
const heights = HeightTable.attach(heightsBuffer, heightsCapacity);
|
|
17
|
+
const xor = Buffer.from(key);
|
|
18
|
+
|
|
19
|
+
function scan(file) {
|
|
20
|
+
const t0 = performance.now();
|
|
21
|
+
const id = String(file).padStart(5, '0');
|
|
22
|
+
const blk = readChainFile(path.join(blocksDir, `blk${id}.dat`), xor);
|
|
23
|
+
const rev = readChainFile(path.join(blocksDir, `rev${id}.dat`), xor);
|
|
24
|
+
const readMs = performance.now() - t0;
|
|
25
|
+
const blocks = [];
|
|
26
|
+
for (const r of records(blk, MAGIC.main, 0, xor)) {
|
|
27
|
+
const rd = new Reader(r.body);
|
|
28
|
+
const h = readHeader(rd);
|
|
29
|
+
blocks.push({ body: r.body, hash: h.hash, previousblockhash: h.previousblockhash, ntx: rd.varint() });
|
|
30
|
+
}
|
|
31
|
+
const pairs = pairBlocksWithUndo(blocks, [...records(rev, MAGIC.main, 32, xor)]);
|
|
32
|
+
const sink = new RowSink(1 << 18);
|
|
33
|
+
const indexed = [];
|
|
34
|
+
let stale = 0, missingUndo = 0;
|
|
35
|
+
blocks.forEach((b, i) => {
|
|
36
|
+
const height = heights.get(b.hash);
|
|
37
|
+
if (height < 0) { stale++; return; } // not on the chain this build covers
|
|
38
|
+
const undo = pairs.get(i);
|
|
39
|
+
if (!undo && height !== 0) { missingUndo++; return; }
|
|
40
|
+
blockRows(b.body, undo ? undo.body : null, height, sink);
|
|
41
|
+
indexed.push(height);
|
|
42
|
+
});
|
|
43
|
+
// counting sort by the first key byte: 256 buffers the main thread appends as they are
|
|
44
|
+
const all = sink.bytes();
|
|
45
|
+
const counts = new Uint32Array(257);
|
|
46
|
+
for (let at = 0; at < all.length; at += ROW) counts[all[at] + 1]++;
|
|
47
|
+
for (let b = 0; b < 256; b++) counts[b + 1] += counts[b];
|
|
48
|
+
const out = Buffer.allocUnsafe(all.length);
|
|
49
|
+
const fill = counts.slice(0, 256);
|
|
50
|
+
for (let at = 0; at < all.length; at += ROW) { const b = all[at]; all.copy(out, fill[b]++ * ROW, at, at + ROW); }
|
|
51
|
+
const parts = [];
|
|
52
|
+
for (let b = 0; b < 256; b++) if (counts[b + 1] > counts[b]) parts.push([b, counts[b] * ROW, counts[b + 1] * ROW]);
|
|
53
|
+
const ab = out.buffer.slice(out.byteOffset, out.byteOffset + out.length);
|
|
54
|
+
return {
|
|
55
|
+
msg: { type: 'scanned', file, rows: sink.n, blocks: indexed.length, heights: indexed, stale, missingUndo, parts, readMs, ms: performance.now() - t0, data: ab },
|
|
56
|
+
transfer: [ab],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sortBucket(bucket, dir) {
|
|
61
|
+
const t0 = performance.now();
|
|
62
|
+
const name = (s) => path.join(dir, `bucket-${bucket.toString(16).padStart(2, '0')}${s}`);
|
|
63
|
+
const fd = openSync(name('.unsorted'), 'r');
|
|
64
|
+
const size = fstatSync(fd).size;
|
|
65
|
+
const buf = Buffer.allocUnsafe(size);
|
|
66
|
+
for (let got = 0; got < size;) { const k = readSync(fd, buf, got, size - got, got); if (!k) break; got += k; }
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
const n = size / ROW;
|
|
69
|
+
// order by the next 16 key bits into sub-buckets, then compare the rest numerically:
|
|
70
|
+
// hi = key bytes 3..7 (40 bits), lo = height (24 bits) and position (16 bits)
|
|
71
|
+
const sub = (i) => (buf[i * ROW + 1] << 8) | buf[i * ROW + 2];
|
|
72
|
+
const counts = new Uint32Array(65537);
|
|
73
|
+
for (let i = 0; i < n; i++) counts[sub(i) + 1]++;
|
|
74
|
+
for (let s = 0; s < 65536; s++) counts[s + 1] += counts[s];
|
|
75
|
+
const order = new Uint32Array(n), fill = counts.slice(0, 65536);
|
|
76
|
+
for (let i = 0; i < n; i++) order[fill[sub(i)]++] = i;
|
|
77
|
+
const hi = new Float64Array(n), lo = new Float64Array(n);
|
|
78
|
+
for (let i = 0; i < n; i++) { const at = i * ROW; hi[i] = buf.readUIntBE(at + 3, 5); lo[i] = buf.readUIntBE(at + 8, 3) * 65536 + buf.readUInt16BE(at + 11); }
|
|
79
|
+
for (let s = 0; s < 65536; s++) {
|
|
80
|
+
const part = order.subarray(counts[s], counts[s + 1]);
|
|
81
|
+
if (part.length > 1) part.sort((x, y) => hi[x] - hi[y] || lo[x] - lo[y]);
|
|
82
|
+
}
|
|
83
|
+
const out = Buffer.allocUnsafe(size);
|
|
84
|
+
const sparse = [];
|
|
85
|
+
let w = 0, dupes = 0;
|
|
86
|
+
for (let k = 0; k < n; k++) {
|
|
87
|
+
const at = order[k] * ROW;
|
|
88
|
+
// the same block can be stored twice in blk files; its rows are then identical and kept once
|
|
89
|
+
if (w > 0 && buf.compare(out, w - ROW, w - ROW + 13, at, at + 13) === 0) { dupes++; continue; }
|
|
90
|
+
if ((w / ROW) % blockRowsPerIndex === 0) sparse.push(buf.readBigUInt64BE(at));
|
|
91
|
+
buf.copy(out, w, at, at + ROW);
|
|
92
|
+
w += ROW;
|
|
93
|
+
}
|
|
94
|
+
const idx = Buffer.from(new BigUint64Array(sparse).buffer);
|
|
95
|
+
const write = (file, data) => { const f = openSync(file + '.tmp', 'w'); for (let o = 0; o < data.length;) o += writeSync(f, data, o, data.length - o); closeSync(f); renameSync(file + '.tmp', file); };
|
|
96
|
+
write(path.join(dir, `seg-${bucket.toString(16).padStart(2, '0')}.rows`), out.subarray(0, w));
|
|
97
|
+
write(path.join(dir, `seg-${bucket.toString(16).padStart(2, '0')}.idx`), idx);
|
|
98
|
+
unlinkSync(name('.unsorted'));
|
|
99
|
+
return { msg: { type: 'sorted', bucket, rows: w / ROW, dupes, ms: performance.now() - t0 }, transfer: [] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
parentPort.on('message', (job) => {
|
|
103
|
+
try {
|
|
104
|
+
const r = job.type === 'scan' ? scan(job.file) : sortBucket(job.bucket, job.dir);
|
|
105
|
+
parentPort.postMessage(r.msg, r.transfer);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
parentPort.postMessage({ type: 'error', job, message: err.stack || err.message });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// RAW TRANSACTIONS AND BLOCKS, DECODED HERE (docs/DEFECTS.md: "the explorer pays roughly 3-5x for
|
|
2
|
+
// verbose RPC it re-parses anyway" and "we can read the block files after all").
|
|
3
|
+
//
|
|
4
|
+
// One decoder for two jobs. The explorer asks the node for `getrawtransaction <txid> 2` and
|
|
5
|
+
// `getblock <hash> 2`, and the node spends most of that reply serializing JSON it could have sent as
|
|
6
|
+
// bytes -- measured on the Umbrel, 1,843 ms at verbosity 2 against 373 ms at verbosity 0 for the same
|
|
7
|
+
// block. And an address index has to be built from the block files, which hold exactly these bytes.
|
|
8
|
+
// Both are this module: bytes in, the fields Core's verbose reply carries out, named the way Core
|
|
9
|
+
// names them so a caller can switch source without switching vocabulary.
|
|
10
|
+
//
|
|
11
|
+
// WHAT IT IS NOT. Raw bytes do not say what an input spends: no prevout value, no prevout address, so
|
|
12
|
+
// no fee. Verbosity 2 carries those because Core looks them up in its UTXO/undo data. A caller that
|
|
13
|
+
// needs them still has to find the spent outputs -- from the parent transactions, or from an index
|
|
14
|
+
// built over the chain. Deciding that is the next step; decoding is this one.
|
|
15
|
+
//
|
|
16
|
+
// Pure and dependency-free (Rule 1): node:crypto for SHA-256, everything else by hand. Checked
|
|
17
|
+
// field-for-field against Core's own verbose output on real blocks (test/chain-tx.test.js carries the
|
|
18
|
+
// fixtures; scripts/decode-check.js replays whole blocks from a live node).
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
|
|
21
|
+
const sha256 = (b) => createHash('sha256').update(b).digest();
|
|
22
|
+
export const hash256 = (b) => sha256(sha256(b));
|
|
23
|
+
const rev = (b) => Buffer.from(b).reverse().toString('hex');
|
|
24
|
+
|
|
25
|
+
// --- networks ------------------------------------------------------------
|
|
26
|
+
// the address encodings of each chain Core names in getblockchaininfo.chain
|
|
27
|
+
export const NETWORKS = Object.freeze({
|
|
28
|
+
main: { hrp: 'bc', p2pkh: 0x00, p2sh: 0x05 },
|
|
29
|
+
test: { hrp: 'tb', p2pkh: 0x6f, p2sh: 0xc4 },
|
|
30
|
+
testnet4: { hrp: 'tb', p2pkh: 0x6f, p2sh: 0xc4 },
|
|
31
|
+
signet: { hrp: 'tb', p2pkh: 0x6f, p2sh: 0xc4 },
|
|
32
|
+
regtest: { hrp: 'bcrt', p2pkh: 0x6f, p2sh: 0xc4 },
|
|
33
|
+
});
|
|
34
|
+
const netOf = (network) => NETWORKS[network] ?? NETWORKS.main;
|
|
35
|
+
|
|
36
|
+
// --- reading -------------------------------------------------------------
|
|
37
|
+
export class Reader {
|
|
38
|
+
constructor(buf, pos = 0) { this.buf = buf; this.pos = pos; }
|
|
39
|
+
need(n) {
|
|
40
|
+
if (this.pos + n > this.buf.length) throw new RangeError(`truncated: need ${n} bytes at ${this.pos} of ${this.buf.length}`);
|
|
41
|
+
}
|
|
42
|
+
u8() { this.need(1); return this.buf[this.pos++]; }
|
|
43
|
+
u32() { this.need(4); const v = this.buf.readUInt32LE(this.pos); this.pos += 4; return v; }
|
|
44
|
+
i32() { this.need(4); const v = this.buf.readInt32LE(this.pos); this.pos += 4; return v; }
|
|
45
|
+
u64() {
|
|
46
|
+
this.need(8);
|
|
47
|
+
const v = this.buf.readBigUInt64LE(this.pos); this.pos += 8;
|
|
48
|
+
if (v > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError(`amount ${v} is past the safe integer range`);
|
|
49
|
+
return Number(v);
|
|
50
|
+
}
|
|
51
|
+
varint() {
|
|
52
|
+
const b = this.u8();
|
|
53
|
+
if (b < 0xfd) return b;
|
|
54
|
+
if (b === 0xfd) { this.need(2); const v = this.buf.readUInt16LE(this.pos); this.pos += 2; return v; }
|
|
55
|
+
if (b === 0xfe) return this.u32();
|
|
56
|
+
return this.u64();
|
|
57
|
+
}
|
|
58
|
+
bytes(n) { this.need(n); const v = this.buf.subarray(this.pos, this.pos + n); this.pos += n; return v; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- addresses -----------------------------------------------------------
|
|
62
|
+
const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
63
|
+
export function base58check(version, payload) {
|
|
64
|
+
const body = Buffer.concat([Buffer.from([version]), payload]);
|
|
65
|
+
const full = Buffer.concat([body, hash256(body).subarray(0, 4)]);
|
|
66
|
+
let n = BigInt('0x' + full.toString('hex'));
|
|
67
|
+
let out = '';
|
|
68
|
+
while (n > 0n) { out = B58[Number(n % 58n)] + out; n /= 58n; }
|
|
69
|
+
for (const byte of full) { if (byte !== 0) break; out = '1' + out; }
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
74
|
+
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
75
|
+
function polymod(values) {
|
|
76
|
+
let chk = 1;
|
|
77
|
+
for (const v of values) {
|
|
78
|
+
const top = chk >>> 25;
|
|
79
|
+
chk = ((chk & 0x1ffffff) << 5) ^ v;
|
|
80
|
+
for (let i = 0; i < 5; i++) if ((top >>> i) & 1) chk ^= GEN[i];
|
|
81
|
+
}
|
|
82
|
+
return chk >>> 0;
|
|
83
|
+
}
|
|
84
|
+
// BIP173 for witness v0, BIP350 (bech32m) for v1 and up
|
|
85
|
+
export function segwitAddress(hrp, version, program) {
|
|
86
|
+
const data = [version];
|
|
87
|
+
let acc = 0, bits = 0;
|
|
88
|
+
for (const b of program) {
|
|
89
|
+
acc = (acc << 8) | b; bits += 8;
|
|
90
|
+
while (bits >= 5) { bits -= 5; data.push((acc >>> bits) & 31); }
|
|
91
|
+
}
|
|
92
|
+
if (bits > 0) data.push((acc << (5 - bits)) & 31);
|
|
93
|
+
const expand = [...hrp].map((c) => c.charCodeAt(0) >> 5).concat([0], [...hrp].map((c) => c.charCodeAt(0) & 31));
|
|
94
|
+
const constant = version === 0 ? 1 : 0x2bc830a3;
|
|
95
|
+
const mod = polymod([...expand, ...data, 0, 0, 0, 0, 0, 0]) ^ constant;
|
|
96
|
+
const check = [];
|
|
97
|
+
for (let i = 0; i < 6; i++) check.push((mod >>> (5 * (5 - i))) & 31);
|
|
98
|
+
return hrp + '1' + [...data, ...check].map((d) => CHARSET[d]).join('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- addresses back to scripts -------------------------------------------
|
|
102
|
+
// The inverse of the encoders above, for lookups that start from an address someone typed. Returns
|
|
103
|
+
// the scriptPubKey an address pays, or null for anything that is not a valid address on `network`
|
|
104
|
+
// -- a checksum that fails is null, never a best guess.
|
|
105
|
+
function base58decode(str) {
|
|
106
|
+
let n = 0n;
|
|
107
|
+
for (const ch of str) { const v = B58.indexOf(ch); if (v < 0) return null; n = n * 58n + BigInt(v); }
|
|
108
|
+
let hex = n.toString(16); if (hex.length % 2) hex = '0' + hex;
|
|
109
|
+
const lead = str.match(/^1*/)[0].length;
|
|
110
|
+
return Buffer.concat([Buffer.alloc(lead), n === 0n ? Buffer.alloc(0) : Buffer.from(hex, 'hex')]);
|
|
111
|
+
}
|
|
112
|
+
function bech32decode(addr) {
|
|
113
|
+
const lower = addr.toLowerCase();
|
|
114
|
+
if (addr !== lower && addr !== addr.toUpperCase()) return null; // mixed case is invalid
|
|
115
|
+
const sep = lower.lastIndexOf('1');
|
|
116
|
+
if (sep < 1 || sep + 7 > lower.length || lower.length > 90) return null;
|
|
117
|
+
const hrp = lower.slice(0, sep);
|
|
118
|
+
const data = [];
|
|
119
|
+
for (const ch of lower.slice(sep + 1)) { const v = CHARSET.indexOf(ch); if (v < 0) return null; data.push(v); }
|
|
120
|
+
const expand = [...hrp].map((c) => c.charCodeAt(0) >> 5).concat([0], [...hrp].map((c) => c.charCodeAt(0) & 31));
|
|
121
|
+
const mod = polymod([...expand, ...data]);
|
|
122
|
+
const version = data[0];
|
|
123
|
+
const constant = version === 0 ? 1 : 0x2bc830a3;
|
|
124
|
+
if (mod !== constant) return null;
|
|
125
|
+
let acc = 0, bits = 0;
|
|
126
|
+
const program = [];
|
|
127
|
+
for (const v of data.slice(1, -6)) {
|
|
128
|
+
acc = (acc << 5) | v; bits += 5;
|
|
129
|
+
while (bits >= 8) { bits -= 8; program.push((acc >>> bits) & 255); }
|
|
130
|
+
}
|
|
131
|
+
if (bits >= 5 || ((acc << (8 - bits)) & 255)) return null; // non-zero padding
|
|
132
|
+
return { hrp, version, program: Buffer.from(program) };
|
|
133
|
+
}
|
|
134
|
+
export function addressToScript(address, network = 'main') {
|
|
135
|
+
const net = netOf(network);
|
|
136
|
+
const a = String(address ?? '').trim();
|
|
137
|
+
const sw = a.toLowerCase().startsWith(net.hrp + '1') ? bech32decode(a) : null;
|
|
138
|
+
if (sw) {
|
|
139
|
+
if (sw.hrp !== net.hrp || sw.version > 16 || sw.program.length < 2 || sw.program.length > 40) return null;
|
|
140
|
+
if (sw.version === 0 && sw.program.length !== 20 && sw.program.length !== 32) return null;
|
|
141
|
+
return Buffer.concat([Buffer.from([sw.version === 0 ? 0 : 0x50 + sw.version, sw.program.length]), sw.program]);
|
|
142
|
+
}
|
|
143
|
+
const raw = base58decode(a);
|
|
144
|
+
if (!raw || raw.length !== 25) return null;
|
|
145
|
+
if (!hash256(raw.subarray(0, 21)).subarray(0, 4).equals(raw.subarray(21))) return null;
|
|
146
|
+
const hash = raw.subarray(1, 21);
|
|
147
|
+
if (raw[0] === net.p2pkh) return Buffer.concat([Buffer.from([0x76, 0xa9, 0x14]), hash, Buffer.from([0x88, 0xac])]);
|
|
148
|
+
if (raw[0] === net.p2sh) return Buffer.concat([Buffer.from([0xa9, 0x14]), hash, Buffer.from([0x87])]);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- scripts -------------------------------------------------------------
|
|
153
|
+
// Core's Solver (script/solver.cpp), in its order, with its names.
|
|
154
|
+
const OP_0 = 0x00, OP_PUSHDATA1 = 0x4c, OP_PUSHDATA2 = 0x4d, OP_PUSHDATA4 = 0x4e, OP_1 = 0x51, OP_16 = 0x60;
|
|
155
|
+
const OP_RETURN = 0x6a, OP_DUP = 0x76, OP_EQUAL = 0x87, OP_EQUALVERIFY = 0x88, OP_HASH160 = 0xa9;
|
|
156
|
+
const OP_CHECKSIG = 0xac, OP_CHECKMULTISIG = 0xae;
|
|
157
|
+
|
|
158
|
+
// Walk a script's opcodes; null if a push runs past the end (Core's GetOp failing).
|
|
159
|
+
function ops(s) {
|
|
160
|
+
const out = [];
|
|
161
|
+
let i = 0;
|
|
162
|
+
while (i < s.length) {
|
|
163
|
+
const op = s[i++];
|
|
164
|
+
let len = 0;
|
|
165
|
+
if (op > OP_0 && op < OP_PUSHDATA1) len = op;
|
|
166
|
+
else if (op === OP_PUSHDATA1) { if (i + 1 > s.length) return null; len = s[i]; i += 1; }
|
|
167
|
+
else if (op === OP_PUSHDATA2) { if (i + 2 > s.length) return null; len = s.readUInt16LE(i); i += 2; }
|
|
168
|
+
else if (op === OP_PUSHDATA4) { if (i + 4 > s.length) return null; len = s.readUInt32LE(i); i += 4; }
|
|
169
|
+
if (i + len > s.length) return null;
|
|
170
|
+
out.push({ op, data: len ? s.subarray(i, i + len) : null });
|
|
171
|
+
i += len;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
const validPubkeySize = (k) => (k.length === 33 && (k[0] === 2 || k[0] === 3)) || (k.length === 65 && (k[0] === 4 || k[0] === 6 || k[0] === 7));
|
|
176
|
+
const smallInt = (op) => (op === OP_0 ? 0 : op >= OP_1 && op <= OP_16 ? op - OP_1 + 1 : null);
|
|
177
|
+
|
|
178
|
+
export function classifyScript(s, network = 'main') {
|
|
179
|
+
const net = netOf(network);
|
|
180
|
+
// P2SH first, exactly as Core checks it
|
|
181
|
+
if (s.length === 23 && s[0] === OP_HASH160 && s[1] === 0x14 && s[22] === OP_EQUAL) {
|
|
182
|
+
return { type: 'scripthash', address: base58check(net.p2sh, s.subarray(2, 22)) };
|
|
183
|
+
}
|
|
184
|
+
// witness programs: a version opcode then one direct push of 2..40 bytes, 4..42 in all
|
|
185
|
+
if (s.length >= 4 && s.length <= 42 && (s[0] === OP_0 || (s[0] >= OP_1 && s[0] <= OP_16)) && s[1] + 2 === s.length) {
|
|
186
|
+
const version = smallInt(s[0]);
|
|
187
|
+
const program = s.subarray(2);
|
|
188
|
+
if (version === 1 && program.length === 2 && program[0] === 0x4e && program[1] === 0x73) {
|
|
189
|
+
return { type: 'anchor', address: segwitAddress(net.hrp, 1, program) };
|
|
190
|
+
}
|
|
191
|
+
if (version === 0 && program.length === 20) return { type: 'witness_v0_keyhash', address: segwitAddress(net.hrp, 0, program) };
|
|
192
|
+
if (version === 0 && program.length === 32) return { type: 'witness_v0_scripthash', address: segwitAddress(net.hrp, 0, program) };
|
|
193
|
+
if (version === 1 && program.length === 32) return { type: 'witness_v1_taproot', address: segwitAddress(net.hrp, 1, program) };
|
|
194
|
+
if (version !== 0) return { type: 'witness_unknown', address: segwitAddress(net.hrp, version, program) };
|
|
195
|
+
return { type: 'nonstandard', address: null };
|
|
196
|
+
}
|
|
197
|
+
// OP_RETURN followed by nothing but pushes
|
|
198
|
+
if (s.length >= 1 && s[0] === OP_RETURN) {
|
|
199
|
+
const rest = ops(s.subarray(1));
|
|
200
|
+
if (rest && rest.every((o) => o.op <= OP_16)) return { type: 'nulldata', address: null };
|
|
201
|
+
}
|
|
202
|
+
// pay to a bare public key
|
|
203
|
+
if ((s.length === 35 || s.length === 67) && s[0] === s.length - 2 && s[s.length - 1] === OP_CHECKSIG && validPubkeySize(s.subarray(1, s.length - 1))) {
|
|
204
|
+
return { type: 'pubkey', address: null };
|
|
205
|
+
}
|
|
206
|
+
if (s.length === 25 && s[0] === OP_DUP && s[1] === OP_HASH160 && s[2] === 0x14 && s[23] === OP_EQUALVERIFY && s[24] === OP_CHECKSIG) {
|
|
207
|
+
return { type: 'pubkeyhash', address: base58check(net.p2pkh, s.subarray(3, 23)) };
|
|
208
|
+
}
|
|
209
|
+
// bare multisig: OP_m <keys...> OP_n OP_CHECKMULTISIG, 1 <= m <= n, every key a valid size
|
|
210
|
+
if (s.length >= 1 && s[s.length - 1] === OP_CHECKMULTISIG) {
|
|
211
|
+
const o = ops(s);
|
|
212
|
+
if (o && o.length >= 4) {
|
|
213
|
+
const m = smallInt(o[0].op), n = smallInt(o[o.length - 2].op);
|
|
214
|
+
const keys = o.slice(1, -2);
|
|
215
|
+
if (m != null && n != null && m >= 1 && keys.length === n && m <= n
|
|
216
|
+
&& keys.every((k) => k.data && k.op < OP_PUSHDATA1 && validPubkeySize(k.data))) {
|
|
217
|
+
return { type: 'multisig', address: null };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { type: 'nonstandard', address: null };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --- transactions --------------------------------------------------------
|
|
225
|
+
/**
|
|
226
|
+
* Decode one transaction from `r` (a Reader positioned at its first byte). Returns Core's verbose
|
|
227
|
+
* shape -- txid, hash, version, size, vsize, weight, locktime, vin, vout -- with amounts in SATOSHIS
|
|
228
|
+
* as integers (`value_sat`), never floating BTC: a decoder is not the place to lose a satoshi.
|
|
229
|
+
*/
|
|
230
|
+
export function readTx(r, network = 'main') {
|
|
231
|
+
const start = r.pos;
|
|
232
|
+
const version = r.i32();
|
|
233
|
+
let segwit = false;
|
|
234
|
+
// BIP144: a zero where the input count belongs, then a flag of 1
|
|
235
|
+
if (r.buf[r.pos] === 0x00 && r.buf[r.pos + 1] === 0x01) { segwit = true; r.pos += 2; }
|
|
236
|
+
const afterMarker = r.pos;
|
|
237
|
+
const nin = r.varint();
|
|
238
|
+
const vin = [];
|
|
239
|
+
for (let i = 0; i < nin; i++) {
|
|
240
|
+
const prev = r.bytes(32), vout = r.u32();
|
|
241
|
+
const script = r.bytes(r.varint());
|
|
242
|
+
const sequence = r.u32();
|
|
243
|
+
const coinbase = vout === 0xffffffff && prev.every((b) => b === 0);
|
|
244
|
+
vin.push(coinbase
|
|
245
|
+
? { coinbase: script.toString('hex'), sequence }
|
|
246
|
+
: { txid: rev(prev), vout, scriptSig: { hex: script.toString('hex') }, sequence });
|
|
247
|
+
}
|
|
248
|
+
const nout = r.varint();
|
|
249
|
+
const vout = [];
|
|
250
|
+
for (let n = 0; n < nout; n++) {
|
|
251
|
+
const value_sat = r.u64();
|
|
252
|
+
const script = r.bytes(r.varint());
|
|
253
|
+
const { type, address } = classifyScript(script, network);
|
|
254
|
+
vout.push({ value_sat, n, scriptPubKey: { hex: script.toString('hex'), type, ...(address ? { address } : {}) } });
|
|
255
|
+
}
|
|
256
|
+
const witnessStart = r.pos;
|
|
257
|
+
if (segwit) {
|
|
258
|
+
for (const input of vin) {
|
|
259
|
+
const items = r.varint();
|
|
260
|
+
const stack = [];
|
|
261
|
+
for (let k = 0; k < items; k++) stack.push(r.bytes(r.varint()).toString('hex'));
|
|
262
|
+
if (stack.length) input.txinwitness = stack;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const witnessEnd = r.pos;
|
|
266
|
+
const locktime = r.u32();
|
|
267
|
+
const end = r.pos;
|
|
268
|
+
const full = r.buf.subarray(start, end);
|
|
269
|
+
// the txid hashes the transaction WITHOUT marker, flag and witness
|
|
270
|
+
const base = segwit
|
|
271
|
+
? Buffer.concat([r.buf.subarray(start, start + 4), r.buf.subarray(afterMarker, witnessStart), r.buf.subarray(witnessEnd, end)])
|
|
272
|
+
: full;
|
|
273
|
+
const size = full.length, weight = base.length * 3 + size;
|
|
274
|
+
return {
|
|
275
|
+
txid: rev(hash256(base)), hash: rev(hash256(full)),
|
|
276
|
+
version, size, vsize: Math.ceil(weight / 4), weight, locktime, vin, vout,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function decodeTx(hexOrBuf, network = 'main') {
|
|
281
|
+
const buf = Buffer.isBuffer(hexOrBuf) ? hexOrBuf : Buffer.from(String(hexOrBuf), 'hex');
|
|
282
|
+
const r = new Reader(buf);
|
|
283
|
+
const tx = readTx(r, network);
|
|
284
|
+
if (r.pos !== buf.length) throw new RangeError(`${buf.length - r.pos} trailing bytes after the transaction`);
|
|
285
|
+
return tx;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --- blocks --------------------------------------------------------------
|
|
289
|
+
export function readHeader(r) {
|
|
290
|
+
const start = r.pos;
|
|
291
|
+
const version = r.i32();
|
|
292
|
+
const previousblockhash = rev(r.bytes(32));
|
|
293
|
+
const merkleroot = rev(r.bytes(32));
|
|
294
|
+
const time = r.u32(), bits = r.u32(), nonce = r.u32();
|
|
295
|
+
return {
|
|
296
|
+
hash: rev(hash256(r.buf.subarray(start, start + 80))),
|
|
297
|
+
version, previousblockhash, merkleroot, time, bits: bits.toString(16).padStart(8, '0'), nonce,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function decodeBlock(hexOrBuf, network = 'main') {
|
|
302
|
+
const buf = Buffer.isBuffer(hexOrBuf) ? hexOrBuf : Buffer.from(String(hexOrBuf), 'hex');
|
|
303
|
+
const r = new Reader(buf);
|
|
304
|
+
const header = readHeader(r);
|
|
305
|
+
const n = r.varint();
|
|
306
|
+
const tx = [];
|
|
307
|
+
for (let i = 0; i < n; i++) tx.push(readTx(r, network));
|
|
308
|
+
if (r.pos !== buf.length) throw new RangeError(`${buf.length - r.pos} trailing bytes after the block`);
|
|
309
|
+
return { ...header, size: buf.length, nTx: n, tx };
|
|
310
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// The block being built, assembled HERE from the mempool the node already gave us.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS EXISTS. The next-block card used to come from `getblocktemplate`, which costs the
|
|
4
|
+
// operator's node 1.3-1.5 s of its single RPC thread and 1.79 MB per call (nextblock.js measured
|
|
5
|
+
// it on height 966265). That is a real tax on someone else's hardware, paid once a minute, for
|
|
6
|
+
// data we can derive: mempool.space does not call it either -- its mempool-blocks.ts makes no
|
|
7
|
+
// Core RPC call at all and builds templates from the mempool it already holds.
|
|
8
|
+
//
|
|
9
|
+
// WHAT MADE IT POSSIBLE, and a claim in nextblock.js that was true of the wrong node. That module
|
|
10
|
+
// says `getrawmempool` verbose "returns vsize and fees.base with no depends and no ancestorcount",
|
|
11
|
+
// so the dependency graph had to come from the template. That was measured against the
|
|
12
|
+
// experimental build. Measured against Bitcoin Core on an Umbrel, 2026-09-13, 29,742 entries:
|
|
13
|
+
//
|
|
14
|
+
// depends[] present, 24,796 entries in a package
|
|
15
|
+
// ancestorcount present, with ancestorsize and fees.ancestor
|
|
16
|
+
// descendantcount present, with descendantsize and fees.descendant
|
|
17
|
+
// chunkweight present, with fees.chunk <- Core's own cluster linearization
|
|
18
|
+
//
|
|
19
|
+
// So Core publishes the whole graph, and more: `fees.chunk` over `chunkweight` is the feerate of
|
|
20
|
+
// the chunk the cluster-mempool linearization put this transaction in, which is the number Core's
|
|
21
|
+
// own miner sorts by. We do not have to reimplement the block assembler -- mempool.space wrote a
|
|
22
|
+
// Rust engine for that because it targets nodes that publish less. We sort by the node's own
|
|
23
|
+
// answer and take packages in order.
|
|
24
|
+
//
|
|
25
|
+
// THIS MODULE IS PURE. It takes a getrawmempool(true) reply and returns an object shaped like a
|
|
26
|
+
// getblocktemplate reply, so summarizeTemplate, templateCells, packagesFromTemplate and
|
|
27
|
+
// blockEconomy consume it unchanged. That is the whole seam: no consumer knows the difference.
|
|
28
|
+
//
|
|
29
|
+
// TWO CONVERSIONS THAT ARE EASY TO GET WRONG, and wrong silently:
|
|
30
|
+
// 1. `depends` in getrawmempool are TXIDs. In getblocktemplate they are INDICES into the
|
|
31
|
+
// transactions array, and packagesFromTemplate walks them as indices. Passed through as
|
|
32
|
+
// strings they are not integers, every `Number.isInteger(d)` test fails, and the package
|
|
33
|
+
// analysis reports every transaction as a singleton -- a wrong answer that looks fine.
|
|
34
|
+
// 2. mempool fees are in BTC; template fees are in SATOSHIS. A factor of 1e8 in a feerate is
|
|
35
|
+
// not a subtle graph.
|
|
36
|
+
|
|
37
|
+
const VBYTES = 4; // weight units per virtual byte
|
|
38
|
+
const SATS = 100_000_000;
|
|
39
|
+
/** BTC (a JSON number) to whole satoshis. */
|
|
40
|
+
const toSat = (btc) => (Number.isFinite(btc) ? Math.round(btc * SATS) : 0);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The block subsidy at a height, in satoshis. Integer halving arithmetic -- 50e8 is exactly
|
|
44
|
+
* representable and >> would overflow past 31 bits, so this divides.
|
|
45
|
+
*/
|
|
46
|
+
export function subsidyAt(height, { interval = 210_000 } = {}) {
|
|
47
|
+
if (!Number.isFinite(height) || height < 0) return null;
|
|
48
|
+
const halvings = Math.floor(height / interval);
|
|
49
|
+
if (halvings >= 64) return 0;
|
|
50
|
+
let sat = 50 * SATS;
|
|
51
|
+
for (let i = 0; i < halvings; i++) sat = Math.floor(sat / 2);
|
|
52
|
+
return sat;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The feerate we sort by, in sat/vB.
|
|
57
|
+
*
|
|
58
|
+
* Core's cluster mempool has already worked out which transactions must travel together and in
|
|
59
|
+
* what order: `fees.chunk` over `chunkweight` is that answer. Where it is absent (an older node,
|
|
60
|
+
* or an entry that predates the index) the ancestor feerate is the next best thing, because it is
|
|
61
|
+
* the number that accounts for a child paying for its parents. The transaction's own feerate is
|
|
62
|
+
* the last resort and the one that gets CPFP wrong, so it is only used when nothing else is there.
|
|
63
|
+
*/
|
|
64
|
+
export function orderingRate(e) {
|
|
65
|
+
const chunkW = Number(e?.chunkweight);
|
|
66
|
+
const chunkF = Number(e?.fees?.chunk);
|
|
67
|
+
if (Number.isFinite(chunkW) && chunkW > 0 && Number.isFinite(chunkF)) {
|
|
68
|
+
return toSat(chunkF) / (chunkW / VBYTES);
|
|
69
|
+
}
|
|
70
|
+
const ancSize = Number(e?.ancestorsize);
|
|
71
|
+
const ancFee = Number(e?.fees?.ancestor);
|
|
72
|
+
if (Number.isFinite(ancSize) && ancSize > 0 && Number.isFinite(ancFee)) {
|
|
73
|
+
return toSat(ancFee) / ancSize; // ancestorsize is already vbytes
|
|
74
|
+
}
|
|
75
|
+
const vs = Number(e?.vsize);
|
|
76
|
+
const base = Number(e?.fees?.base);
|
|
77
|
+
if (Number.isFinite(vs) && vs > 0 && Number.isFinite(base)) return toSat(base) / vs;
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Every in-mempool ancestor of `txid`, nearest last, or null when the graph is broken.
|
|
83
|
+
*
|
|
84
|
+
* Depth-first over `depends`. A parent that is not in the map is a parent that has been mined or
|
|
85
|
+
* evicted between the node building the reply and us reading it; the child is then unselectable
|
|
86
|
+
* this round rather than selectable with a missing parent, because a template that contains a
|
|
87
|
+
* child without its parent is not a block anyone could mine.
|
|
88
|
+
*/
|
|
89
|
+
function ancestorsOf(txid, pool, cache) {
|
|
90
|
+
if (cache.has(txid)) return cache.get(txid);
|
|
91
|
+
const out = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const walk = (id) => {
|
|
94
|
+
const e = pool.get(id);
|
|
95
|
+
if (!e) return false;
|
|
96
|
+
for (const p of e.depends ?? []) {
|
|
97
|
+
if (seen.has(p)) continue;
|
|
98
|
+
seen.add(p);
|
|
99
|
+
if (!pool.has(p)) return false;
|
|
100
|
+
if (!walk(p)) return false;
|
|
101
|
+
out.push(p); // parents before children
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
105
|
+
const ok = walk(txid);
|
|
106
|
+
const res = ok ? out : null;
|
|
107
|
+
cache.set(txid, res);
|
|
108
|
+
return res;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Assemble the next block from a getrawmempool(true) reply.
|
|
113
|
+
*
|
|
114
|
+
* Greedy over the node's own ordering feerate, taking each transaction with its unselected
|
|
115
|
+
* ancestors as one indivisible package -- which is what makes a child carry its parent into the
|
|
116
|
+
* block rather than being stranded above it.
|
|
117
|
+
*
|
|
118
|
+
* `reserveWeight` is the coinbase transaction's room. Core reserves 4,000 weight units by default
|
|
119
|
+
* (-blockmaxweight leaves it), and a template that fills all 4,000,000 describes a block that
|
|
120
|
+
* could not be mined.
|
|
121
|
+
*/
|
|
122
|
+
export function templateFromMempool(raw, {
|
|
123
|
+
weightLimit = 4_000_000,
|
|
124
|
+
reserveWeight = 4_000,
|
|
125
|
+
height = null,
|
|
126
|
+
previousblockhash = null,
|
|
127
|
+
bits = null,
|
|
128
|
+
version = null,
|
|
129
|
+
at = Date.now(),
|
|
130
|
+
maxTx = 0, // 0 = no cap; a guard for pathological pools
|
|
131
|
+
} = {}) {
|
|
132
|
+
const entries = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
133
|
+
const pool = new Map();
|
|
134
|
+
for (const [txid, e] of Object.entries(entries)) {
|
|
135
|
+
if (!e || typeof e !== 'object') continue;
|
|
136
|
+
const weight = Number(e.weight);
|
|
137
|
+
if (!Number.isFinite(weight) || weight <= 0) continue;
|
|
138
|
+
pool.set(txid, {
|
|
139
|
+
txid,
|
|
140
|
+
weight,
|
|
141
|
+
vsize: Number.isFinite(Number(e.vsize)) ? Number(e.vsize) : Math.ceil(weight / VBYTES),
|
|
142
|
+
feeSat: toSat(Number(e.fees?.base)),
|
|
143
|
+
wtxid: typeof e.wtxid === 'string' ? e.wtxid : null,
|
|
144
|
+
depends: Array.isArray(e.depends) ? e.depends.filter((d) => typeof d === 'string') : [],
|
|
145
|
+
rate: orderingRate(e),
|
|
146
|
+
time: Number.isFinite(Number(e.time)) ? Number(e.time) : null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const budget = Math.max(0, weightLimit - reserveWeight);
|
|
151
|
+
const order = [...pool.values()].sort((a, b) => b.rate - a.rate || a.txid.localeCompare(b.txid));
|
|
152
|
+
const cache = new Map();
|
|
153
|
+
const chosen = []; // txids, parents always before children
|
|
154
|
+
const picked = new Set();
|
|
155
|
+
let used = 0;
|
|
156
|
+
let truncated = false;
|
|
157
|
+
|
|
158
|
+
for (const tx of order) {
|
|
159
|
+
if (picked.has(tx.txid)) continue;
|
|
160
|
+
if (maxTx && chosen.length >= maxTx) { truncated = true; break; }
|
|
161
|
+
const anc = ancestorsOf(tx.txid, pool, cache);
|
|
162
|
+
if (anc == null) continue; // a parent left the pool: not mineable now
|
|
163
|
+
const pkg = [];
|
|
164
|
+
let w = 0;
|
|
165
|
+
for (const id of anc) {
|
|
166
|
+
if (picked.has(id)) continue;
|
|
167
|
+
pkg.push(id);
|
|
168
|
+
w += pool.get(id).weight;
|
|
169
|
+
}
|
|
170
|
+
pkg.push(tx.txid);
|
|
171
|
+
w += tx.weight;
|
|
172
|
+
if (used + w > budget) continue; // keep going: a smaller package may still fit
|
|
173
|
+
for (const id of pkg) { picked.add(id); chosen.push(id); }
|
|
174
|
+
used += w;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// TXIDS TO INDICES. packagesFromTemplate walks `depends` as positions in this same array, so
|
|
178
|
+
// the remap happens once, here, against the array we just built. A parent that was not selected
|
|
179
|
+
// cannot appear -- by construction it cannot happen, since a child is only taken with its
|
|
180
|
+
// ancestors, but a dropped entry would otherwise become index -1 and corrupt the walk.
|
|
181
|
+
const pos = new Map(chosen.map((id, i) => [id, i]));
|
|
182
|
+
const transactions = chosen.map((id) => {
|
|
183
|
+
const e = pool.get(id);
|
|
184
|
+
return {
|
|
185
|
+
txid: id,
|
|
186
|
+
hash: e.wtxid ?? id,
|
|
187
|
+
fee: e.feeSat,
|
|
188
|
+
weight: e.weight,
|
|
189
|
+
vsize: e.vsize,
|
|
190
|
+
depends: e.depends.map((d) => pos.get(d)).filter((i) => Number.isInteger(i)),
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const feesSat = transactions.reduce((n, t) => n + t.fee, 0);
|
|
195
|
+
const sub = subsidyAt(height);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
// the getblocktemplate field names, so every existing consumer reads this unchanged
|
|
199
|
+
height,
|
|
200
|
+
previousblockhash,
|
|
201
|
+
weightlimit: weightLimit,
|
|
202
|
+
sizelimit: null,
|
|
203
|
+
sigoplimit: null,
|
|
204
|
+
coinbasevalue: sub == null ? null : sub + feesSat,
|
|
205
|
+
mintime: null,
|
|
206
|
+
bits,
|
|
207
|
+
version,
|
|
208
|
+
curtime: Math.floor(at / 1000),
|
|
209
|
+
transactions,
|
|
210
|
+
// ...and our own provenance, so nothing downstream has to guess where this came from
|
|
211
|
+
assembledLocally: true,
|
|
212
|
+
source: 'getrawmempool',
|
|
213
|
+
poolSize: pool.size,
|
|
214
|
+
selected: transactions.length,
|
|
215
|
+
weightUsed: used,
|
|
216
|
+
reserveWeight,
|
|
217
|
+
truncated,
|
|
218
|
+
subsidySat: sub,
|
|
219
|
+
feesSat,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The honest note that travels with locally-assembled templates. */
|
|
224
|
+
export const LOCAL_TEMPLATE_NOTE = 'the block being built is assembled here from getrawmempool(true), '
|
|
225
|
+
+ 'which the monitor already reads for the mempool view: transactions are taken in the order of '
|
|
226
|
+
+ "the node's own cluster-mempool chunk feerate, each with its unselected ancestors, until the "
|
|
227
|
+
+ 'weight budget is spent. It costs the node no extra call. It is this software\'s reconstruction '
|
|
228
|
+
+ "of what a miner would choose, not the node's own getblocktemplate reply, so it can differ at "
|
|
229
|
+
+ 'the margin -- sigop limits and policy the mempool does not publish are not modelled.';
|