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.
Files changed (122) hide show
  1. package/CHANGELOG.md +929 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +4 -0
  4. package/README.md +191 -4
  5. package/SECURITY.md +38 -0
  6. package/bin/blockyard.js +41 -0
  7. package/config/pool-map.json +2620 -0
  8. package/docs/API.md +1577 -0
  9. package/docs/ARCHITECTURE.md +1394 -0
  10. package/docs/AUTO-UPDATE.md +269 -0
  11. package/docs/CONFIGURATION.md +847 -0
  12. package/docs/DEFECTS.md +813 -0
  13. package/docs/EFFECTS-AGENTS.md +448 -0
  14. package/docs/GETTING-STARTED.md +205 -0
  15. package/docs/INSTALL.md +547 -0
  16. package/docs/MEASUREMENTS.md +1401 -0
  17. package/docs/RULES.md +681 -0
  18. package/docs/SECURITY-AUDIT-2026-09-14.md +177 -0
  19. package/docs/SECURITY-AUDIT.md +258 -0
  20. package/docs/SECURITY.md +212 -0
  21. package/docs/TROUBLESHOOTING.md +332 -0
  22. package/docs/USER-GUIDE.md +1262 -0
  23. package/package.json +53 -5
  24. package/public/404.html +9 -0
  25. package/public/css/app.css +2009 -0
  26. package/public/donate-qr.png +0 -0
  27. package/public/index.html +1085 -0
  28. package/public/js/about.js +112 -0
  29. package/public/js/agents.js +1141 -0
  30. package/public/js/app.js +1386 -0
  31. package/public/js/arkanoid.js +806 -0
  32. package/public/js/blockanoid.js +347 -0
  33. package/public/js/blockout.js +347 -0
  34. package/public/js/blockpack.js +428 -0
  35. package/public/js/blockscene3d.js +2830 -0
  36. package/public/js/breakout.js +224 -0
  37. package/public/js/charts.js +635 -0
  38. package/public/js/depthchart.js +315 -0
  39. package/public/js/details3d.js +4342 -0
  40. package/public/js/doom.js +31 -0
  41. package/public/js/dosaudio.js +48 -0
  42. package/public/js/dosgame.js +389 -0
  43. package/public/js/dosio.js +186 -0
  44. package/public/js/dospc.js +1353 -0
  45. package/public/js/dosworker.js +196 -0
  46. package/public/js/explorer.js +405 -0
  47. package/public/js/feepalette.js +149 -0
  48. package/public/js/fmt.js +162 -0
  49. package/public/js/goggles.js +886 -0
  50. package/public/js/kiosk.js +41 -0
  51. package/public/js/login.js +88 -0
  52. package/public/js/markets.js +395 -0
  53. package/public/js/mining.js +1416 -0
  54. package/public/js/panels.js +970 -0
  55. package/public/js/pricechart.js +189 -0
  56. package/public/js/quake.js +20 -0
  57. package/public/js/settings.js +1096 -0
  58. package/public/js/soundcard.js +459 -0
  59. package/public/js/tetris.js +226 -0
  60. package/public/js/tetrust.js +356 -0
  61. package/public/js/tetsound.js +175 -0
  62. package/public/js/theme.js +235 -0
  63. package/public/js/wolf3d.js +22 -0
  64. package/public/js/x86.js +1978 -0
  65. package/public/login.html +33 -0
  66. package/scripts/blockfile-measure.js +156 -0
  67. package/scripts/browser-check.mjs +286 -0
  68. package/scripts/check.js +173 -0
  69. package/scripts/decode-check.js +81 -0
  70. package/scripts/doc-counts.js +109 -0
  71. package/scripts/donate-qr.py +23 -0
  72. package/scripts/dos-bench.js +56 -0
  73. package/scripts/fake-node.js +534 -0
  74. package/scripts/index-bench.js +216 -0
  75. package/scripts/index-benchmark.js +117 -0
  76. package/scripts/index-build.js +40 -0
  77. package/scripts/live-render-check.mjs +89 -0
  78. package/scripts/manage-users.js +132 -0
  79. package/scripts/motion-check.mjs +138 -0
  80. package/scripts/pool-map.js +157 -0
  81. package/scripts/setup.js +432 -0
  82. package/scripts/shots.mjs +278 -0
  83. package/scripts/smoke.sh +327 -0
  84. package/scripts/tls.js +31 -0
  85. package/scripts/ui.js +174 -0
  86. package/server/auth/sessions.js +221 -0
  87. package/server/auth/users.js +243 -0
  88. package/server/chain/blockfile.js +234 -0
  89. package/server/chain/index/build.js +210 -0
  90. package/server/chain/index/heights.js +36 -0
  91. package/server/chain/index/live.js +276 -0
  92. package/server/chain/index/rows.js +145 -0
  93. package/server/chain/index/store.js +154 -0
  94. package/server/chain/index/worker.js +109 -0
  95. package/server/chain/tx.js +310 -0
  96. package/server/collect/gbt.js +229 -0
  97. package/server/collect/logparse.js +765 -0
  98. package/server/collect/logtail.js +189 -0
  99. package/server/collect/markets.js +333 -0
  100. package/server/collect/mining.js +333 -0
  101. package/server/collect/monitor.js +2545 -0
  102. package/server/collect/network.js +295 -0
  103. package/server/collect/nextblock.js +275 -0
  104. package/server/collect/sync.js +386 -0
  105. package/server/config.js +644 -0
  106. package/server/http/api.js +1319 -0
  107. package/server/http/explorer.js +418 -0
  108. package/server/http/games.js +77 -0
  109. package/server/http/server.js +420 -0
  110. package/server/http/sse.js +176 -0
  111. package/server/http/static.js +212 -0
  112. package/server/main.js +673 -0
  113. package/server/netinfo.js +253 -0
  114. package/server/rpc/allowlist.js +130 -0
  115. package/server/rpc/client.js +414 -0
  116. package/server/store/audit.js +148 -0
  117. package/server/store/history.js +220 -0
  118. package/server/store/ledger.js +290 -0
  119. package/server/store/ring.js +173 -0
  120. package/server/tls/selfsigned.js +160 -0
  121. package/server/util/fmt.js +29 -0
  122. 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