Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/two-mode-provenance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ jobs:
scripts/build_sn39_rotation_manifest.py \
scripts/build_sn39_release_manifest.py \
scripts/finalize_sn39_public_release.py \
scripts/migrate_sn39_status_stream.py \
scripts/publish_sn39_validator_status.py \
scripts/run_sn39_public_reproduction.py \
scripts/sn39_hotkey_rotation_operator.py \
Expand Down Expand Up @@ -92,6 +93,7 @@ jobs:
scripts/build_sn39_rotation_manifest.py \
scripts/build_sn39_release_manifest.py \
scripts/finalize_sn39_public_release.py \
scripts/migrate_sn39_status_stream.py \
scripts/publish_sn39_validator_status.py \
scripts/run_sn39_public_reproduction.py \
scripts/sn39_hotkey_rotation_operator.py \
Expand Down Expand Up @@ -134,7 +136,10 @@ jobs:
scaffold/publisher/tests/test_legacy_chain_guard.py \
scaffold/publisher/tests/test_mechanism_weightset.py \
scaffold/publisher/tests/test_weight_vector_scope.py \
tests/thin/test_validator.py
tests/thin/test_validator.py \
tests/thin/test_status_sanitization.py \
tests/thin/test_status_stream_contract.py \
tests/thin/test_status_stream_migration.py
- name: Reproduction imports
run: |
"$SN39_VENV/bin/python" - <<'PY'
Expand Down
3 changes: 3 additions & 0 deletions config/validator-mainnet-sn39.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ require_completed_launch_for_broadcast = true

[logs]
jsonl = "/var/log/cathedral-validator/validator-events.jsonl"
# Public status reads this fixed allowlist projection. The raw journal above
# carries hotkeys and caller-supplied fields and remains private.
status_jsonl = "/var/log/cathedral-validator/validator-status.jsonl"
6 changes: 4 additions & 2 deletions deploy/sn39/cathedral-sn39-public-status.service
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ After=cathedral-validator-sn39.service
# pre-cutover publisher: the live host runs its own status publisher against
# the current validator's event stream, and that unit must stay in place until
# step 8 of the staged cutover replaces it.
ConditionPathExists=/var/log/cathedral-validator/validator-events.jsonl
ConditionPathExists=/var/log/cathedral-validator/validator-status.jsonl

