Files
archy/apps/qortal

Qortal preparation components — not a catalogue-ready package

This directory contains startup validation and a locally buildable candidate image, not an installable manifest, published image or accepted app entry. Full package, first boot, lifecycle, credential, provenance and operator gates remain. Bounded initialized-fixture API evidence is described separately below.

The initial packaging proposal is Gitea PR164 at f115478fdfd7f561fed4f48d46333b6b204f7253, subsequently updated to 4cc625e4d1aebe57fece3ef5bb8e70f40b372732. The image/nginx/status page are selectively derived from that proposal, retaining the stronger guard here. Acceptance/replacement must follow Archipelago's canonical ngit workflow. Its generic manifest-selected credential reader is deliberately not included: declaring a shared generated-secret filename does not establish exclusive app ownership.

Startup contract

start.sh expects Python 3 and check-settings.py installed as /usr/local/bin/qortal-check-settings, the pinned jar at /usr/local/qortal/qortal.jar, and the companion nginx configuration at /etc/qortal-ui/nginx.conf. The Dockerfile supplies these prerequisites and pins the Temurin base, official JAR and upstream logging configuration. Build locally with podman build -t localhost/archipelago-qortal:6.1.9-archy.1 apps/qortal. The logging file is explicitly selected from the read-only image.

The operator's /qortal/settings.json is never repaired or overwritten. It must be a regular, non-symlink file containing strict UTF-8 JSON, at most 1 MiB, with unique decoded property names. Core accepts some additional comment/trailing comma syntax; this candidate intentionally refuses those extensions rather than risk checking a different interpretation. Any existing settings conversion requires explicit review and preservation of the original, not an automatic migration.

Required explicit booleans:

{
  "localAuthBypassEnabled": false,
  "apiRestricted": true,
  "autoUpdateEnabled": false
}

Other ordinary settings are retained. userPath, apiKey and apiKeyPath are refused even if null: settings redirection or overriding the managed key source would invalidate the startup check. Auto-update is disabled for pinned package provenance; missing flags fail closed rather than silently modifying policy. apiRestricted is not a substitute for route-by-route API-key authorization.

The helper writes a private, exclusive-created runtime snapshot in a fresh mode-0700 temporary directory. Only that snapshot is passed to Core. Edits to the persistent source after validation cannot change this launch's document. Snapshot files are removed after both children stop. SIGTERM/SIGINT are forwarded; the wrapper waits for Core and UI cleanup and preserves the stop exit status.

QORTAL_API_KEY must contain the platform-generated 32-byte hexadecimal key. A new apikey.txt is created mode 0600 without a newline. An existing regular file must be private and match exactly; missing/malformed input, symlinks, FIFOs, exposed permissions or conflicts are refused. The wrapper does not rotate, trim, repair or replace an existing key to make startup succeed. Conflicts require explicit recovery. The environment secret is removed before either child launches, and failures do not print values.

Evidence and remaining exits

  • test_settings.py: eleven tests with multiple adversarial cases, run in a secret-free, network-none, read-only disposable Python container.
  • Actual official Qortal 6.1.9 jar parser paired with the new entrypoint in a bounded disposable JRE/Python image: safe snapshot interpretation, original escaped-name regressions, duplicate keys, redirection, environment-key removal, key preservation, child cleanup and TERM behavior pass.
  • The initial entrypoint/parser probe used stub children; it did not qualify the actual daemon. Subsequent checks below run the actual JAR and nginx.

The source jar's SHA256 was verified as a5fbe763645a53995565c9cb93491e024384431ee29eee38a4146b6a13ed2e87, matching the official release asset and proposal pin. No daemon/network/funds were launched by the parser test. Images used for tests are not release artifacts.

Package-side empty-cache overlay prerequisite

prepare-runtime-jar.py is the package entrypoint boundary for the accepted empty-cache behavior. It never replaces /usr/local/qortal/qortal.jar. It reads the official JAR and replacement class as bounded no-follow regular-file snapshots, verifies both against empty-cache-overlay-contract.json, and creates an exclusive runtime JAR plus rollback receipt in the private runtime directory. It rejects duplicate/missing target entries, signed JARs, changed bytes, noncanonical contract fields, existing outputs and rollback byte/path drift. start.sh runs this preparation before settings, credentials, nginx or Core and launches only the verified runtime JAR.

