Skip to content

Commit d183337

Browse files
committed
publicize dn
1 parent ce9c56f commit d183337

8 files changed

Lines changed: 166 additions & 89 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,5 @@ Notification watches use a bearer capability stored by browser origin. Host a
6262
production PINGME installation on a dedicated origin and do not add third-party
6363
scripts. A shared GitHub Pages origin is suitable only for the public Monitor.
6464

65-
The destructive one-time backend switch is documented in the runbook and runs
66-
as `scripts/switch-backend.sh --fresh`.
65+
Production backend deployments use `scripts/deploy-backend.sh`: `--fresh`
66+
resets all state, while `--upgrade` preserves and backs up the current database.

collector/README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,13 @@ requests require it as `Authorization: Bearer …`. A PATCH replaces the address
9191
list; deleting a subscription removes the watch and its channels.
9292

9393
`GET /live` reports process liveness. `GET /health` reports operational source
94-
and delivery health. Public events include node-local and L1 observations but never
95-
watch/address associations, provider endpoints, or delivery metadata. For an L1
96-
round, `data.nodeEvidence` contains every earlier node offense or completed
97-
inactive epoch matching both the target address and one of the round's target
98-
epochs. This is explicitly correlated node evidence: L1 votes and payloads do
99-
not encode an offense type.
94+
and delivery health. Public events include node-local, Sentinel, and L1
95+
observations but never watch/address associations, provider endpoints, or
96+
delivery metadata. Catch-up and notification-test events remain
97+
endpoint-scoped. For an L1 round, `data.nodeEvidence` contains every earlier
98+
node offense or completed inactive epoch matching both the target address and
99+
one of the round's target epochs. This is explicitly correlated node evidence:
100+
L1 votes and payloads do not encode an offense type.
100101

101102
## Storage and delivery
102103

collector/src/api-server.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717

1818
const WATCHLIST_ID_PATTERN = /^[0-9a-f-]{36}$/i;
1919
const MAX_SEQUENCER_FILTERS = 100;
20+
const PUBLIC_EVENT_SOURCES = ['aztec_node', 'aztec_sentinel', 'ethereum_l1'];
2021