[Service]
Type=oneshot
Expand Down Expand Up @@ -47,7 +47,9 @@ SystemCallArchitectures=native
RestrictAddressFamilies=AF_UNIX
CapabilityBoundingSet=
AmbientCapabilities=
ReadOnlyPaths=/var/log/cathedral-validator/validator-events.jsonl
# The raw event journal remains private. This unit sees only the strict
# fixed-field projection produced by the validator.
ReadOnlyPaths=/var/log/cathedral-validator/validator-status.jsonl
ReadOnlyPaths=/var/lib/cathedral-public-evidence/index.json
# Optional. release.json exists only once a root-signed release has been
# sealed, and it is absent on the host today. An unprefixed ReadOnlyPaths= on a
Expand Down
4 changes: 3 additions & 1 deletion deploy/sn39/cathedral-sn39-release-launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ def _child_environment(
"PYTHONNOUSERSITE": "1",
}
if mode in {"preflight", "launch", "continuous", "reconcile"}:
environment["CATHEDRAL_VALIDATOR_JSONL_GROUP"] = "cathedral-validator-log"
# The separate public publisher reads only the fixed-field status
# projection. Never grant its group access to the raw event journal.
environment["CATHEDRAL_VALIDATOR_STATUS_GROUP"] = "cathedral-validator-log"
if release_sha is not None:
environment["CATHEDRAL_SN39_RELEASE_SHA"] = release_sha
if launch_config_sha256 is not None:
Expand Down
2 changes: 1 addition & 1 deletion deploy/sn39/cathedral-validator-sn39-reconcile.service
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ User=cathedral-validator
# /var/log/cathedral-validator from group cathedral-validator-log to group
# cathedral-validator the first time it ran, silently revoking the group read
# that cathedral-sn39-public-status.service depends on. The status publisher
# would then stop being able to read validator-events.jsonl, and because it is
# would then stop being able to read validator-status.jsonl, and because it is
# a oneshot timer the failure surfaces only as a status page that stopped
# updating.
Group=cathedral-validator-log
Expand Down
6 changes: 4 additions & 2 deletions deploy/sn39/cathedral-validator-sn39.service
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ User=cathedral-validator
# Primary group is cathedral-validator-log, NOT cathedral-validator, and the
# difference from the launch and reconcile units is deliberate. LogsDirectory=
# below creates /var/log/cathedral-validator 0750 owned by this process's
# primary group, and cathedral-sn39-public-status.service reads
# validator-events.jsonl out of it using SupplementaryGroups=cathedral-validator-log.
# primary group, and cathedral-sn39-public-status.service reads the sanitized
# validator-status.jsonl projection out of it using the reader group. The raw
# validator-events.jsonl journal remains 0600 and is never group-readable.
# Setting this to cathedral-validator to match the one-shots would leave the
# status publisher unable to read the stream it exists to publish. The one-shots
# write their own LogsDirectory that nothing else reads, so they have no reason
# to widen their primary group.
Group=cathedral-validator-log
SupplementaryGroups=cathedral-validator-evidence
Environment=HOME=/var/lib/cathedral-validator
Environment=CATHEDRAL_VALIDATOR_STATUS_GROUP=cathedral-validator-log
# Belt and braces. Conflicts= only acts on units systemd is asked to start;
# it cannot stop a writer someone launched by hand. This refuses to start
# at all while any known SN39 writer is active, because the legacy writer
Expand Down
58 changes: 43 additions & 15 deletions docs/SN39_LAUNCH_CUTOVER_20260726.md
Original file line number Diff line number Diff line change
Expand Up @@ -526,19 +526,45 @@ Apply in this order. Each step is independently reversible.
6. **Verify the publisher emits v2** with the exact required field set, using
the assertion command in section 3 against port 8012. Do not continue until
it prints `OK`. This is the gate for step 7.
7. **Install the validator release** with the bumped pins. Provenance can only
7. **Stop every validator writer and privatize both legacy raw journals.**
Prove every unit named by `WRITER_UNITS` in
`scripts/migrate_sn39_status_stream.py` is inactive, then run the reviewed
script from the immutable release as root:

```sh
/usr/bin/python3.12 -I -E -s \
"$release/scripts/migrate_sn39_status_stream.py"
stat -c '%a %U:%G %n' \
/var/log/cathedral-validator/validator-events.jsonl \
/var/log/cathedral-validator-launch/validator-events.jsonl 2>/dev/null || true
```