The checked-in contract deliberately records upstream_provenance_status as pending and has no provenance receipt hash. The Dockerfile therefore copies the helper and contract but does not embed the retained test-only class. Until an independent upstream/package provenance decision supplies and reviews that exact class and changes the contract atomically, startup refuses with exit 78. The retained fixture JAR and class under historical evidence are not silently promoted into package inputs.

Rollback verifies the unchanged official JAR and exact prepared runtime bytes before deleting only the runtime JAR and its receipt. Operator /qortal data is outside this helper. Credential drift remains separately fail-closed through qortal-check-settings; if an existing API key conflicts, the newly created settings snapshot is removed and neither key nor operator data is rewritten. Focused tests exercise nonempty state preservation, prepare/rollback, missing or changed official JAR, changed class, pending provenance, changed runtime rollback refusal and credential conflict through the production helpers.

Actual image fixture — 2026-10-21

tests/apps/qortal/run-disposable.sh IMAGE initialized-fixture passes with the actual Core/nginx processes, nonroot UID/GID 1000:100, network-none, read-only root and disposable storage. It checks missing/wrong API-key denial on the real GET /admin/stop route (403), correct-key stop (200), restricted nginx proxy routes, private managed-key bytes/mode/inode, absence of the environment secret from both children, operator-settings/sentinel preservation across recreation, and refusal of a changed secret without rotation or disclosure. No live node, wallet, peers or funds are involved.

This deliberately does not qualify fresh genesis or bootstrap. With bootstrap=false, the official 6.1.9 JAR fails before API startup with statement is not in batch mode during empty QDN-cache initialization. Default fresh mode preserves that failing reproduction. The separate initialized fixture uses the actual pinned Core genesis processor, commits the empty-cache metadata flag, and uses bootstrap=true, archiveEnabled=false in synthetic settings to avoid the upstream height-below-three rebuild. It invents no chain transactions or signatures; it is nevertheless an explicit prepared database, not a fix, installer migration, first-boot pass or bootstrap qualification. Never use InitializedGenesis.java against operator data.

The image's local tag is not yet admitted by the fixed credential reader; the same-origin privileged credential boundary is also unresolved. No generic credential schema or catalogue manifest is activated here.

Empty-cache candidate — 2026-10-22

A narrow unpublished test-only source patch is retained at tests/apps/qortal/empty-cache.patch. It skips executeBatch() only when no signature entries were added; normal lower-case cache population, completion flag, transaction commit and nonempty batch updates remain unchanged. It does not preseed a database or bypass startup validation.

EmptyCacheRegression.java runs the actual pinned HSQLDB and cache manager against an in-memory synthetic schema. The original class reproduces the empty batch error. The patched class passes empty initialization, idempotence and nonempty latest-signature selection/preservation. No Core or wallet is started by this focused probe.

make-cache-fixture.py creates an exclusive new test JAR from the exact original SHA256, refuses signed or ambiguous originals, and verifies that exactly one class entry differs. It does not overwrite the original or publish a package. The candidate overlay SHA256 is 252b6c42a1ec2e7f35b879676650e44579a8f221665a7a69e421a2a5cba7581a.

With that overlay, run-disposable.sh IMAGE fresh passes actual Core/nginx fresh startup, API-key negatives, restricted UI proxy, graceful stop/recreation, private-key/settings preservation and changed-secret refusal. This used an empty named disposable volume, not InitializedGenesis.java, with external network disabled, 2 GiB memory and zero swap allowance. To supply a test overlay, set QORTAL_FIXTURE_JAR and its exact QORTAL_FIXTURE_JAR_SHA256; neither modifies the image or installed data.

This does NOT make the original official JAR fixed. Source/provenance and independent review, clean compatible packaging, nonempty upgrade/rollback, network/bootstrap/gateway and installed-node qualification remain required. No production Dockerfile, manifest or catalog entry is switched to this overlay.

Still required: independent security review, a bounded credential card or actual app-owned secret design, sanitized manifest/image/UI integration, API denial and Hub/gateway checks, health truthfulness, sizing, upgrade/backup/recovery/rollback, package/source acceptance and signed catalogue admission. Do not deploy these components as a completed app or overwrite a live operator configuration.