Express API behind sysnode.info. Aggregates Syscoin Core RPC + Blockbook + masternode telemetry + a small authenticated subsystem for governance proposal drafts, vote reminders, and "Pay-with-Pali" collateral flows.
The public dashboard is served by sysnode-info; this repo is the backend half of that stack.
Public, unauthenticated routes (read-only). Canonical URL casing is
lowercase, matching the historical https://syscoin.dev/mnstats
convention and the existing /govlist. Express's default routing is
case-insensitive, so legacy camelCase callers (/mnStats, /mnList,
…) continue to work at the route layer; the bundled nginx config
(deploy/nginx/sysnode.conf.example) uses a case-insensitive regex
match for the same reason, so external bookmarks/clients with mixed
casing keep working through the same-origin proxy. New integrations
should use lowercase.
GET /mnstats— chain + market + masternode summary, refreshed byservices/sysMain.jsGET /mncount— historical masternode count seriesGET /mnlist— freshmasternode_listRPC passthroughPOST /mnsearch— paginated/searched view over the in-memory tracker snapshotPOST /govlist— active + historical governance proposals (Syscoin Coregobject list)GET /api/privacy/v1/pools/:poolId/snapshot— opt-in anonymous Classic ETH pool snapshots for local wallet verification; see configuration and protocol.GET /api/privacy/v1/relayers— separately opt-in anonymous Classic ETH relayer discovery; see source checks and endpoint boundaries.
Authenticated routes (cookie + CSRF, same-site):
/auth/*— registration, verification, login, session, delete account/vault— encrypted per-user blob (notification prefs, proposal drafts; one row per user, conditional GET/PUT with ETag)/gov/*— masternode lookup, vote relay, vote receipts (/gov/mns/lookup,/gov/vote,/gov/receipts, …)/gov/proposals/*— governance proposal wizard, submissions, collateral PSBT
- Node.js 26.12.1 (pinned in
.nvmrcand CI; engines allow patched Node 26 releases from 26.12.1, below 27) - A reachable
syscoindon mainnet or testnet, with RPC enabled - SMTP server for verification emails and vote reminders (or
MAIL_TRANSPORT=logfor dry-run) - SQLite 3 (via
better-sqlite3, no separate install; native module compiles atnpm install)
Optional, used only for the Pali PSBT collateral path:
- A Blockbook instance for the same network as the RPC node (
https://blockbook.syscoin.org/for mainnet,https://blockbook-dev.syscoin.org/for testnet)
git clone https://github.com/syscoin/sysnode-backend.git
cd sysnode-backend
nvm install && nvm use # exact version from .nvmrc
npm ci
cp .env.example .env # then edit — see .env.example for inline docs
npm run dev # nodemon on :3001
npm test # full jest suite (~830 cases)
npm run audit:prod # fail on critical production dependency issuesnpm run audit:prod is wired into CI and fails on high or critical production
vulnerabilities. The pinned syscoinjs-lib dependency uses ethers v6; the backend
does not call ethers directly. Remaining low/moderate findings include the
transitive Ethereum proof stack and other application dependencies. Review the
current audit output rather than applying npm audit fix --force, which can
replace the transaction library with an incompatible version.
pm2 is intentionally not an application dependency. If you use it for process
supervision, install it globally or through your host image, as shown in the
single-host deployment notes below.
All configuration is via environment variables. .env.example is the source of truth and carries inline rationale for every field. The short form:
| Variable | Purpose |
|---|---|
PORT, BASE_URL |
Where the server listens, and the public URL baked into email links |
CORS_ORIGIN, FRONTEND_URL |
SPA origin for credentialed CORS and verification-link base |
TRUST_PROXY |
Reverse-proxy hop count (or CIDR) so req.ip is the real client |
SYSNODE_DB_PATH |
Path to the SQLite file (auto-created) |
SYSNODE_AUTH_PEPPER |
32-byte hex secret; required in production |
SMTP_*, MAIL_FROM, MAIL_TRANSPORT |
Mail delivery; MAIL_TRANSPORT=log prints to stdout |
SYSCOIN_RPC_HOST, SYSCOIN_RPC_PORT |
RPC endpoint (default 127.0.0.1:8370) |
SYSCOIN_RPC_COOKIE_PATH |
Preferred — absolute path to Core's .cookie for same-host deployments |
SYSCOIN_RPC_USER, SYSCOIN_RPC_PASS |
Fallback static creds for remote RPC nodes |
SYSCOIN_NETWORK, SYSCOIN_BLOCKBOOK_URL |
Enables the Pay-with-Pali collateral PSBT path |
Automatic, SHA-pinned deployment on the existing persistent host is documented in
deploy/automation/README.md. It runs only after
successful default-branch CI and preserves the live environment and database.
Real voting-key custody should use a same-origin deployment for the authenticated API surface. Serve the SPA and proxy /auth, /vault, and /gov from the same public HTTPS origin:
https://sysnode.info/ -> sysnode-info build
https://sysnode.info/auth/* -> sysnode-backend
https://sysnode.info/vault/* -> sysnode-backend
https://sysnode.info/gov/* -> sysnode-backend
This keeps the sid and csrf cookies host-only with Secure; SameSite=Lax, and lets the SPA read the csrf cookie from the same host before mirroring it into X-CSRF-Token. Do not deploy the real-key auth/vault/voting surface as sysnode.info plus a cross-site API host.
For production, set at least:
NODE_ENV=production
FRONTEND_URL=https://sysnode.info
CORS_ORIGIN=https://sysnode.info
TRUST_PROXY=1 # or the exact trusted proxy/CIDR for your edge
SYSNODE_AUTH_PEPPER=<32-byte-hex-secret>Production startup refuses non-secure cookies, non-HTTPS FRONTEND_URL, or a credentialed CORS origin that differs from FRONTEND_URL.
A working same-origin nginx vhost lives at deploy/nginx/sysnode.conf.example. The example is the canonical reference, not a drop-in: copy it to /etc/nginx/conf.d/sysnode.conf, replace sysnode.example.com with your real hostname, then run sudo certbot --nginx -d <hostname> to obtain a Let's Encrypt cert and let Certbot rewrite the listen 443 ssl block. Any TLS terminator that preserves Host and forwards X-Forwarded-Proto: https will work — the example is nginx because that's what we run, not because nginx is special.
Both apps emit Strict-Transport-Security from their own code: the backend via helmet's defaults (max-age=31536000; includeSubDomains), the frontend via the security-header map in sysnode-info/server.js. Do not add add_header Strict-Transport-Security at the edge — duplicating it produces two response headers, which is noisy in audits even though browsers only honour the first per RFC 6797 §8.1. The default Certbot snippet at /etc/letsencrypt/options-ssl-nginx.conf ships with HSTS enabled; comment that line out (or override per-vhost) when standing up a new box, otherwise every response will carry it twice.
Owning HSTS in code means any deployer — behind nginx, Caddy, a managed load balancer, or directly on a TLS-terminating Node — gets HSTS without having to remember to add it at their edge.
On production hosts, expose only SSH and the TLS terminator publicly. The Node processes on :3000 and :3001 are implementation details behind nginx and should not be reachable directly from the internet. If the app processes bind all interfaces, enforce this with the host firewall or cloud security group:
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseDo not allow 3000/tcp or 3001/tcp on production. The /auth, /vault, /gov, /mnstats, /mncount, /mnlist, /mnsearch, and /govlist paths should be reachable only through the same-origin nginx vhost described above. Before syscoind is running, the backend is expected to fail closed if SYSCOIN_RPC_COOKIE_PATH is missing; nginx will return 502 for backend routes rather than exposing partial authenticated functionality.
The backend supports both authentication modes and picks cookie over static when both are configured (with a one-line warning at boot). Cookie auth is zero-secret-management: syscoind rewrites the cookie on every restart, and the backend picks up the new token automatically via a 401-driven replay. Use it for any deployment where the backend runs on the same host as syscoind.
For remote RPC nodes, either configure rpcauth= in syscoin.conf and use the static SYSCOIN_RPC_USER / SYSCOIN_RPC_PASS here, or mount the cookie file via a secure channel.
These steps stand up sysnode-backend + sysnode-info on one Ubuntu box that already runs syscoind. HTTP-only; intended for staging and testing, not production. Everything installs into the user's home directory — no sudo required on most steps (a couple of optional hardening steps do need it; they are clearly marked).
The commands below target Ubuntu 24.04 LTS with Node 26.12.1. Port layout: backend :3001, frontend :3000. For an existing production installation, follow the runtime migration requirements in the deployment guide before restarting either service.
Use the exact Node version in .nvmrc; the operating system's default Node package may be older. The package engine requires Node ≥ 26.12.1, < 27. Node 20, 22 and 24 are not qualified for this revision.
# Install the pinned runtime if it is not already available
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 26.12.1 && nvm use 26.12.1
# Use a user-local npm prefix so global installs don't need sudo
mkdir -p ~/.npm-global ~/.local/bin
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$PATH"' >> ~/.bashrc
export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$PATH"
npm install -g pm2 serveEmail delivery (account verification, password-change notices, vote reminders, proposal status updates) goes through any SMTP provider you choose — SMTP2GO, SendGrid, Brevo, Postmark, Mailgun, AWS SES, or a corporate mail relay all work identically from the app's perspective. Pick one, verify a sender domain you control on their dashboard, and note its SMTP host / port / username / password. You'll paste those into .env in the next section.
Governance reminders use a fresh, same-tip Core snapshot, not the market/dashboard
cache. SYSCOIN_NETWORK must be mainnet or testnet; the service verifies the
configured network's genesis, sync state and governance parameters before sending.
Reads abort after a 20-second overall budget and a moving tip gets one retry.
The two reminder buckets are 24–72 hours and at most 24 hours before estimated
payout-trigger selection, using Core's 150-second block projection and its RPC
maturity-block count. Reminders stop once the maturity height is reached. This
is not a claim that Core refuses later proposal votes.
Only valid proposal objects with a supported calendar window are included.
Timestamp fields must be integer Unix seconds with 0 <= start < end <= 253402300799
(year 9999). This notification/presentation bound does not change Core validity:
malformed or millisecond-looking on-chain dates are excluded, never reinterpreted.
Payment eligibility follows Core's two-hour window tolerance. Vote receipts are
matched to the governance object ID, not its separate data hash. A locally
recorded relayed/confirmed vote on any eligible proposal suppresses both buckets;
votes made through other tools may be unknown, so email copy does not assert that
the recipient has not voted. Preferences and their existing defaults are unchanged.
The log deduplicates by user, next-superblock height and bucket; one in-process tick runs at a time to avoid overlap while SMTP is pending. A snapshot older than 60 seconds stops further deliveries until a fresh tick. SMTP and SQLite are not one atomic transaction: a crash or log failure after SMTP accepts a message can still cause a retry, so this is best-effort delivery, not exactly-once mail.
Governance vote delivery distinguishes rejection from uncertainty. Each Core RPC
has a 10-second transport deadline and a vote batch has a 30-second total deadline.
No timed-out vote is automatically retried: a response lost after submission is
reported as submission_unknown, not as a successful relay or a definite failure.
Entries the batch never invoked are not_submitted and do not create receipts.
Response totals are accepted, rejected (definite only), unknown, and
notSubmitted. A valid Core JSON-RPC rejection remains definite, including when
delivered with HTTP 500.
An uncertain receipt records the latest requested intent. Another submission is held until explicit reconciliation: the exact time/outcome/signal confirms it; a newer on-chain vote can supersede it with an explicit marker. Older votes do not confirm a newer uncertain intent. Fresh Core reconciliation after the 10-minute grace period can mark a still-unobserved request stale for an explicit retry; elapsed time or a cached/failed read alone cannot do so. Reconciliation uses conditional updates so late reads cannot overwrite a newer request. No vote signatures or private keys are stored in receipt rows, and no database migration is needed for the additional status. Relay completions follow Core's signed-time/outcome ordering (equal-time yes < no < abstain), retaining a newer or higher-outcome uncertain intent rather than presenting an older success as its result. For the same intent, weaker late results cannot overwrite known success. This does not bypass Core's independent receive-time vote cooldown. When the configured receipt store cannot be read, that entry is not submitted: availability must not bypass an unresolved delivery hold. Other independent entries may still proceed. This does not change vote signatures or key custody. Receipt writes after a broadcast remain best-effort: if storage fails, the API still reports uncertainty and the client must retain its hold, but a restart or lost client state cannot rely on a receipt that was never persisted. This is not an exactly-once submission guarantee.
In production the backend refuses to boot unless SMTP_HOST is set (or MAIL_TRANSPORT=log is set explicitly for stdout-only dry-run), so this step is required before the backend will start with NODE_ENV=production.
mkdir -p ~/apps && cd ~/apps
git clone https://github.com/syscoin/sysnode-backend.git
git clone https://github.com/syscoin/sysnode-info.gitcd ~/apps/sysnode-backend
npm ci
# Locate the Core cookie (path depends on the user that runs syscoind)
ls -l ~/.syscoin/.cookie 2>/dev/null || sudo ls -l /root/.syscoin/.cookie
PEPPER=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
COOKIE_PATH=/home/ubuntu/.syscoin/.cookie # adjust to match the ls above
SERVER_IP=$(hostname -I | awk '{print $1}') # or hardcode the public IP
cat > .env <<EOF
PORT=3001
BASE_URL=http://${SERVER_IP}:3001
CORS_ORIGIN=http://${SERVER_IP}:3000
FRONTEND_URL=http://${SERVER_IP}:3000
NODE_ENV=development
TRUST_PROXY=loopback
SYSNODE_DB_PATH=./data/sysnode.db
SYSNODE_AUTH_PEPPER=${PEPPER}
# SMTP — paste your transactional provider's credentials here. Port 465 is
# treated as implicit TLS; any other port (587 is standard) uses STARTTLS.
# MAIL_FROM must be a sender address you have verified at the provider.
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
MAIL_FROM=no-reply@example.com
MAIL_TRANSPORT=smtp
# Syscoin Core RPC (cookie mode, preferred for same-host)
SYSCOIN_RPC_HOST=127.0.0.1
SYSCOIN_RPC_PORT=8370
SYSCOIN_RPC_COOKIE_PATH=${COOKIE_PATH}
SYSCOIN_RPC_LOG_LEVEL=error
# Pay-with-Pali (mainnet)
SYSCOIN_NETWORK=mainnet
SYSCOIN_BLOCKBOOK_URL=https://blockbook.syscoin.org/
EOF
mkdir -p dataThe backend does not load
.envautomatically. It has nodotenvdependency. We load the file with Node's native--env-file=flag (available in the pinned Node runtime), which is why every invocation ofnodefor this repo below passes--env-file=.env.
Cookie file permissions. If the backend user can't read
~/.syscoin/.cookie(different uid thansyscoind), addrpccookieperms=groupto~/.syscoin/syscoin.confand restartsyscoind, then add the backend's user to the syscoin group. The backend's boot log prints the exact errno (ENOENT/EACCES) if it can't read the file.
Quick sanity check — confirms .env is loaded and RPC cookie auth works against Core:
node --env-file=.env -e '
const { client, rpcServices } = require("./services/rpcClient");
rpcServices(client.callRpc).getBlockchainInfo().call()
.then(r => console.log(r.chain, r.blocks, "ibd=" + r.initialblockdownload))
.catch(e => { console.error(e.message); process.exit(1); });
'
# expected: "main <height> ibd=false"REACT_APP_API_BASE is a Create React App build-time variable — it must be set before npm run build or the bundle will keep pointing at the default.
cd ~/apps/sysnode-info
npm ci
SERVER_IP=$(hostname -I | awk '{print $1}')
REACT_APP_API_BASE=http://${SERVER_IP}:3001 npm run buildcd ~/apps/sysnode-backend
pm2 start "node --env-file=.env server.js" --name sysnode-backend
cd ~/apps/sysnode-info
pm2 start "serve -s build -l 3000" --name sysnode-info
pm2 save
pm2 listOptional — survive a full reboot. Requires sudo; skip if you don't have it and just run pm2 resurrect after any reboot:
pm2 startup systemd -u $USER --hp $HOME # prints one sudo line; paste itIf ufw isn't active on the host, your cloud security-group / network-layer rules are what matter — adjust those instead.
sudo ufw status
# If active:
sudo ufw allow 3000/tcp # frontend
sudo ufw allow 3001/tcp # backend API# Backend reachable + RPC cookie auth working (real stats from Core)
curl -s http://<server-ip>:3001/mnstats | head -c 200
# Mail pipeline. Replace TEST_RECIPIENT with an inbox you can open. If SMTP is
# wired up correctly a verification email arrives within seconds — check the
# spam folder too, transactional mail from a brand-new sender domain often
# lands there until reputation builds at the receiver.
cd ~/apps/sysnode-backend
TEST_RECIPIENT=you@example.com node --env-file=.env -e '
const { createMailer } = require("./lib/mailer");
createMailer({ transport: "smtp" }).sendVerification({
to: process.env.TEST_RECIPIENT,
link: process.env.BASE_URL + "/auth/verify?t=smoketest",
}).then(() => console.log("sent to " + process.env.TEST_RECIPIENT))
.catch(e => { console.error("FAILED:", e.message); process.exit(1); });
'Then exercise the UI:
- Open
http://<server-ip>:3000— dashboard loads. - Register a user in the UI with an email address you can open — the verification email should arrive within seconds (check the spam folder too). Click the link to activate the account.
- Go into the governance proposal wizard — the Pay with Pali button should be enabled (assuming your browser has Pali installed and the chain guard verified mainnet).
pm2 stop sysnode-backend sysnode-info
cd ~/apps/sysnode-backend
git pull && npm ci
cd ~/apps/sysnode-info
git pull && npm ci
REACT_APP_API_BASE=http://<server-ip>:3001 npm run build
pm2 restart sysnode-backend sysnode-info| Symptom | Likely cause | Check |
|---|---|---|
Backend exits at boot, failed to read rpc cookie at ... ENOENT |
Wrong SYSCOIN_RPC_COOKIE_PATH |
sudo -u <syscoind-user> cat <path> |
Backend exits at boot, ... EACCES |
Backend user can't read the cookie | Use rpccookieperms=group + usermod -aG |
| Backend rejects RPC with 401 after a Core restart once, then recovers | Expected — cookie rotated, backend replayed with the new one | No action |
| Pay with Pali button disabled | paliChainGuard reports pali_path_chain_mismatch or pali_path_rpc_down |
GET /gov/proposals/network returns a paliPathReason |
| Verification emails never arrive | SMTP creds wrong, sender domain not verified at the provider, or mail filtered into spam | Check backend logs for 5xx SMTP responses, the provider's dashboard for bounces/delivery status, and the recipient's spam folder |
Frontend hits https://syscoin.dev instead of the test backend |
REACT_APP_API_BASE not set at build time |
Rebuild with the env var inline |
Enable /api/sentry/v1/ with the existing SYSCOIN_NETWORK and
SYSCOIN_BLOCKBOOK_URL settings plus SYSCOIN_SENTRY_GENESIS_HASH for the exact
configured network. The Core node must provide protx_capabilities and, for PQ
operators, extended protx_operator_key_info. Missing capability metadata disables
signing; the backend never guesses an activation state from key length.
Run Core with txindex=1 and wait for the transaction index to synchronize before
enabling management: historical registration and transaction readback use
getrawtransaction. Transport errors or an unavailable/syncing index are not
treated as transaction absence and do not demote confirmed operation records.
GET /capabilities and GET /context are rate-limited public chain reads for
wallets. All node-watch and operation endpoints require a verified session;
writes also require the configured exact Origin and the existing CSRF token.
The public API remains /api/sentry/v1/, including Pali's default
https://sysnode.info/api/sentry/v1/context. The proxy example forwards that
path without rewriting. Existing proxies that strip the leading /api are
also supported through the internal /sentry/v1/ prefix. Both forms mount the
same router, session/CSRF checks and rate-limit buckets; the internal prefix is
not a separate public API or a weaker authentication path. Do not expose raw
backend ports or change the wallet's trusted service URL to accommodate a proxy.
The existing live proxy and Core metadata APIs must be verified after rollout;
updating these repository examples alone does not update deployed systems.
No route accepts an RPC URL, RPC method name, or owner/operator private key.
POST /api/sentry/v1/nodes accepts a public watch record
{ network, proTxHash, label?, preserveExisting? }. The optional boolean
preserveExisting: true is intended for batch imports: an existing label for
the same user, network and ProTx hash is retained atomically, even if another
tab saved it during the on-chain lookup. It still validates the registered node
independently before inserting. Omit the option (or use false) to retain the
existing single-node label-update behavior. The response remains the user's
complete watch list. Watching a node grants no signing authority and imports
no private key.
GET /api/sentry/v1/voting-context?network=testnet&proposalHash=<hash>&proTxHashes=<hash1>,<hash2>
is a public, read-only observation for Pali's governance-vote approval. It accepts
at most 20 unique lowercase ProTx hashes and uses the same independently pinned
Core network/genesis check. Its fixed gobject_get call verifies a valid,
non-deleted proposal and returns the on-chain name/URL, not a dapp-provided title.
The optional URL is omitted unless it is an HTTP(S) address without credentials;
this does not reject an otherwise valid proposal with a non-web URI.
The response is { chain, now, proposalHash, proposal: { name, url? }, nodes };
each node has { proTxHash, collateralOutpoint: { txid, vout }, votingKeyId, valid: true }. valid means a confirmed registered identity with a voting key,
not a claim about its hosting provider or operational status. Active PQ voting
is not supported by this legacy ECDSA context and is explicitly rejected.
The request has a 10-second total deadline, at most four concurrent node reads,
and a 64-KiB limit per Core response. It has a separate 12-request-per-minute IP
limit shared across both proxy prefixes, in addition to the normal read limit.
A changed chain tip rejects the combined
observation so the wallet can request a fresh one. This endpoint does not sign,
relay, import or persist anything; the existing /gov/vote relay and encrypted
browser-vault voting path are unchanged.
After deployment, run the anonymous, read-only verifier with independently trusted chain expectations (do not copy the genesis from the service being checked):
npm run verify:sentry-deployment -- \
--origin https://sysnode.info --network mainnet \
--genesis <trusted-genesis-hash> --era legacy-v3Optionally add --pro-tx-hash <known-node-hash> to check the public node-context
route. The command requires HTTPS, rejects redirects, bounds each request to
10 seconds and 128 KiB, and exits nonzero on an HTML/rewritten route, mismatched
chain, missing operations or broken anonymous-auth boundary. A legacy-v3
check also requires PQ preparation to be explicitly false and rejects advertised
PQ operations. --era pq-v4 is an explicit different expectation, not an
automatic fallback. --timeout-ms can set a request deadline up to 30 seconds.
Successful output is JSON with ok: true; no server bodies or credentials are
printed on failure. It sends only unauthenticated GETs to capabilities,
operations and, when requested, context. It does not log in, inspect a user's
history, sign or broadcast. Authenticated writes and a real operator lifecycle
still require their separate rollout validation; this check cannot certify them.
Preparation uses the shared @sidhujag/sysweb3-sentry codec and public fee-account
data. /operations/:id/authorize checks completed wallet authorizations and
returns a fee-signing PSBT. /operations/:id/submit checks the stored immutable
transaction, current authority, fee limit and testmempoolaccept, then records
the exact signed bytes and transaction ID before one broadcast attempt.
GET /operations/:id reconciles uncertain submissions and verifies on-chain
readback. It never creates a replacement transaction. SQLite journals contain
public transaction data only and are isolated by authenticated user ID.
After a network failure, the user may explicitly request
POST /operations/:id/retry with an empty body. The backend checks for the
original transaction first, then revalidates and republishes only the stored
signed bytes if needed. A per-operation cooldown and atomic claim bound retries;
no new signature, new transaction ID, replacement transaction or fee bump occurs.
Run npm test -- --runInBand and npm run test:sentry-codec. Codec integration
uses real legacy/PQ signatures and fee signatures with synthetic RPC fixtures;
it does not submit transactions to a public network.
For the actual legacy consensus smoke test, set SENTRY_CORE_BIN to a directory
containing the metadata-enabled syscoind and syscoin-cli, then run
npm run test:sentry-core. It creates a fresh disconnected regtest and synthetic
wallet, registers a node, signs NEVM add/update/remove requests with the shared
SDK, and requires Core mempool acceptance plus mined registration readback.
The test stops its Core process and prints its isolated temporary data directory
for inspection. It never uses a configured service RPC or an existing wallet.
MIT