Every existing raw journal must report `600 cathedral-validator:*`. The
script refuses active writers, symlinks, hard links, unexpected ownership,
and modes other than 0600 or the one reviewed legacy mode, 0640. Do not
change modes with a pathname-based `chmod` while a writer is running.
8. **Install the validator release** with the bumped pins. Provenance can only
reach FULL after step 4 has produced at least one manifest stamped
`9540de44...`; before that the pin and the evidence still disagree.
8. **Replace the pre-cutover status publisher.** The live host runs its own
`cathedral-sn39-public-status.service` against the current validator's event
stream. The shipped unit is paired with `cathedral-validator-sn39.service`
and reads `/var/log/cathedral-validator/validator-events.jsonl`, which does
not exist until step 7 has run. Install it only after step 7, and confirm it
actually ran: its `ConditionPathExists=` makes systemd SKIP it silently if
the log is absent, so a skipped unit and a working one look identical in
`systemctl status`. Check `journalctl -u cathedral-sn39-public-status` for a
real execution, not just an absence of errors.
9. **Leave the three validator units disabled and inactive** until the launch
9. **Start the selected validator once and require a committed STARTUP.** The
raw journal remains 0600. The validator writes the separate
`/var/log/cathedral-validator/validator-status.jsonl` projection as 0640.
Its STARTUP row must preserve the reviewed `authority` and
`provenance_mode`. An unmatched `STATUS_PUBLICATION_PENDING` means the
raw/status transition was interrupted and every public gate is
`NOT_PROVEN`. Stop the selected validator after this observation. The
launch window remains closed.
10. **Replace the pre-cutover status publisher.** The shipped unit reads only
`/var/log/cathedral-validator/validator-status.jsonl`. It never receives
access to `validator-events.jsonl`. Install it only after step 9 created the
projection, then confirm a real execution in
`journalctl -u cathedral-sn39-public-status`. `ConditionPathExists=` skips a
missing projection without making the unit fail, so `systemctl status`
alone is insufficient. Treat `logs/status.json` as the commit marker:
require `publication.phase=COMMITTED`, verify both published event-view
digests, then re-read the same committed generation. `PENDING`, a digest
mismatch, or a changed generation is `NOT_PROVEN`.
11. **Leave the three validator units disabled and inactive** until the launch
window. Their single-writer guards are unchanged: each names the other SN39
writers in `Conflicts=` and refuses to start via `ExecStartPre` while any of
them is active.
Expand All @@ -550,15 +576,17 @@ Apply in this order. Each step is independently reversible.
| 2 | `rm /etc/sysusers.d/cathedral-sn39-validator.conf`. Accounts already created remain, which is harmless and matches the current host. |
| 3 | `rm /etc/tmpfiles.d/cathedral-sn39-validator.conf`. No ownership was changed, so there is nothing to restore. This is the property proven above. |
| 4 | Reinstall the previous exporter. Evidence returns to stamping `b77c7cf...`, which is wrong but is the current production behavior. |
| 5 | Reinstall the previous publisher revision. It returns to emitting v1, which the pre-cutover validator accepts. Roll back step 7 with it or the validator has nothing it will accept. |
| 7 | Reinstall the previous validator release. The pins revert together because they moved together. |
| 8 | Reinstall the pre-cutover status publisher unit. Nothing else depends on the shipped one. |
| 5 | Reinstall the previous publisher revision. It returns to emitting v1, which the pre-cutover validator accepts. Roll back step 8 with it or the validator has nothing it will accept. |
| 7 | Do not restore group access to a raw journal. Mode 0600 is compatible with the validator and is a permanent privacy hardening. |
| 8 | Reinstall the previous validator release. The pins revert together because they moved together. |
| 9 | Stop the selected validator. A partial public transition remains `NOT_PROVEN`; do not delete the fence to manufacture a PASS. |
| 10 | Either keep the sanitized publisher or stop public publication. Do not reinstall a publisher that requires group access to the raw journal. |

Steps 1 through 3 do not touch the producer and can be done outside a
maintenance window. Step 4 restarts the exporter and should be done between
export cycles.

Steps 5 through 7 are one transaction. Rolling back the validator without
Steps 5, 6, and 8 are one transaction. Rolling back the validator without
rolling back the publisher, or the reverse, reproduces the version mismatch in
section 3 from the opposite direction: a v2 publisher feeding the `98b862b`
validator fails the same exact-field-set comparison. Roll them back together.
Expand Down
45 changes: 45 additions & 0 deletions docs/SN39_MAINNET_RELEASE_20260724.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ stream are published at:
- `https://api.cathedral.computer/v1/evidence/logs/validator-events.jsonl`
- `https://api.cathedral.computer/v1/evidence/index.json`

`logs/status.json` is the commit marker for both event views. A reader must:

1. require `publication.phase == "COMMITTED"`;
2. fetch both event views and verify their published SHA-256 digests;
3. fetch `status.json` again and require the same committed generation.

`PENDING`, a digest mismatch, or a changed generation is `NOT_PROVEN`. The
publisher replaces a fail-closed PENDING marker first, then both views, then
the COMMITTED marker last. A crash at any intermediate write therefore cannot
leave an older PASS beside a newer unmatched event fence.

## Current operational blockers

The software candidate is not the final launch release, and this document does
Expand Down Expand Up @@ -606,6 +617,22 @@ install -D -o root -g root -m 0644 \
systemd-sysusers /etc/sysusers.d/cathedral-sn39-validator.conf
systemd-tmpfiles --create /etc/tmpfiles.d/cathedral-sn39-validator.conf

