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,160 @@
|
|
|
1
|
+
// A SELF-SIGNED CERTIFICATE, MADE HERE (operator, 2026-09-15: "Can we make the signing process
|
|
2
|
+
// part of the installer to generate per-user certs for their nodes?" -- then "make https the
|
|
3
|
+
// forced default"). Node can make a key pair and sign bytes but cannot write an X.509
|
|
4
|
+
// certificate, and this repository has no dependencies by decision, so the certificate is
|
|
5
|
+
// assembled by hand: a v3 TBSCertificate in DER, signed with ECDSA P-256 over SHA-256, wrapped
|
|
6
|
+
// in PEM. Every install gets its own key and its own certificate, valid for a little over two
|
|
7
|
+
// years, naming the addresses the monitor is reached on (subjectAltName), and the server makes
|
|
8
|
+
// one on first start when no certificate of the operator's own is configured.
|
|
9
|
+
//
|
|
10
|
+
// The shape is the one `openssl req -x509` produces: issuer == subject (CN=blockyard),
|
|
11
|
+
// basicConstraints CA:TRUE, keyUsage digitalSignature+keyCertSign, extKeyUsage serverAuth. A
|
|
12
|
+
// browser warns once per address, as with any self-signed certificate, and then remembers it.
|
|
13
|
+
// Node's own X509Certificate parses the result, which is what the tests hold it to.
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import net from 'node:net';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------- DER, the little that is needed
|
|
20
|
+
const len = (n) => {
|
|
21
|
+
if (n < 0x80) return Buffer.from([n]);
|
|
22
|
+
const b = [];
|
|
23
|
+
for (let x = n; x > 0; x = Math.floor(x / 256)) b.unshift(x & 0xff);
|
|
24
|
+
return Buffer.from([0x80 | b.length, ...b]);
|
|
25
|
+
};
|
|
26
|
+
const tlv = (tag, body) => Buffer.concat([Buffer.from([tag]), len(body.length), body]);
|
|
27
|
+
const seq = (...parts) => tlv(0x30, Buffer.concat(parts));
|
|
28
|
+
const set = (...parts) => tlv(0x31, Buffer.concat(parts));
|
|
29
|
+
const int = (v) => {
|
|
30
|
+
let b = Buffer.isBuffer(v) ? Buffer.from(v) : Buffer.from([v]);
|
|
31
|
+
while (b.length > 1 && b[0] === 0 && !(b[1] & 0x80)) b = b.subarray(1);
|
|
32
|
+
if (b[0] & 0x80) b = Buffer.concat([Buffer.from([0]), b]);
|
|
33
|
+
return tlv(0x02, b);
|
|
34
|
+
};
|
|
35
|
+
const bool = (v) => tlv(0x01, Buffer.from([v ? 0xff : 0]));
|
|
36
|
+
// (arcs as an array, not dotted text: the address-hygiene test reads a four-arc OID as an IPv4 address)
|
|
37
|
+
const oid = (arcs) => {
|
|
38
|
+
const p = Array.isArray(arcs) ? arcs : String(arcs).split('.').map(Number);
|
|
39
|
+
const out = [40 * p[0] + p[1]];
|
|
40
|
+
for (const v of p.slice(2)) {
|
|
41
|
+
const s = [];
|
|
42
|
+
let x = v;
|
|
43
|
+
do { s.unshift(x & 0x7f); x = Math.floor(x / 128); } while (x > 0);
|
|
44
|
+
for (let i = 0; i < s.length - 1; i++) s[i] |= 0x80;
|
|
45
|
+
out.push(...s);
|
|
46
|
+
}
|
|
47
|
+
return tlv(0x06, Buffer.from(out));
|
|
48
|
+
};
|
|
49
|
+
const utf8 = (s) => tlv(0x0c, Buffer.from(s, 'utf8'));
|
|
50
|
+
const octet = (b) => tlv(0x04, b);
|
|
51
|
+
const bits = (b, unused = 0) => tlv(0x03, Buffer.concat([Buffer.from([unused]), b]));
|
|
52
|
+
const utc = (d) => {
|
|
53
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
54
|
+
return tlv(0x17, Buffer.from(`${p(d.getUTCFullYear() % 100)}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}Z`, 'ascii'));
|
|
55
|
+
};
|
|
56
|
+
const ctx = (n, body, constructed = true) => tlv((constructed ? 0xa0 : 0x80) | n, body);
|
|
57
|
+
|
|
58
|
+
// an IP address as the bytes subjectAltName wants: four for v4, sixteen for v6
|
|
59
|
+
export function ipBytes(addr) {
|
|
60
|
+
const kind = net.isIP(addr);
|
|
61
|
+
if (kind === 4) return Buffer.from(addr.split('.').map(Number));
|
|
62
|
+
if (kind !== 6) return null;
|
|
63
|
+
let a = addr;
|
|
64
|
+
// an embedded v4 tail (::ffff:192.0.2.1) becomes its two hextets
|
|
65
|
+
const m = a.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/);
|
|
66
|
+
if (m) { const b = m[2].split('.').map(Number); a = `${m[1]}${((b[0] << 8) | b[1]).toString(16)}:${((b[2] << 8) | b[3]).toString(16)}`; }
|
|
67
|
+
const [head, tail = ''] = a.split('::');
|
|
68
|
+
const hs = head ? head.split(':') : [], ts = tail ? tail.split(':') : [];
|
|
69
|
+
const fill = a.includes('::') ? 8 - hs.length - ts.length : 0;
|
|
70
|
+
const hex = [...hs, ...Array(Math.max(0, fill)).fill('0'), ...ts];
|
|
71
|
+
if (hex.length !== 8) return null;
|
|
72
|
+
const out = Buffer.alloc(16);
|
|
73
|
+
hex.forEach((h, i) => out.writeUInt16BE(parseInt(h, 16), i * 2));
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const pem = (label, der) => `-----BEGIN ${label}-----\n${der.toString('base64').replace(/(.{64})/g, '$1\n').replace(/\n$/, '')}\n-----END ${label}-----\n`;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Make a self-signed certificate and its private key.
|
|
81
|
+
* @param {object} [o]
|
|
82
|
+
* @param {string} [o.cn='blockyard'] the common name (issuer and subject alike)
|
|
83
|
+
* @param {string[]} [o.sans=[]] addresses and names the certificate is valid for
|
|
84
|
+
* @param {number} [o.days=825] validity, from `now`
|
|
85
|
+
* @param {number} [o.now=Date.now()]
|
|
86
|
+
* @returns {{ cert: string, key: string, fingerprint: string, notAfter: number, sans: string[] }}
|
|
87
|
+
*/
|
|
88
|
+
export function makeSelfSigned({ cn = 'blockyard', sans = [], days = 825, now = Date.now() } = {}) {
|
|
89
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
|
90
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
91
|
+
const ECDSA_SHA256 = oid([1, 2, 840, 10045, 4, 3, 2]);
|
|
92
|
+
const name = seq(set(seq(oid([2, 5, 4, 3]), utf8(cn))));
|
|
93
|
+
const notBefore = new Date(now - 5 * 60 * 1000); // five minutes of clock skew
|
|
94
|
+
const notAfter = new Date(now + days * 86_400_000);
|
|
95
|
+
const serial = crypto.randomBytes(16); serial[0] &= 0x7f; // positive, 128 bits
|
|
96
|
+
// the names: unique, addresses as bytes, everything else as a DNS name
|
|
97
|
+
const uniq = [...new Set(sans.map((s) => String(s).trim()).filter(Boolean))];
|
|
98
|
+
// [2] dNSName and [7] iPAddress are IMPLICIT: the tag replaces the string's own, so the body is
|
|
99
|
+
// the bare bytes (an IA5String TLV inside read back as '"\u0016\u0009localhost"')
|
|
100
|
+
const names = uniq.map((s) => { const ip = ipBytes(s); return ip ? ctx(7, ip, false) : ctx(2, Buffer.from(s, 'ascii'), false); });
|
|
101
|
+
const ext = (id, critical, body) => seq(oid(id), ...(critical ? [bool(true)] : []), octet(body));
|
|
102
|
+
const extensions = [
|
|
103
|
+
ext([2, 5, 29, 19], true, seq(bool(true))), // basicConstraints CA:TRUE
|
|
104
|
+
ext([2, 5, 29, 15], true, bits(Buffer.from([0x84]), 2)), // keyUsage: digitalSignature, keyCertSign
|
|
105
|
+
ext([2, 5, 29, 37], false, seq(oid([1, 3, 6, 1, 5, 5, 7, 3, 1]))), // extKeyUsage: serverAuth
|
|
106
|
+
...(names.length ? [ext([2, 5, 29, 17], false, seq(...names))] : []), // subjectAltName
|
|
107
|
+
];
|
|
108
|
+
const tbs = seq(
|
|
109
|
+
ctx(0, int(2)), // version 3
|
|
110
|
+
int(serial),
|
|
111
|
+
seq(ECDSA_SHA256),
|
|
112
|
+
name, // issuer
|
|
113
|
+
seq(utc(notBefore), utc(notAfter)),
|
|
114
|
+
name, // subject: the same, which is what "self-signed" means
|
|
115
|
+
spki,
|
|
116
|
+
ctx(3, seq(...extensions)),
|
|
117
|
+
);
|
|
118
|
+
const signature = crypto.sign('sha256', tbs, { key: privateKey, dsaEncoding: 'der' });
|
|
119
|
+
const cert = pem('CERTIFICATE', seq(tbs, seq(ECDSA_SHA256), bits(signature)));
|
|
120
|
+
const key = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
121
|
+
const x = new crypto.X509Certificate(cert);
|
|
122
|
+
return { cert, key, fingerprint: x.fingerprint256, notAfter: Date.parse(x.validTo), sans: uniq };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A name as it compares: an IP by its bytes (Node prints v6 expanded and upper-case), a DNS name lower-case. */
|
|
126
|
+
export const canonName = (s) => { const b = ipBytes(String(s).trim()); return b ? `ip:${b.toString('hex')}` : String(s).trim().toLowerCase(); };
|
|
127
|
+
/** The names a certificate carries, canonical (canonName), IPs and DNS names alike. */
|
|
128
|
+
export function certNames(certPem) {
|
|
129
|
+
const x = new crypto.X509Certificate(certPem);
|
|
130
|
+
return (x.subjectAltName ?? '').split(',').map((s) => s.trim().replace(/^(DNS|IP Address):/, '')).filter(Boolean).map(canonName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The monitor's own certificate under `dir`: made on first use, kept after that, remade when it
|
|
135
|
+
* is within a fortnight of expiry or no longer names one of `mustName`. Returns the files and
|
|
136
|
+
* whether anything was written.
|
|
137
|
+
*/
|
|
138
|
+
export function ensureSelfSigned(dir, { sans = [], mustName = [], days = 825, now = Date.now(), force = false } = {}) {
|
|
139
|
+
const certFile = path.join(dir, 'cert.pem'), keyFile = path.join(dir, 'key.pem');
|
|
140
|
+
let why = null;
|
|
141
|
+
if (force) why = 'asked to';
|
|
142
|
+
else if (!fs.existsSync(certFile) || !fs.existsSync(keyFile)) why = 'none yet';
|
|
143
|
+
else {
|
|
144
|
+
try {
|
|
145
|
+
const x = new crypto.X509Certificate(fs.readFileSync(certFile, 'utf8'));
|
|
146
|
+
const have = certNames(fs.readFileSync(certFile, 'utf8'));
|
|
147
|
+
const missing = mustName.filter((n) => !have.includes(canonName(n)));
|
|
148
|
+
if (Date.parse(x.validTo) - now < 14 * 86_400_000) why = `it expires ${x.validTo}`;
|
|
149
|
+
else if (missing.length) why = `it does not name ${missing.join(', ')}`;
|
|
150
|
+
else if (!x.checkPrivateKey(crypto.createPrivateKey(fs.readFileSync(keyFile, 'utf8')))) why = 'the key does not match it';
|
|
151
|
+
} catch (err) { why = `it cannot be read (${err.message})`; }
|
|
152
|
+
}
|
|
153
|
+
if (!why) return { certFile, keyFile, made: false, why: null };
|
|
154
|
+
const made = makeSelfSigned({ sans: [...new Set([...sans, ...mustName])], days, now });
|
|
155
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
156
|
+
const write = (file, text) => { const tmp = `${file}.tmp`; fs.writeFileSync(tmp, text, { mode: 0o600 }); fs.renameSync(tmp, file); };
|
|
157
|
+
write(keyFile, made.key);
|
|
158
|
+
write(certFile, made.cert);
|
|
159
|
+
return { certFile, keyFile, made: true, why, fingerprint: made.fingerprint, notAfter: made.notAfter, sans: made.sans };
|
|
160
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Server-side formatting. Deliberately small: the browser has its own copy of
|
|
2
|
+
// the display rules (public/js/fmt.js) and the two must not drift in the
|
|
3
|
+
// one place that matters -- the ETA shape, which this project already prints as
|
|
4
|
+
// DD:HH:MM:SS in its own status line.
|
|
5
|
+
|
|
6
|
+
// DD:HH:MM:SS, as used by the node's downloader ETA and its heartbeat `uptime`.
|
|
7
|
+
export function formatEta(totalSeconds) {
|
|
8
|
+
if (totalSeconds == null || !Number.isFinite(totalSeconds) || totalSeconds < 0) return null;
|
|
9
|
+
const s = Math.floor(totalSeconds);
|
|
10
|
+
const d = Math.floor(s / 86400);
|
|
11
|
+
const h = Math.floor((s % 86400) / 3600);
|
|
12
|
+
const m = Math.floor((s % 3600) / 60);
|
|
13
|
+
const sec = s % 60;
|
|
14
|
+
return `${pad(d, 2)}:${pad(h, 2)}:${pad(m, 2)}:${pad(sec, 2)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pad(n, w) { return String(n).padStart(w, '0'); }
|
|
18
|
+
|
|
19
|
+
// Decimal units, matching how the node prints sizes ("4.0KB" for 4096 bytes).
|
|
20
|
+
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
21
|
+
export function formatBytes(n, dp = 1) {
|
|
22
|
+
if (n == null || !Number.isFinite(n)) return 'n/a';
|
|
23
|
+
let v = Math.abs(n);
|
|
24
|
+
let i = 0;
|
|
25
|
+
while (v >= 1000 && i < UNITS.length - 1) { v /= 1000; i += 1; }
|
|
26
|
+
return `${(n < 0 ? '-' : '')}${v.toFixed(i === 0 ? 0 : dp)} ${UNITS[i]}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# BlockYard -- multi-user web monitor for a Bitcoin Core node.
|
|
2
|
+
#
|
|
3
|
+
# Install:
|
|
4
|
+
# sudo cp systemd/blockyard.service /etc/systemd/system/
|
|
5
|
+
# sudo systemctl daemon-reload && sudo systemctl enable --now blockyard
|
|
6
|
+
#
|
|
7
|
+
# Read-only by design: this unit needs no access to the node's datadir beyond the
|
|
8
|
+
# log file and the RPC cookie, and the app performs no node writes unless
|
|
9
|
+
# BLOCKYARD_ENABLE_ACTIONS=1 plus an explicit BLOCKYARD_ACTIONS list are set.
|
|
10
|
+
#
|
|
11
|
+
# Nothing in this file is machine- or person-specific on purpose: it is committed,
|
|
12
|
+
# so real usernames and host addresses belong in config/local.json, which is
|
|
13
|
+
# gitignored. Two lines below are therefore placeholders you must edit.
|
|
14
|
+
[Unit]
|
|
15
|
+
Description=BlockYard - Bitcoin node monitor
|
|
16
|
+
Documentation=file:///storage/blockyard/AGENTS.md
|
|
17
|
+
After=network-online.target
|
|
18
|
+
Wants=network-online.target
|
|
19
|
+
StartLimitIntervalSec=600
|
|
20
|
+
StartLimitBurst=5
|
|
21
|
+
|
|
22
|
+
[Service]
|
|
23
|
+
Type=simple
|
|
24
|
+
# EDIT ME: the account that may read <datadir>/<chain>/.cookie and the node log.
|
|
25
|
+
# Cookie authentication is unusable without that read access, so a wrong user here
|
|
26
|
+
# shows up as a permanently-offline node rather than a clear error.
|
|
27
|
+
User=blockyard
|
|
28
|
+
Group=blockyard
|
|
29
|
+
WorkingDirectory=/storage/blockyard
|
|
30
|
+
# Name the interpreter, do not go looking for it. Measured on this box 2026-09-09:
|
|
31
|
+
# /usr/bin/node is v18.19.1 -- below package.json engines ">=22", and it starts
|
|
32
|
+
# far enough to fail on syntax, which reads like a code bug.
|
|
33
|
+
# /usr/local/bin/node is a symlink into a root-owned private runtime; that home is
|
|
34
|
+
# mode 0700, so for any User= but root it does not exist -- and
|
|
35
|
+
# /usr/local/bin is FIRST on systemd's default PATH.
|
|
36
|
+
# `/usr/bin/env node` therefore resolves either to a runtime that is too old or to
|
|
37
|
+
# nothing. Pin PATH for child processes and give ExecStart an absolute path; generate
|
|
38
|
+
# the path once with `node -v` on the target and paste it in, since this file is
|
|
39
|
+
# committed and a per-machine interpreter belongs in a drop-in, not here.
|
|
40
|
+
#Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin
|
|
41
|
+
#ExecStart=/absolute/path/to/node server/main.js
|
|
42
|
+
ExecStart=/usr/bin/env node server/main.js
|
|
43
|
+
# always, not on-failure. The outage this box actually had -- service gone, nothing on
|
|
44
|
+
# the port, no error line anywhere in the log, no journal entry because nothing was
|
|
45
|
+
# supervising it -- is exactly the case on-failure cannot see: an exit 0, or a stop that
|
|
46
|
+
# leaves no trace. With Restart=always the exit status and signal at least reach the
|
|
47
|
+
# journal, which is the difference between "it died" and "it died of 9/KILL at 08:35".
|
|
48
|
+
Restart=always
|
|
49
|
+
RestartSec=10
|
|
50
|
+
KillSignal=SIGTERM
|
|
51
|
+
# SIGTERM triggers the durable-shutdown path (history + sessions are saved with
|
|
52
|
+
# tmp+fsync+rename), so give it room to finish rather than SIGKILL mid-flush.
|
|
53
|
+
TimeoutStopSec=45
|
|
54
|
+
|
|
55
|
+
# BINDING. Which address to serve is a per-machine decision, so it lives in
|
|
56
|
+
# config/local.json (`server.hosts`), not here -- that file is gitignored and the
|
|
57
|
+
# app refuses to start if a configured address is absent at boot, rather than
|
|
58
|
+
# silently falling back to a wildcard. A previous revision of this file embedded a
|
|
59
|
+
# real LAN address here AND had two Environment= assignments concatenated onto one
|
|
60
|
+
# line, which silently discarded BLOCKYARD_PORT; both are fixed by not putting
|
|
61
|
+
# machine facts in a committed file in the first place.
|
|
62
|
+
#
|
|
63
|
+
# Consequences worth remembering: binding a single non-loopback address means
|
|
64
|
+
# 127.0.0.1 on the host itself stops working (curl checks must use that address),
|
|
65
|
+
# and clients that reach the box over a tunnel need that tunnel's address listed.
|
|
66
|
+
Environment=BLOCKYARD_PORT=21000
|
|
67
|
+
Environment=BLOCKYARD_NODE_URL=http://127.0.0.1:8331
|
|
68
|
+
Environment=BLOCKYARD_DATADIR=/home/bitcoin/.bitcoin
|
|
69
|
+
Environment=BLOCKYARD_LOGFILE=/home/bitcoin/.bitcoin/debug.log
|
|
70
|
+
Environment=BLOCKYARD_LOG_LEVEL=info
|
|
71
|
+
# ACCESS: hardened out of the box (2026-09-15) -- bound to 127.0.0.1 and sign-in ON;
|
|
72
|
+
# the first start prints the admin password once (or set BLOCKYARD_ADMIN_PASSWORD).
|
|
73
|
+
# To serve a LAN address put it in config/local.json (`server.hosts`) or uncomment
|
|
74
|
+
# the first line; to open the monitor to readers with no account, the second --
|
|
75
|
+
# then anyone who can reach the bound addresses reads it as role "viewer".
|
|
76
|
+
#Environment=BLOCKYARD_BIND=0.0.0.0
|
|
77
|
+
#Environment=BLOCKYARD_AUTH=0
|
|
78
|
+
#
|
|
79
|
+
# Node writes stay off, and while accounts are off they stay off even if actions are
|
|
80
|
+
# enabled -- a write with no identity behind it is not attributable. Enabling needs
|
|
81
|
+
# ALL of: the two lines below, accounts on (above), or the explicit
|
|
82
|
+
# BLOCKYARD_ALLOW_WRITES_WITHOUT_AUTH=1. The second line names the specific actions, so
|
|
83
|
+
# "we turned on writes" is never an unspecific statement.
|
|
84
|
+
#Environment=BLOCKYARD_ENABLE_ACTIONS=1
|
|
85
|
+
#Environment=BLOCKYARD_ACTIONS=testmempoolaccept,savemempool
|
|
86
|
+
|
|
87
|
+
# Modest hardening. NoNewPrivileges plus a read-only /home is enough here: the
|
|
88
|
+
# service must read the node's RPC cookie and log, so a full PrivateTmp/
|
|
89
|
+
# ProtectSystem=strict would fight the thing it is monitoring.
|
|
90
|
+
NoNewPrivileges=true
|
|
91
|
+
ProtectKernelTunables=true
|
|
92
|
+
ProtectKernelModules=true
|
|
93
|
+
ProtectControlGroups=true
|
|
94
|
+
RestrictSUIDSGID=true
|
|
95
|
+
LockPersonality=true
|
|
96
|
+
|
|
97
|
+
StandardOutput=journal
|
|
98
|
+
StandardError=journal
|
|
99
|
+
SyslogIdentifier=blockyard
|
|
100
|
+
|
|
101
|
+
[Install]
|
|
102
|
+
WantedBy=multi-user.target
|