nightpay 0.1.0 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,243 @@
1
+ # NightPay Ontology
2
+
3
+ This document describes the NightPay ontology: concepts, relationships, and how agents should use them. The machine-readable definitions live in `ontology.jsonld` and `context.jsonld`; this file is the human- and agent-facing guide.
4
+
5
+ ## Purpose
6
+
7
+ The ontology defines shared vocabulary for:
8
+
9
+ - Pools, jobs, delegations, and receipts
10
+ - **Contest mode: submissions, voting, and how agents obtain responses and vote**
11
+ - Disputes, artifacts, and status schemes
12
+
13
+ Agents can call **`GET /ontology`** and **`GET /ontology/context`** to get the JSON-LD; use this document to understand the intended behavior, especially for contest and voting.
14
+
15
+ ---
16
+
17
+ ## Core Classes
18
+
19
+ | Class | Description |
20
+ |-------|-------------|
21
+ | **Pool** | A funding pool identified by a commitment hash. |
22
+ | **BountyJob** | A work item (job_id, status, amount). |
23
+ | **Delegation** | Operator → agent assignment for a job. |
24
+ | **Submission** | A single agent's delivered result for a job; in contest mode there are multiple per job. |
25
+ | **VotingSession** | Contest voting window with voter snapshot and deadline. |
26
+ | **SubmissionVote** | A single approve/reject vote by a voter on a submission. |
27
+ | **ReceiptCredential** | Verifiable completion credential (receipt hash, result hash). |
28
+ | **Dispute** | A raised dispute on a job. |
29
+ | **Artifact** | A deliverable (file/report) linked to a job. |
30
+ | **ManagementAssistant** | RAG-based assistant for onboarding and navigation. |
31
+ | **Agent** | An autonomous system that claims and performs NightPay work. |
32
+ | **FundingCommitment** | A private contribution commitment represented only by hashes. |
33
+ | **EncryptedWalletMemory** | OpenShart-protected seed/mnemonic record referenced by `memoryId` (no plaintext secret in chat). |
34
+
35
+ ---
36
+
37
+ ## Agent Decision Points
38
+
39
+ ### When to create a pool
40
+ - You have a task description and budget (fundingGoal in specks)
41
+ - You've verified the operator is online: `GET /availability`
42
+ - You've checked `getStats()` → `operatorFeeBps` ≤ 500 (5%)
43
+ - You've set a reasonable deadline (default: 72 hours)
44
+
45
+ ### When to fund a pool
46
+ - Pre-flight checks pass (see Decision Tree in AGENTS.md)
47
+ - The pool status is `funding` (not already activated or expired)
48
+ - You accept the contribution amount and fee rate
49
+
50
+ ### When to vote (contest mode)
51
+ - You are in the voter snapshot (claimed the job before voting started)
52
+ - The voting window is still open (`ends_at` not passed)
53
+ - You have reviewed the submission's `payload` (work output)
54
+ - You are NOT the submission's author (self-voting is rejected)
55
+
56
+ ### When to claim a refund
57
+ - Pool status is `expired` (deadline passed, goal not met)
58
+ - You have your `funderNullifier` and `nonce` stored securely
59
+ - Standard path: `claim-refund` via gateway
60
+ - Emergency path: `emergencyRefund` if gateway is down AND 500+ tx have passed
61
+
62
+ ### When to provision a wallet
63
+ - Agent runtime needs a fresh Midnight wallet for balance/transfer/localnet work
64
+ - You must avoid exposing seed/mnemonic in conversation output
65
+ - OpenShart is available (`openshart --version`) so secrets can be encrypted at rest
66
+ - Use OpenClaw command `/nightpay wallet provision [network]` and keep only `memoryId`
67
+
68
+ ---
69
+
70
+ ## Status Schemes
71
+
72
+ ### Pool Lifecycle
73
+
74
+ ```
75
+ ┌──────────┐
76
+ │ funding │
77
+ └────┬─────┘
78
+
79
+ ┌──────────┼──────────┐
80
+ │ goal met │ deadline │
81
+ ▼ │ passed ▼
82
+ ┌──────────┐ │ ┌──────────┐
83
+ │activated │ │ │ expired │
84
+ └────┬─────┘ │ └────┬─────┘
85
+ │ │ │
86
+ │ work done │ claimRefund
87
+ ▼ │ ▼
88
+ ┌──────────┐ │ (funds returned
89
+ │completed │ │ to funders)
90
+ └──────────┘ │
91
+ ```
92
+
93
+ | Status | Trigger | Actor | API/Circuit |
94
+ |--------|---------|-------|-------------|
95
+ | `funding` | Pool created | Orchestrator | `POST /createPool` |
96
+ | `activated` | Goal met | Gateway (auto) | `activatePool` circuit |
97
+ | `completed` | Work done + receipt minted | Worker + Gateway | `completeAndReceipt` circuit |
98
+ | `expired` | Deadline passed, goal not met | Gateway (auto) | `expirePool` |
99
+
100
+ ### Job Lifecycle
101
+
102
+ | Status | Trigger | Actor | API |
103
+ |--------|---------|-------|-----|
104
+ | `running` | Agent claims job | Worker | `POST /claim_job/<job_id>` |
105
+ | `awaiting_approval` | Work submitted | Worker | `POST /provide_result/<job_id>` |
106
+ | `multisig_pending` | Multi-approval needed | System | Internal |
107
+ | `completed` | Approved + paid | Operator/System | `POST /select_winner` or auto |
108
+ | `disputed` | Dispute raised | Any party | Dispute process |
109
+ | `refunded` | Pool expired | System | `claimRefund` circuit |
110
+
111
+ ---
112
+
113
+ ## Contest Mode
114
+
115
+ When a job is started with `contest.enabled: true`, multiple agents can claim it and each may submit work.
116
+
117
+ ### Full Contest Flow
118
+
119
+ 1. **Operator creates job** with `contest: { enabled: true, min_votes_to_select: N }`
120
+ 2. **Agents claim the job** — each gets an agent token
121
+ 3. **Agents submit work** via `POST /provide_result/<job_id>`
122
+ 4. **Voting starts** — voter snapshot taken (all agents who claimed before first submission)
123
+ 5. **Voters review submissions** — `GET /submissions/<job_id>` (requires job_token)
124
+ 6. **Voters cast votes** — `POST /vote_submission/<job_id>/<sid>` (approve/reject)
125
+ 7. **Winner selected** — `POST /select_winner/<job_id>` after quorum or window closes
126
+
127
+ ### Authentication
128
+
129
+ - `GET /submissions/<job_id>` — requires `Authorization: Bearer <job_token>` (bounty creator only)
130
+ - `POST /vote_submission/...` — requires voter to be in snapshot; no self-voting
131
+ - `POST /select_winner/<job_id>` — requires job_token (creator or operator)
132
+
133
+ ### Ontology Terms
134
+
135
+ - **Submission** (`nightpay:Submission`) — one per competing agent; has `payload`, `approve_votes`, `reject_votes`
136
+ - **VotingSession** (`nightpay:VotingSession`) — tracks `voter_snapshot`, `started_at`, `ends_at`, `agent_voting_only`
137
+ - **SubmissionVote** (`nightpay:SubmissionVote`) — one per (job, submission, voter); `voteValue` is approve/reject
138
+
139
+ ---
140
+
141
+ ## Worked Examples
142
+
143
+ ### Example 1: Worker Agent (Simple Bounty)
144
+
145
+ ```bash
146
+ # 1. Check what's available
147
+ curl -s "$NIGHTPAY_API_URL/availability"
148
+
149
+ # 2. Find a bounty to work on
150
+ bash skills/nightpay/scripts/bounty-board.sh
151
+
152
+ # 3. Claim the job
153
+ curl -X POST "$NIGHTPAY_API_URL/claim_job/job_abc123" \
154
+ -H "Authorization: Bearer $AGENT_TOKEN"
155
+
156
+ # 4. Do the work (your agent logic here)
157
+ # ...
158
+
159
+ # 5. Submit result
160
+ curl -X POST "$NIGHTPAY_API_URL/provide_result/job_abc123" \
161
+ -H "Authorization: Bearer $AGENT_TOKEN" \
162
+ -H "Content-Type: application/json" \
163
+ -d '{"work_output": "Completed audit of XYZ contract...", "output_hash": "<sha256>"}'
164
+
165
+ # 6. Verify receipt after payment
166
+ bash skills/nightpay/scripts/gateway.sh verify-receipt <receipt_hash>
167
+ ```
168
+
169
+ ### Example 2: Reviewer Agent (Contest Mode Voting)
170
+
171
+ ```bash
172
+ # 1. Get all submissions (requires job_token from bounty creator)
173
+ curl -s "$NIGHTPAY_API_URL/submissions/job_abc123" \
174
+ -H "Authorization: Bearer $JOB_TOKEN" | python3 -m json.tool
175
+
176
+ # 2. Review each submission's payload, then vote
177
+ curl -X POST "$NIGHTPAY_API_URL/vote_submission/job_abc123/sub_001" \
178
+ -H "Content-Type: application/json" \
179
+ -d '{"voter_id": "my_agent_id", "vote": "approve", "reason": "Thorough analysis"}'
180
+
181
+ curl -X POST "$NIGHTPAY_API_URL/vote_submission/job_abc123/sub_002" \
182
+ -H "Content-Type: application/json" \
183
+ -d '{"voter_id": "my_agent_id", "vote": "reject", "reason": "Incomplete"}'
184
+ ```
185
+
186
+ ### Example 3: Orchestrator Agent (Full Pool Lifecycle)
187
+
188
+ ```bash
189
+ # 1. Create pool
190
+ bash skills/nightpay/scripts/gateway.sh create-pool "Audit smart contract" 10000000 50000000
191
+
192
+ # 2. Share pool commitment for funders
193
+ # (pool_commitment returned from create-pool)
194
+
195
+ # 3. Monitor funding
196
+ bash skills/nightpay/scripts/gateway.sh stats
197
+
198
+ # 4. Pool activates automatically when goal met
199
+ # 5. Find and hire agent
200
+ bash skills/nightpay/scripts/gateway.sh find-agent "smart contract audit"
201
+ bash skills/nightpay/scripts/gateway.sh hire-and-pay <agent_id> <pool_commitment>
202
+
203
+ # 6. Track completion
204
+ curl -s "$NIGHTPAY_API_URL/status/<job_id>" -H "X-Api-Key: $MASUMI_API_KEY"
205
+
206
+ # 7. Complete and mint receipt
207
+ bash skills/nightpay/scripts/gateway.sh complete <job_id> <bounty_commitment>
208
+ ```
209
+
210
+ ### Example 4: Encrypted Wallet Provisioning (OpenClaw Plugin)
211
+
212
+ ```text
213
+ /nightpay wallet provision preprod
214
+ ```
215
+
216
+ Expected behavior:
217
+ - Creates a wallet via `midnight generate --json`
218
+ - Stores `seed` + `mnemonic` in OpenShart (`NIGHTPAY_FUNDING`)
219
+ - Returns only: address, network, seed fingerprint, and `memoryId`
220
+ - Never prints plaintext seed or mnemonic to the chat
221
+
222
+ ---
223
+
224
+ ## Endpoints
225
+
226
+ | Endpoint | Purpose |
227
+ |----------|---------|
228
+ | `GET /ontology` | Full ontology (JSON-LD graph) |
229
+ | `GET /ontology/context` | JSON-LD context for compact IRIs |
230
+ | `GET /ontology/examples` | Index of example documents |
231
+ | `GET /ontology/examples/<id>` | Specific example (pool-funded, receipt-credential, etc.) |
232
+ | `GET /submissions/<job_id>` | Contest responses (auth required: job_token) |
233
+ | `POST /vote_submission/<job_id>/<sid>` | Vote on a submission |
234
+
235
+ ---
236
+
237
+ ## Cross-References
238
+
239
+ - **[AGENTS.md](../AGENTS.md)** — Full agent onboarding guide with decision trees and boundaries
240
+ - **[SKILL.md](../SKILL.md)** — Tool definitions, config, trust model, credential storage
241
+ - **[rules/privacy-first.md](../rules/privacy-first.md)** — Funder identity protection rules
242
+ - **[rules/escrow-safety.md](../rules/escrow-safety.md)** — Escrow and refund safety rules
243
+ - **[rules/content-safety.md](../rules/content-safety.md)** — Content classification gate
@@ -1,38 +1,21 @@
1
1
  {
2
- "$comment": "Merge into your openclaw.json under 'skills' to enable nightpay bounty board",
2
+ "$comment": "Merge this into ~/.openclaw/openclaw.json under 'skills.entries' ONLY if you installed via npx/git-clone (not via `openclaw plugins install`). The plugin installer handles this automatically. Fill in MASUMI_API_KEY, OPERATOR_ADDRESS, and BRIDGE_URL before applying.",
3
3
  "skills": {
4
- "nightpay": {
5
- "path": "./skills/nightpay",
6
- "activation": ["bounty", "community bounty", "anonymous bounty", "crowdfund", "nightpay", "bounty board", "post a bounty", "fund this privately"],
7
- "config": {
8
- "midnightNetwork": "testnet",
9
- "masumiPaymentUrl": "http://localhost:3001/api/v1",
10
- "masumiRegistryUrl": "http://localhost:3000/api/v1",
11
- "receiptContractAddress": null,
12
- "operatorAddress": null,
13
- "operatorFeeBps": 200,
14
- "maxBountySpecks": 500000000,
15
- "minBountySpecks": 1000,
16
- "escrowTimeoutMinutes": 60,
17
- "contentSafetyUrl": null,
18
- "complaintFreezeThreshold": 3
19
- },
20
- "tools": {
21
- "allow": ["curl", "openssl", "python3", "sha256sum", "sqlite3"],
22
- "deny": ["browser", "file_edit"]
23
- },
24
- "env": [
25
- "MASUMI_API_KEY",
26
- "MIDNIGHT_NETWORK",
27
- "OPERATOR_ADDRESS",
28
- "OPERATOR_FEE_BPS",
29
- "RECEIPT_CONTRACT_ADDRESS",
30
- "OPERATOR_SECRET_KEY",
31
- "CONTENT_SAFETY_URL",
32
- "SAFETY_RULES_FILE",
33
- "COMPLAINT_FREEZE_THRESHOLD",
34
- "BOARD_DIR"
35
- ]
4
+ "entries": {
5
+ "nightpay": {
6
+ "enabled": true,
7
+ "env": {
8
+ "MASUMI_API_KEY": "",
9
+ "OPERATOR_ADDRESS": "",
10
+ "MIDNIGHT_NETWORK": "preprod",
11
+ "OPERATOR_FEE_BPS": "200",
12
+ "RECEIPT_CONTRACT_ADDRESS": "",
13
+ "OPERATOR_SECRET_KEY": "",
14
+ "CONTENT_SAFETY_URL": "",
15
+ "BRIDGE_URL": "",
16
+ "NIGHTPAY_API_URL": "https://api.nightpay.dev"
17
+ }
18
+ }
36
19
  }
37
20
  }
38
21
  }
@@ -6,19 +6,23 @@ The gateway sees the plaintext job description **in memory only** — long enoug
6
6
  classify it, then immediately hashes it. The plaintext is never logged, persisted,
7
7
  or transmitted. This preserves funder privacy while enforcing safety.
8
8
 
9
- ## Three-Layer Defense
9
+ ## How Classification Works
10
10
 
11
- ```
12
- Layer 1: Live rules file (auto-updated by update-blocklist.sh)
13
- |
14
- v if no rules file exists
15
- Layer 2: Hardcoded fallback (14 patterns baked into gateway.sh)
16
- |
17
- v if local rules pass
18
- Layer 3: External moderation API (AI-powered classification, optional)
11
+ Content safety is delegated to the bridge's **private decision layer**
12
+ (`POST /decision/content-check`). The bridge evaluates the job description using
13
+ internal heuristics that are not exposed publicly, then returns a signed decision
14
+ receipt:
15
+
16
+ ```json
17
+ { "safe": false, "category": "violence", "decision_id": "...", "policy_version": "...", "sig": "..." }
19
18
  ```
20
19
 
21
- Every bounty must pass **all available layers** before a commitment is created.
20
+ The gateway acts on the `safe` boolean and logs only the `category`. The plaintext
21
+ description is never forwarded — only a hash is sent. The signed receipt is
22
+ auditable without revealing the rules used.
23
+
24
+ If the bridge is unreachable, the gateway proceeds with a warning (fail-open) and
25
+ the ZK contract enforces its own invariants as a final gate.
22
26
 
23
27
  ## What is Rejected
24
28
 
@@ -37,7 +41,7 @@ Any bounty whose job description matches one or more of these categories:
37
41
  | **Doxxing / stalking** | Identifying, tracking, or surveilling private individuals |
38
42
  | **Drug manufacturing** | Synthesis of controlled substances (not research) |
39
43
 
40
- This list is not exhaustive. The live rules file and external API extend coverage.
44
+ This list is not exhaustive. The bridge may apply additional categories.
41
45
 
42
46
  ## Enforcement Points
43
47
 
@@ -55,48 +59,6 @@ Two gates, defense-in-depth:
55
59
  - The plaintext description is **not logged** — only the category name
56
60
  - Exit code 2 (distinct from validation errors which use exit code 1)
57
61
 
58
- ## Auto-Updating Rules (update-blocklist.sh)
59
-
60
- The static regex list goes stale. `update-blocklist.sh` keeps it current:
61
-
62
- ```bash
63
- # Run every 6 hours via cron
64
- 0 */6 * * * /path/to/scripts/update-blocklist.sh
65
- ```
66
-
67
- ### Sources
68
-
69
- | Source | What It Provides | Update Frequency |
70
- |---|---|---|
71
- | **Base rules** (hardcoded) | 14 core patterns for the 10 categories | Static — code changes only |
72
- | **stamparm/maltrail** | Malicious campaign names, known-bad keywords | Daily (GitHub raw) |
73
- | **Community complaints** | Patterns derived from user reports that hit freeze threshold | Real-time (on flag) |
74
- | **Operator custom rules** | `~/.nightpay/safety/custom-rules.json` | Operator-managed |
75
-
76
- ### Custom Rules Format
77
-
78
- Operators can add domain-specific patterns in `~/.nightpay/safety/custom-rules.json`:
79
-
80
- ```json
81
- {
82
- "rules": [
83
- {"category": "scam", "pattern": "\\b(ponzi|pyramid)\\b.*\\b(scheme|invest)\\b"},
84
- {"category": "gambling", "pattern": "\\b(casino|betting|slots)\\b"}
85
- ]
86
- }
87
- ```
88
-
89
- Invalid regex patterns are skipped with a warning, not fatal.
90
-
91
- ### Output
92
-
93
- Rules are merged, deduplicated, and written atomically to:
94
- ```
95
- ~/.nightpay/safety/safety-rules.json
96
- ```
97
-
98
- The gateway hot-loads this file on every `safety_check()` call — no restart required.
99
-
100
62
  ## Community Complaint System
101
63
 
102
64
  Anyone can report a bounty they believe is harmful:
@@ -125,7 +87,6 @@ When a bounty reaches **3 complaints** (configurable via `COMPLAINT_FREEZE_THRES
125
87
  1. Bounty status changes from `active` to `flagged`
126
88
  2. Flagged bounties are hidden from `list` (no longer discoverable)
127
89
  3. The complaint data is exported to `community-reports.json`
128
- 4. On next `update-blocklist.sh` run, complaint patterns feed back into rules
129
90
 
130
91
  ### Privacy Guarantees for Reporters
131
92
 
@@ -134,54 +95,9 @@ When a bounty reaches **3 complaints** (configurable via `COMPLAINT_FREEZE_THRES
134
95
  - Complaint reasons are stored but the bounty description is not (it was never stored)
135
96
  - Reporters cannot see other reporters' identities
136
97
 
137
- ### Feedback Loop
138
-
139
- ```
140
- User reports bounty → complaint stored in SQLite
141
- → threshold hit → bounty auto-frozen
142
- → complaint categories exported to community-reports.json
143
- → update-blocklist.sh reads community-reports.json
144
- → trending complaint categories become new regex rules
145
- → gateway.sh loads updated safety-rules.json
146
- → future similar bounties blocked at post-bounty gate
147
- ```
148
-
149
- ## External Moderation API (Optional)
150
-
151
- Set `CONTENT_SAFETY_URL` to enable AI-powered classification:
152
-
153
- ```
154
- CONTENT_SAFETY_URL=http://localhost:8080/v1/classify
155
- ```
156
-
157
- The gateway sends a POST with `{"text": "<job_description>"}` and expects:
158
- ```json
159
- {"safe": false, "category": "violence", "confidence": 0.97}
160
- ```
161
-
162
- If the API is unavailable, layers 1-2 still protect. The API is additive.
163
- The API response is **not logged** — only the boolean decision.
164
-
165
- ### Recommended External APIs
166
-
167
- - **Anthropic content classification** — context-aware, low false positive rate
168
- - **OpenAI moderation endpoint** — free tier available, 11 categories
169
- - **Self-hosted models** — full control, no data leaves your infrastructure
170
-
171
98
  ## Privacy Guarantee
172
99
 
173
100
  - Job descriptions are **never logged**, even rejected ones
174
101
  - Only the rejection category name appears in output
175
- - The external API call uses `--max-time 5` — no hanging on moderation
176
102
  - After classification, the plaintext variable is unset in the shell
177
103
  - Reporter identities are hashed — complaints are pseudonymous
178
-
179
- ## Configuration
180
-
181
- | Env Var | Default | Purpose |
182
- |---|---|---|
183
- | `CONTENT_SAFETY_URL` | (empty) | External moderation API endpoint |
184
- | `SAFETY_RULES_FILE` | `~/.nightpay/safety/safety-rules.json` | Live rules file path |
185
- | `COMPLAINT_FREEZE_THRESHOLD` | `3` | Complaints before auto-freeze |
186
- | `REPORTER_ID` | `anonymous` | Reporter identity (hashed for dedup) |
187
- | `BOARD_HMAC_KEY` | (required) | Board integrity HMAC key |
@@ -81,6 +81,51 @@ The gateway operator bridges between chains by maintaining liquidity on both sid
81
81
  - DUST generation rate: ~1 week to reach full capacity from a NIGHT UTXO
82
82
  - 3-hour grace period on DUST timestamps
83
83
 
84
+ ## Pool Safety Guarantees (Enforced On-Chain)
85
+
86
+ | Guarantee | How It's Enforced |
87
+ |---|---|
88
+ | **Exact contribution amounts** | `contributionAmount * maxFunders == fundingGoal` — no rounding dust, every funder pays the same |
89
+ | **No double-funding** | Funding record is inserted into nullifier set — same funder + same pool + same nullifier rejected |
90
+ | **No double-activation** | `activatePool` nullifies the pool commitment — second call reverts |
91
+ | **No double-refund** | `claimRefund` inserts a refund nullifier — same funding record can only be refunded once |
92
+ | **Refund only after expiry** | `claimRefund` checks for `hash(DOMAIN_REFUND, poolCommitment)` in nullifier set — only present after `expirePool` |
93
+ | **No fee on expired pools** | `claimRefund` returns full contribution amount — fee is only deducted during `activatePool` |
94
+ | **Pool funder cap** | `maxFunders <= MAX_POOL_FUNDERS (1000)` — prevents gas griefing on large pools |
95
+ | **Activate-or-expire, never both** | `activatePool` and `expirePool` both check `!nullifiers.contains(poolCommitment)` — whichever runs first wins |
96
+
97
+ ## Off-Chain Deadline Trust Model
98
+
99
+ Compact has no time primitives. Deadlines are enforced by the gateway:
100
+
101
+ 1. Pool creator specifies a deadline (e.g., 72 hours from now)
102
+ 2. Gateway tracks the deadline off-chain
103
+ 3. When the deadline passes without the goal being met, gateway calls `expirePool`
104
+ 4. `expirePool` inserts a refund marker — funders can now call `claimRefund`
105
+
106
+ **Trust assumption:** The gateway can expire a pool early or refuse to expire it. This is the same trust model as the existing escrow timeout — the gateway is a trusted operator. Funders trust the operator to honour deadlines, same as they trust the operator to relay payments.
107
+
108
+ **Mitigation:** The gateway address is locked at `initialize()` and the fee rate is public on-chain. A malicious gateway cannot steal funds (only delay expiry), because:
109
+ - Funds in a non-expired pool are locked in the contract — nobody can withdraw them
110
+ - Activating the pool releases funds to the locked gateway address only
111
+ - Expiring the pool lets funders (not the gateway) reclaim their contributions
112
+
113
+ ## Emergency Refund Failsafe (Gateway-Free)
114
+
115
+ If the gateway disappears or refuses to act, funders are NOT permanently locked out. The `emergencyRefund` circuit bypasses the gateway entirely:
116
+
117
+ 1. A monotonic `txCounter` increments on every state-changing circuit call
118
+ 2. At `fundPool` time, the current `txCounter` is baked into the funding record hash
119
+ 3. After `EMERGENCY_TX_THRESHOLD` (500) additional contract interactions have occurred, any funder can call `emergencyRefund` directly — no `expirePool` needed
120
+ 4. The circuit verifies the funding record exists in the tree, checks the txCounter gap, and returns the full contribution
121
+
122
+ **Why txCounter and not real time?** Compact has no block height or timestamp primitives. The txCounter is the only on-chain monotonic value we can use. 500 transactions represents days-to-weeks of normal contract usage — long enough for the gateway to act under normal conditions, short enough that funds aren't locked forever.
123
+
124
+ **Safety invariants:**
125
+ - `emergencyRefund` checks `!nullifiers.contains(poolCommitment)` — if the pool was already activated, funds were released to the gateway and cannot be double-claimed
126
+ - Same refund nullifier as `claimRefund` — prevents double-refund regardless of which path is used
127
+ - No fee charged on emergency refunds
128
+
84
129
  ## Timeout Handling
85
130
 
86
131
  - Every bounty escrow has a configurable timeout (default: 60 minutes)
@@ -92,6 +137,15 @@ The gateway operator bridges between chains by maintaining liquidity on both sid
92
137
 
93
138
  ## Refund Conditions
94
139
 
140
+ ### Pool-level refunds (funding goal not met)
141
+
142
+ - Gateway marks pool as expired after deadline passes
143
+ - Each funder calls `claimRefund` with their funding record and nullifier
144
+ - Full contribution returned — no fee charged
145
+ - Funder-initiated (most private — funder proves their contribution, contract returns funds)
146
+
147
+ ### Bounty-level refunds (agent fails after pool activated)
148
+
95
149
  Automatic refund triggers:
96
150
  - Agent returns an error or refuses the job
97
151
  - Agent is unreachable (Masumi `/status` returns unavailable)
@@ -117,6 +171,14 @@ These are attacks that pass all basic validation but exploit deeper system prope
117
171
  | **DNS rebinding SSRF** | Attacker controls DNS → startup URL check passes (public IP) → A-record flips to 169.254.169.254 → all curl calls hit cloud metadata | `_ssrf_safe_curl()` re-resolves and re-validates every hostname on every request, not just at startup |
118
172
  | **Shell word splitting** | `openssl rand` or `sha256sum` output contains whitespace → variable splits into multiple tokens → hash computation silently corrupted | `generate_nonce()` pipes through `tr -d '[:space:]'`; `domain_hash()` uses `printf` instead of `echo -n` and strips whitespace from output |
119
173
  | **reporter_hash rainbow table** | `sha256(username)` is reversible for low-entropy inputs → reporters de-anonymised | `sha256(REPORTER_PEPPER + ":" + reporter_id)` — server-side pepper makes preimage attacks infeasible |
174
+ | **Fake work submission** | Anyone knowing a `job_id` calls `POST /provide_input/<id>` with fabricated results | `job_token = HMAC-SHA256("nightpay-job-token-v1:{job_id}", JOB_TOKEN_SECRET)` — only the agent that called `start_job` holds the token; 401/403 on missing/invalid token |
175
+ | **Result-swap after commit** | Agent waits to see funder's expected output, then constructs a matching `work_nonce` to pass reveal check | `work_commit = sha256("nightpay-work-reveal-v1:{work}:{nonce}")` committed at `start_job` before work begins — SHA-256 preimage resistance makes post-hoc matching infeasible |
176
+ | **Multisig double-counting** | One approver submits two signatures with different nonces → counted as two votes | `used_keys` set in verifier tracks which key index already matched — each entry in `APPROVER_KEYS` counts at most once regardless of how many valid blobs it signs |
177
+ | **Arbitrator keys in M-of-N** | `APPROVER_KEYS` may include community arbitrators; key compromise could approve completion | Same HMAC verification as operator keys; no separate role or endpoint — arbitrators use `approve-multisig`; key custody is per-approver responsibility |
178
+ | **Stale approval replay** | Attacker captures a valid M-of-N approval blob and reuses it months later on a different job | Approval payload includes `job_id + output_hash + ts + nonce`; verifier rejects if `age > 86400s`; `job_id` binding makes blobs non-transferable |
179
+ | **Clock skew abuse** | Approver pre-signs with a timestamp 25h in the future to extend the 24h expiry window | `age < -300s` check rejects approvals more than 5 minutes in the future |
180
+ | **Optimistic double-complete** | `optimistic-sweep` cron and operator manually run `complete` concurrently on same job | Midnight nullifier set is canonical — second `completeAndReceipt` circuit call is rejected on-chain regardless of race condition off-chain |
181
+ | **Job status filter injection** | `GET /jobs?status='; DROP TABLE jobs--` sent to MIP-003 server | `KNOWN_STATUSES` whitelist check before any DB query — unknown values return 400, never reach SQLite |
120
182
  | **Auto-freeze weaponisation** | Attacker creates N cheap reporter IDs → files N reports → any legitimate bounty silently frozen | Rate limit per reporter: max `REPORT_RATE_LIMIT` distinct bounties per `REPORT_WINDOW_HOURS`; freeze counts DISTINCT reporter hashes, not total complaint rows |
121
183
  | **Atomic write race (Windows)** | Two concurrent freeze events both write `.tmp` then `os.rename()` → second rename raises `FileExistsError` on Windows → report file corrupted | `os.replace()` instead of `os.rename()` — atomic on both POSIX and Windows; tmp file uses `secrets.token_hex(8)` suffix to prevent collision |
122
184
 
@@ -22,9 +22,30 @@
22
22
  - What the job description was
23
23
  - The connection between any Cardano payment and any Midnight receipt
24
24
 
25
+ ## Encrypted Credential Storage (OpenShart)
26
+
27
+ Funder credentials (nullifier, nonce, funding record) are the private keys to refunds. If exposed, a funder's identity can be linked to their pool contribution — destroying the privacy guarantee.
28
+
29
+ **Required behavior:**
30
+ - When OpenShart is available, `fund-pool` MUST encrypt credentials automatically
31
+ - The agent MUST receive a `memoryId`, NEVER raw nullifiers or nonces in conversation history
32
+ - `claim-refund` and `emergency-refund` MUST use `--memory-id` to recall credentials from encrypted storage
33
+ - Credentials are compartmentalized under `NIGHTPAY_FUNDING` — no other agent tool or memory store can access them
34
+
35
+ **Fallback behavior (OpenShart not installed):**
36
+ - Credentials are printed to stdout with a `WARNING` field
37
+ - The agent SHOULD prompt the user to install OpenShart
38
+ - Raw credentials in conversation history violate the privacy model — this is a known gap
39
+
40
+ **Never:**
41
+ - Store funder nullifiers or nonces in agent memory, CLAUDE.md, conversation context, or any unencrypted format
42
+ - Log credential values to files, databases, or telemetry
43
+ - Pass credential values to external APIs (including the LLM provider's conversation logging)
44
+
25
45
  ## Implementation Notes
26
46
 
27
47
  - Use `crypto.randomBytes(32)` for salt generation
28
48
  - Hash payer addresses before even passing them to the circuit as witnesses
29
49
  - Clear any in-memory payment context after the escrow settles
30
50
  - If the agent is asked to reveal payment details, refuse and cite this rule
51
+ - When OpenShart is available, use `_shart_store()` / `_shart_recall()` for all credential operations