# Maintenance boundary for the status-stream privacy upgrade. Stop every
# possible writer before changing either existing raw journal from the legacy
# 0640 mode to 0600. The migration verifies stopped services, opens with
# O_NOFOLLOW, validates the descriptor and inode, and covers both the
# continuous and one-shot launch log paths.
systemctl stop \
cathedral-validator-sn39.service \
cathedral-validator-sn39-launch.service \
cathedral-validator-sn39-reconcile.service \
cathedral-thin-validator.service \
cathedral-confidential-validator-sn39.service \
cathedral-confidential-validator.service \
cathedral-validator.service
/usr/bin/python3.12 -I -E -s \
"$release/scripts/migrate_sn39_status_stream.py"

# Provision only the already-registered validator HOTKEY into the service
# account. Run this on a secure interactive console. The source key may prompt
# for its password; neither its mnemonic nor private bytes are printed.
Expand Down Expand Up @@ -916,6 +943,24 @@ sudo /usr/bin/python3 -I -E -s \
`cathedral-validator-sn39.service` and
`cathedral-sn39-public-status.timer`.

Before enabling the timer, require a committed STARTUP in
`/var/log/cathedral-validator/validator-status.jsonl` with the reviewed
`authority` and `provenance_mode`. The raw
`/var/log/cathedral-validator/validator-events.jsonl` must remain 0600. The
public unit reads only the 0640 status projection. If the newest unmatched
row is `STATUS_PUBLICATION_PENDING`, the transition was interrupted and the
published authority and provenance gates remain `NOT_PROVEN` until a clean
process restart emits a new committed STARTUP.

### Status-stream rollback

Stop every validator unit before rolling back. Reverting application bytes
does not justify restoring raw-journal group access. Leave both raw journals
0600. Either retain the sanitized status publisher or stop the public timer.
Never reinstall a legacy publisher that reads `validator-events.jsonl`. A
failed or partial status transition stays `NOT_PROVEN`; deleting its fence is
not a rollback.

The public status card is operational telemetry, not launch authorization. It
reports authority `PASS` only for a fresh observed exact 90/10 submission whose
UIDs were resolved from the signed rewarded and owner/burn hotkeys. A
Expand Down
10 changes: 10 additions & 0 deletions scaffold/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"provenance_burn_hotkey": None,
"provenance_index_max_age_secs": 3600.0,
"jsonl": None, # JSONL event stream file
"status_jsonl": None, # sanitized status projection
}

# config-file keys -> our flat config keys (a [section].key map, flattened)
Expand Down Expand Up @@ -116,6 +117,7 @@
("provenance", "source_revision"): "provenance_source_revision",
("provenance", "burn_hotkey"): "provenance_burn_hotkey",
("logs", "jsonl"): "jsonl",
("logs", "status_jsonl"): "status_jsonl",
}

# env var -> our flat config key
Expand Down Expand Up @@ -145,6 +147,7 @@
"CATHEDRAL_PROVENANCE_SOURCE_REVISION": "provenance_source_revision",
"CATHEDRAL_PROVENANCE_BURN_HOTKEY": "provenance_burn_hotkey",
"CATHEDRAL_VALIDATOR_JSONL": "jsonl",
"CATHEDRAL_VALIDATOR_STATUS_JSONL": "status_jsonl",
}


Expand Down Expand Up @@ -211,6 +214,7 @@ def _resolve_serve_config(ns: argparse.Namespace) -> SimpleNamespace:
"provenance_source_revision",
"provenance_burn_hotkey",
"jsonl",
"status_jsonl",
):
v = getattr(ns, flat, None)
if v is not None:
Expand Down Expand Up @@ -499,6 +503,12 @@ def main(argv: list[str] | None = None) -> int:
default=None,
help="append the stable JSONL event stream to this file",
)
sp.add_argument(
"--status-jsonl",
dest="status_jsonl",
default=None,
help="append the sanitized operational status projection to this file",
)
sp.add_argument(
"--dry-run",
action="store_true",
Expand Down
Loading
Loading