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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ X402_APPRAISAL_URL=
# ROUND_CONTRACT_ID=C…
# KEEPER_DRY_RUN=true
# ROUND_ID=1
# KEEPER_CHECKPOINT_PATH=.keeper-checkpoint.json # durable watch cursor
# KEEPER_STORE_PATH=.keeper-store.json # watched round queue
# WATCH_POLL_MS=15000
# WATCH_ROUND_IDS=1,2,5
# WATCH_FROM=1
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ deployments/*.local.json
# Keeper
.keeper-store.json
.keeper-store.json.corrupted.*
.keeper-checkpoint.json
.keeper-checkpoint.json.corrupted.*

# Coverage
coverage/
Expand Down
2 changes: 1 addition & 1 deletion docs/THREAT_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
| --- | --- | --- |
| Winner doesn't pay | Escrow locked at commit; settle pulls from escrow | Requires valid reveal |
| Drand never delivers R | `void` after grace refunds all escrow | Grace window must be configured |
| Double settle | Idempotent settle skips settled bids; keeper watchers take an exclusive per-round lease so only one process reveals or settles | Proven in e2e |
| Double settle | Idempotent settle skips settled bids; keeper watch checkpoint records completed steps (hash-verified on startup) so a restart cannot rebroadcast | Proven in e2e |

### Identity privacy

Expand Down
47 changes: 47 additions & 0 deletions services/keeper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,53 @@ Response shape (typed in `@sub-rosa/sdk` as `KeeperStatusResponse`):
6. **Failure states are visible, not hidden.** A round whose on-chain lookup fails is surfaced with `status: "Unknown"` or `status: "NotFound"` and the `lastError` field populated. The process does not crash on upstream errors.
7. **Secrets in responses: none.** The status API never emits secret keys, signed transactions, or bidder private data. Bidder *addresses* (public on-chain identifiers) are included so dashboards can show bidder counts.

## Watch Checkpoint (restart safety)

The in-memory settlement guard is lost on restart. The **watch checkpoint** is the durable version: a small local JSON file (default `.keeper-checkpoint.json`, override with `KEEPER_CHECKPOINT_PATH`) that records how far each round got, so a crash after a confirmed reveal, clear, or settle does not broadcast that step again.

### Checkpoint format

```json
{
"version": 1,
"network": "Test SDF Network ; September 2015",
"contractId": "C...",
"rounds": {
"1": {
"roundId": "1",
"completedSteps": ["open-reveal", "reveal", "clear", "settle"],
"lastCompletedStep": "settle",
"lastTransactionHash": "0x…",
"stepHashes": { "settle": "0x…" },
"updatedAt": "2026-09-30T00:00:00.000Z"
}
}
}
```

| Field | Meaning |
|-------|---------|
| `network` / `contractId` | The deployment the cursor was recorded for. |
| `completedSteps` | Steps observed complete, in completion order. |
| `lastCompletedStep` | The cursor position — the last step this process finished. |
| `lastTransactionHash` | Transaction hash of the last step, when the SDK exposes one. |
| `stepHashes` | Per-step hashes, re-verified on every startup. |
| `updatedAt` | ISO-8601 timestamp of the last write. |

Steps tracked: `open-reveal`, `reveal`, `clear`, `settle`, `void`.

### Startup validation

1. **Binding.** If `network` or `contractId` on disk does not match the process configuration, the keeper refuses to start (`KeeperCheckpointMismatchError`) instead of replaying a cursor from another deployment. Point `KEEPER_CHECKPOINT_PATH` at a per-deployment file, or delete the file when you switch networks.
2. **Hash verification.** Every recorded `stepHashes` entry is re-checked (when a verifier is wired in). A hash that comes back `failed` or `missing` is rolled back so the step is retried; a `confirmed` hash is trusted even if the RPC replica still reports the pre-step status.
3. **Chain reconciliation.** Cursor entries with no hash to verify are checked against the on-chain status. If the chain cannot prove the step happened, the entry is dropped rather than stranding the round.

`open-reveal` is recorded but never used to skip work: whether the reveal window is open is already authoritative on-chain, and trusting the cursor there could strand an Open round. An unreadable or corrupted checkpoint file is backed up (`*.corrupted.<ts>`) and the keeper starts from a fresh cursor rather than guessing.

### Dry-run

`KEEPER_DRY_RUN=true npm run start` prints the checkpoint it *would* write — path, binding, proposed step, and the exact file content — inside the dry-run summary. It submits no transactions (`transactionsSubmitted: 0`) and writes nothing (`checkpoint.filesWritten: 0`). If the existing checkpoint would block a live run, the summary reports it as `checkpoint.mismatch` (`network` or `contractId`).

## Persisted Queue / Store

## Persisted Queue / Store
Expand Down
2 changes: 1 addition & 1 deletion services/keeper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"watch": "node --import tsx src/watch.ts",
"serve": "node --import tsx src/serve.ts",
"queue": "node --import tsx src/queue.ts",
"test": "node --import tsx --test src/dry-run.test.ts src/keeper.test.ts src/watch.test.ts src/store.test.ts src/queue-cli.test.ts src/watch-queue.test.ts src/watch-lease.test.ts src/settlement-guard.test.ts src/status.test.ts src/status-server.test.ts src/queue-replay.test.ts",
"test": "node --import tsx --test src/checkpoint.test.ts src/checkpoint-restart.test.ts src/dry-run.test.ts src/keeper.test.ts src/watch.test.ts src/store.test.ts src/queue-cli.test.ts src/watch-queue.test.ts src/settlement-guard.test.ts src/status.test.ts src/status-server.test.ts src/queue-replay.test.ts",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
Expand Down
Loading