2122
export class CollectorApiServer {
2223
constructor({
@@ -190,7 +191,7 @@ export class CollectorApiServer {
190191
const query = parseEventQuery(url.searchParams, this.network);
191192
const page = this.repository.listEvents({
192193
...query,
193-
sources: ['aztec_node', 'ethereum_l1'],
194+
sources: PUBLIC_EVENT_SOURCES,
194195
});
195196
return writeJson(response, 200, {
196197
schemaVersion: 2,
@@ -206,7 +207,7 @@ export class CollectorApiServer {
206207
}
207208
const network = normalizeNetwork(url.searchParams.get('network') ?? this.network, this.network);
208209
const event = this.repository.getEvent(id);
209-
if (!event || event.network !== network || !['aztec_node', 'ethereum_l1'].includes(event.source)) {
210+
if (!event || event.network !== network || !PUBLIC_EVENT_SOURCES.includes(event.source)) {
210211
throw new HttpError(404, 'event_not_found', 'Event not found');
211212
}
212213
return writeJson(response, 200, {

collector/test/api-server.test.mjs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,45 @@ test('anonymous reads and watch-list creation have tighter independent limits',
595595
assert.equal(secondCreate.body.error.code, 'subscription_rate_limited');
596596
});
597597

598+
test('public journal exposes Sentinel precursor feed and detail', async (t) => {
599+
const repository = healthyRepository();
600+
repository.recordEvent({
601+
id: 'sentinel-first-miss',
602+
network: 'mainnet',
603+
source: 'aztec_sentinel',
604+
type: 'inactivity_first_miss',
605+
severity: 'warning',
606+
title: 'First missed duty observed',
607+
body: 'Precursor evidence, not a registered slash offense.',
608+
data: {
609+
certainty: 'pending',
610+
sequencer: SEQUENCER_A,
611+
epoch: '42',
612+
slot: '1344',
613+
},
614+
observedAt: NOW,
615+
}, [SEQUENCER_A]);
616+
const { baseUrl } = await startApi(t, repository);
617+
618+
const feedResponse = await fetch(
619+
`${baseUrl}/api/v2/events?network=mainnet&address=${SEQUENCER_A}`,
620+
);
621+
assert.equal(feedResponse.status, 200);
622+
const feed = await feedResponse.json();
623+
assert.deepEqual(
624+
feed.data
625+
.filter((event) => event.id === 'sentinel-first-miss')
626+
.map(({ source, type, certainty }) => ({ source, type, certainty })),
627+
[{ source: 'aztec_sentinel', type: 'inactivity_first_miss', certainty: 'pending' }],
628+
);
629+
630+
const detailResponse = await fetch(
631+
`${baseUrl}/api/v2/events/sentinel-first-miss?network=mainnet`,
632+
);
633+
assert.equal(detailResponse.status, 200);
634+
assert.equal((await detailResponse.json()).data.type, 'inactivity_first_miss');
635+
});
636+
598637
function healthyRepository() {
599638
const repository = new OffenseRepository(':memory:');
600639
const [offense] = parseOffenseSnapshot([OFFENSE_A]);

docs/architecture.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ There are three event sources:
4040
- `ethereum_l1` events come from coherently pinned canonical contract reads or
4141
confirmed `Slashed` logs. They are `confirmed`.
4242

43+
All three observation sources are present in the public journal. Authenticated
44+
watch-list event reads are address-scoped views of the same journal, while
45+
catch-up and notification-test artifacts remain endpoint-scoped.
46+
4347
The backend checks node chain, Registry, and Rollup identity before trusting
4448
admin results. It retains prior state when a source fails. An offense can
4549
disappear only after a ready, fresh, non-regressing node cursor advances past

docs/privacy.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ Slashmon never needs validator keys, seed phrases, wallet signatures, or an
1717
Aztec keystore. Protect the database and backups as secrets. The systemd unit
1818
uses a private state directory and `UMask=0077`.
1919

20-
Public API responses expose source health and public-address observations. They
21-
do not expose watch membership, channel endpoints, tokens, or delivery state.
22-
Catch-up and notification-test events remain endpoint-scoped.
20+
Public API responses expose source health and all public-address observation
21+
events from the Aztec node, Sentinel, and Ethereum L1. They do not expose watch
22+
membership, channel endpoints, tokens, or delivery state. Catch-up and
23+
notification-test events remain endpoint-scoped because they describe a
24+
recipient's delivery lifecycle rather than a public network observation.
2325

2426
## Provider exposure
2527

docs/runbook.md

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,6 @@ The supported production deployment is one Node 24 backend under systemd, one
44
SQLite database, and an HTTPS reverse proxy. Never run two backend instances for
55
the same installation.
66

7-
## Switch from an older backend
8-
9-
From a clean checkout of the commit to deploy, run:
10-
11-
```bash
12-
scripts/switch-backend.sh --fresh
13-
```
14-
15-
The script keeps supported credentials and endpoints from
16-
`/etc/slashmon-backend.env`, installs the current commit, removes the old service
17-
unit, and starts `slashmon-backend.service`. It permanently removes the old and
18-
new Slashmon state directories plus `/var/backups/slashmon`; it creates no
19-
database backup and provides no automatic rollback. Run it only when losing all
20-
existing watches, events, delivery state, and checkpoints is intended.
21-
227
## Prepare
238

249
Install Node 24 at `/usr/local/bin/node`, enable Corepack, and create the
@@ -53,7 +38,7 @@ Override `AZTEC_SENTINEL_POLL_INTERVAL_MS`,
5338
`AZTEC_SENTINEL_EPOCH_END_BUFFER_SLOTS`,
5439
`AZTEC_SENTINEL_VALIDATOR_CONCURRENCY`, or
5540
`AZTEC_SENTINEL_VALIDATOR_MAX_RESPONSE_BYTES` only for an observed node or RPC
56-
constraint. The fresh-switch script preserves these settings.
41+
constraint. The deployment script preserves these settings.
5742

5843
Telegram and Web Push are independently optional. Create one Telegram bot or
5944
generate a stable VAPID keypair:
@@ -64,26 +49,27 @@ pnpm --filter @slashmon/backend exec web-push generate-vapid-keys
6449

6550
Changing VAPID keys requires browsers to subscribe again.
6651

67-
## Install
52+
## Deploy
6853

69-
Create an immutable release and install production dependencies:
54+
Run one mode from a clean checkout of the commit to deploy:
7055

7156
```bash
72-
revision="$(git rev-parse --short=12 HEAD)"
73-
release="/opt/slashmon/releases/$revision"
74-
sudo install -d -m 0755 "$release"
75-
git archive HEAD | sudo tar -x -C "$release"
76-
sudo corepack pnpm --dir "$release" install --prod --frozen-lockfile
77-
sudo ln -sfn "$release" /opt/slashmon/current
78-
sudo install -m 0644 collector/deploy/slashmon-backend.service /etc/systemd/system/
79-
sudo systemctl daemon-reload
80-
sudo systemctl enable --now slashmon-backend.service
57+
scripts/deploy-backend.sh --fresh
58+
scripts/deploy-backend.sh --upgrade
8159
```
8260

83-
The unit creates `/var/lib/slashmon` and stores
84-
`/var/lib/slashmon/slashmon.sqlite`. Startup accepts an empty database or the
85-
current schema only. Archive any database from an older Slashmon implementation
86-
and start clean; there is intentionally no migration path.
61+
Use `--fresh` for the first deployment or an intentionally incompatible reset.
62+
It permanently removes all Slashmon state and `/var/backups/slashmon`; it
63+
creates no backup. Use `--upgrade` for a compatible release. It stops the only
64+
writer, checkpoints and checks SQLite, saves a timestamped database under
65+
`/var/backups/slashmon`, switches the immutable release, and restarts the
66+
service without deleting state. Keep the previous release and backup until
67+
source polls and test deliveries succeed. Neither mode provides automatic
68+
rollback.
69+
70+
The unit stores the database at `/var/lib/slashmon/slashmon.sqlite`. Startup
71+
accepts an empty database or the current schema only; there is no migration
72+
path.
8773

8874
To self-host the PWA, install all dependencies and run `pnpm build` in the
8975
release. Serve `dist/` as static files. Frontend RPC URLs must be public HTTPS
@@ -137,23 +123,5 @@ Repeatedly restarting does not repair a stale node or RPC. Web Push 404/410 is
137123
endpoint-specific; shared authentication failures keep urgent work retryable
138124
and degrade channel health.
139125

140-
## Backup and upgrade
141-
142-
Stop the only writer, checkpoint and check SQLite, then copy the database:
143-
144-
```bash
145-
sudo systemctl stop slashmon-backend.service
146-
database=/var/lib/slashmon/slashmon.sqlite
147-
sudo sqlite3 "$database" 'PRAGMA wal_checkpoint(TRUNCATE);'
148-
test "$(sudo sqlite3 "$database" 'PRAGMA quick_check;')" = ok
149-
sudo install -d -m 0700 /var/backups/slashmon
150-
sudo cp --preserve=mode,ownership,timestamps "$database" /var/backups/slashmon/
151-
sudo systemctl start slashmon-backend.service
152-
```
153-
154-
For an upgrade, install a new immutable release, take this backup, switch the
155-
`/opt/slashmon/current` symlink while stopped, and start the service. Keep the
156-
previous release and backup until source polls and test deliveries succeed.
157-
158126
Delivery is at-least-once. A crash after provider acceptance but before the
159127
success commit can duplicate an alert; stable event IDs are the comparison key.

0 commit comments

Comments
 (0)