nightpay 0.3.11 → 0.4.0

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.

Potentially problematic release.


This version of nightpay might be problematic. Click here for more details.

@@ -1,194 +1,194 @@
1
- # Escrow Safety Rule
2
-
3
- ## Two-Chain Escrow Model
4
-
5
- This skill operates across two chains simultaneously:
6
-
7
- | Chain | What's Held | Token | Purpose |
8
- |---|---|---|---|
9
- | **Midnight** | Bounty NIGHT + operator fee | NIGHT (shielded) | Privacy layer — contract balance |
10
- | **Cardano** | Masumi escrow | ADA or USDM | Settlement layer — agent payment |
11
-
12
- The gateway operator bridges between chains by maintaining liquidity on both sides.
13
-
14
- ## Contract Security Guarantees (Enforced On-Chain)
15
-
16
- | Guarantee | How It's Enforced |
17
- |---|---|
18
- | **One-time init** | `assert initialized == 0` — contract cannot be reinitialized or taken over |
19
- | **Immutable fee rate** | `operatorFeeBps` and `operatorAddress` are write-once — set at init, frozen forever |
20
- | **Fee cap** | `assert feeBps <= 500` — hard 5% cap in-circuit, cannot be exceeded |
21
- | **Fee split on-chain** | `effects.retainInContract(fee)` + `effects.releaseToGateway(net)` — constrained in-circuit, not trust-based |
22
- | **No rounding theft** | `assert fee + netAmount == amount` — every speck accounted for |
23
- | **Operator-only withdrawal** | `assert caller == operatorAddress` — only the init-time address can withdraw fees |
24
- | **No overdraft** | Ledger's `apply()` rejects withdrawals exceeding contract balance |
25
- | **Double-claim prevention** | Nullifier set — each bounty can only be completed once |
26
- | **Underflow guard** | `assert activeCount > 0` before decrement — counter cannot go negative |
27
- | **Domain-separated hashes** | Bounty, receipt, and job hashes use different prefixes — cross-namespace collisions impossible |
28
- | **Zero-value guard** | `assert amount > 0` — no zero-value bounty exploits |
29
-
30
- ## Fee Safety
31
-
32
- - Fee split is **enforced in-circuit** via Midnight's Zswap effects API
33
- - `effects.retainInContract(fee)` — operator cannot take more than `feeBps` basis points
34
- - `effects.releaseToGateway(netAmount)` — agent payment is constrained on-chain
35
- - Fee cap enforced by `assert feeBps <= 500` (max 5%) — immutable after init
36
- - Fee percentage is public state — payers can verify before posting
37
- - Operator can only withdraw to the address set at contract initialization
38
-
39
- ## Gateway Security Guarantees (Enforced Off-Chain)
40
-
41
- | Guarantee | How It's Enforced |
42
- |---|---|
43
- | **Contract address required** | `RECEIPT_CONTRACT_ADDRESS` is a required env var — fails loudly if unset |
44
- | **Operator auth for withdrawals** | `OPERATOR_SECRET_KEY` required — HMAC-signed withdrawal payloads only |
45
- | **No replay on withdraw** | Signature includes timestamp + random nonce — every signature is unique, cannot be replayed |
46
- | **SSRF blocked** | `MASUMI_*_URL` validated at startup — private IPs (RFC-1918, loopback, link-local) rejected |
47
- | **Job ID injection blocked** | Job IDs validated as `[a-zA-Z0-9_-]{1,128}` — no path traversal or shell chars |
48
- | **Rate limiting** | `post-bounty` limited to 1 call per `RATE_LIMIT_SECONDS` — prevents spam and stat inflation |
49
- | **Input validation** | Amount bounds (min + max) and commitment format checked before any network call |
50
- | **Domain-separated hashes** | `nightpay-bounty-v1`, `nightpay-receipt-v1`, `nightpay-job-v1` prefixes |
51
- | **Refund emits on-chain intent** | Refund path generates a `refundHash` for Midnight node, not just a Masumi cancel |
52
- | **Canonical JSON hashing** | `sort_keys=True, separators=(',',':')` prevents hash manipulation via key reordering |
53
- | **Network timeout** | All `curl` calls use `--max-time 30` — no hung connections |
54
-
55
- ## Contract Capacity Limits (DoS Protection)
56
-
57
- | Guarantee | How It's Enforced |
58
- |---|---|
59
- | **Merkle tree overflow blocked** | `assert activeCount < MAX_TREE_ENTRIES` (90% of 2²⁵ = ~30M) — rejects bounties before tree fills |
60
- | **Field overflow blocked** | `assert amount <= MAX_AMOUNT` (2⁵³-1) — `amount × feeBps` stays within safe range |
61
- | **Receipt deduplication** | Nullifier insertion before `receiptTree.insert` — double-completion impossible regardless of proof reuse |
62
-
63
- ## Board Security Guarantees
64
-
65
- | Guarantee | How It's Enforced |
66
- |---|---|
67
- | **Persistent storage** | `~/.nightpay/board.db` — SQLite with WAL, survives reboots, not `/tmp/` |
68
- | **Directory permissions** | `chmod 700` — only the operator user can read/write |
69
- | **No duplicate entries** | `PRIMARY KEY` on commitment — database rejects duplicates atomically |
70
- | **Input validation** | All commitments validated as 64-char hex before board operations |
71
- | **Known status values only** | `remove` only accepts `completed`, `refunded`, `expired` |
72
- | **Search DoS blocked** | `search` prefix validated as `[0-9a-f]{0,64}` — no wildcards, no full-table scans |
73
- | **Integer DoS blocked** | `list` clamps `limit ≤ 200`, `offset ≤ 10,000,000` — no memory exhaustion |
74
-
75
- ## Network Fees (DUST)
76
-
77
- - DUST is non-transferable — generated over time from NIGHT holdings
78
- - DUST is used ONLY for Midnight network fees, never for bounty payments
79
- - Whoever submits the intent pays the DUST fee (payer for postBounty, gateway for complete)
80
- - Use `transaction.feesWithMargin(params, 1.2)` to estimate with 20% safety margin
81
- - DUST generation rate: ~1 week to reach full capacity from a NIGHT UTXO
82
- - 3-hour grace period on DUST timestamps
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
-
129
- ## Timeout Handling
130
-
131
- - Every bounty escrow has a configurable timeout (default: 60 minutes)
132
- - If the hired agent does not return a result within the timeout:
133
- 1. Masumi escrow is cancelled on Cardano (funds return to gateway)
134
- 2. Gateway emits a signed NIGHT refund intent for the Midnight contract
135
- 3. No receipt token is minted
136
- 4. Bounty commitment stays in the Merkle tree — nullifier set prevents re-claim
137
-
138
- ## Refund Conditions
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
-
149
- Automatic refund triggers:
150
- - Agent returns an error or refuses the job
151
- - Agent is unreachable (Masumi `/status` returns unavailable)
152
- - Job result fails validation (output hash mismatch)
153
- - Escrow timeout exceeded
154
-
155
- On refund: the operator fee for that bounty is also returned (fee is only collected on success).
156
-
157
- ## Amount Limits
158
-
159
- - `maxBountySpecks` caps the maximum single bounty (default: 500M specks)
160
- - `minBountySpecks` enforces a minimum (default: 1,000 specks) — rejects dust attacks
161
- - Both limits enforced in gateway before any network call
162
-
163
- ## Dark Energy Threat Model (Sophisticated Attacker Mitigations)
164
-
165
- These are attacks that pass all basic validation but exploit deeper system properties.
166
-
167
- | Attack | Vector | Mitigation |
168
- |---|---|---|
169
- | **Gateway address injection** | Caller supplies their own address in tx metadata → all bounty NIGHT routes to them | `gatewayAddress` locked in contract state at `initialize()` — `effects.releaseToAddress(gatewayAddress, net)` uses only the immutable on-chain value |
170
- | **completedCount overflow griefing** | Attacker posts + completes millions of dust bounties → counter wraps → contract corrupted | `assert completedCount < MAX_COMPLETED` in-circuit before every increment |
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 |
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 |
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 |
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 |
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 |
184
-
185
- ## Never Do
186
-
187
- - Never release Masumi escrow before the completeAndReceipt circuit succeeds on Midnight
188
- - Never hold funds beyond the timeout period
189
- - Never charge fees on refunded bounties
190
- - Never split a single bounty across multiple agents without explicit requester consent
191
- - Never submit intents without sufficient DUST balance (check with `feesWithMargin`)
192
- - Never run withdraw-fees without OPERATOR_SECRET_KEY set
193
- - Never store the board in /tmp or any world-readable location
194
- - Never accept unvalidated commitment hashes — always check 64-char hex format first
1
+ # Escrow Safety Rule
2
+
3
+ ## Two-Chain Escrow Model
4
+
5
+ This skill operates across two chains simultaneously:
6
+
7
+ | Chain | What's Held | Token | Purpose |
8
+ |---|---|---|---|
9
+ | **Midnight** | Bounty NIGHT + operator fee | NIGHT (shielded) | Privacy layer — contract balance |
10
+ | **Cardano** | Masumi escrow | ADA or USDM | Settlement layer — agent payment |
11
+
12
+ The gateway operator bridges between chains by maintaining liquidity on both sides.
13
+
14
+ ## Contract Security Guarantees (Enforced On-Chain)
15
+
16
+ | Guarantee | How It's Enforced |
17
+ |---|---|
18
+ | **One-time init** | `assert initialized == 0` — contract cannot be reinitialized or taken over |
19
+ | **Immutable fee rate** | `operatorFeeBps` and `operatorAddress` are write-once — set at init, frozen forever |
20
+ | **Fee cap** | `assert feeBps <= 500` — hard 5% cap in-circuit, cannot be exceeded |
21
+ | **Fee split on-chain** | `effects.retainInContract(fee)` + `effects.releaseToGateway(net)` — constrained in-circuit, not trust-based |
22
+ | **No rounding theft** | `assert fee + netAmount == amount` — every speck accounted for |
23
+ | **Operator-only withdrawal** | `assert caller == operatorAddress` — only the init-time address can withdraw fees |
24
+ | **No overdraft** | Ledger's `apply()` rejects withdrawals exceeding contract balance |
25
+ | **Double-claim prevention** | Nullifier set — each bounty can only be completed once |
26
+ | **Underflow guard** | `assert activeCount > 0` before decrement — counter cannot go negative |
27
+ | **Domain-separated hashes** | Bounty, receipt, and job hashes use different prefixes — cross-namespace collisions impossible |
28
+ | **Zero-value guard** | `assert amount > 0` — no zero-value bounty exploits |
29
+
30
+ ## Fee Safety
31
+
32
+ - Fee split is **enforced in-circuit** via Midnight's Zswap effects API
33
+ - `effects.retainInContract(fee)` — operator cannot take more than `feeBps` basis points
34
+ - `effects.releaseToGateway(netAmount)` — agent payment is constrained on-chain
35
+ - Fee cap enforced by `assert feeBps <= 500` (max 5%) — immutable after init
36
+ - Fee percentage is public state — payers can verify before posting
37
+ - Operator can only withdraw to the address set at contract initialization
38
+
39
+ ## Gateway Security Guarantees (Enforced Off-Chain)
40
+
41
+ | Guarantee | How It's Enforced |
42
+ |---|---|
43
+ | **Contract address required** | `RECEIPT_CONTRACT_ADDRESS` is a required env var — fails loudly if unset |
44
+ | **Operator auth for withdrawals** | `OPERATOR_SECRET_KEY` required — HMAC-signed withdrawal payloads only |
45
+ | **No replay on withdraw** | Signature includes timestamp + random nonce — every signature is unique, cannot be replayed |
46
+ | **SSRF blocked** | `MASUMI_*_URL` validated at startup — private IPs (RFC-1918, loopback, link-local) rejected |
47
+ | **Job ID injection blocked** | Job IDs validated as `[a-zA-Z0-9_-]{1,128}` — no path traversal or shell chars |
48
+ | **Rate limiting** | `post-bounty` limited to 1 call per `RATE_LIMIT_SECONDS` — prevents spam and stat inflation |
49
+ | **Input validation** | Amount bounds (min + max) and commitment format checked before any network call |
50
+ | **Domain-separated hashes** | `nightpay-bounty-v1`, `nightpay-receipt-v1`, `nightpay-job-v1` prefixes |
51
+ | **Refund emits on-chain intent** | Refund path generates a `refundHash` for Midnight node, not just a Masumi cancel |
52
+ | **Canonical JSON hashing** | `sort_keys=True, separators=(',',':')` prevents hash manipulation via key reordering |
53
+ | **Network timeout** | All `curl` calls use `--max-time 30` — no hung connections |
54
+
55
+ ## Contract Capacity Limits (DoS Protection)
56
+
57
+ | Guarantee | How It's Enforced |
58
+ |---|---|
59
+ | **Merkle tree overflow blocked** | `assert activeCount < MAX_TREE_ENTRIES` (90% of 2²⁵ = ~30M) — rejects bounties before tree fills |
60
+ | **Field overflow blocked** | `assert amount <= MAX_AMOUNT` (2⁵³-1) — `amount × feeBps` stays within safe range |
61
+ | **Receipt deduplication** | Nullifier insertion before `receiptTree.insert` — double-completion impossible regardless of proof reuse |
62
+
63
+ ## Board Security Guarantees
64
+
65
+ | Guarantee | How It's Enforced |
66
+ |---|---|
67
+ | **Persistent storage** | `~/.nightpay/board.db` — SQLite with WAL, survives reboots, not `/tmp/` |
68
+ | **Directory permissions** | `chmod 700` — only the operator user can read/write |
69
+ | **No duplicate entries** | `PRIMARY KEY` on commitment — database rejects duplicates atomically |
70
+ | **Input validation** | All commitments validated as 64-char hex before board operations |
71
+ | **Known status values only** | `remove` only accepts `completed`, `refunded`, `expired` |
72
+ | **Search DoS blocked** | `search` prefix validated as `[0-9a-f]{0,64}` — no wildcards, no full-table scans |
73
+ | **Integer DoS blocked** | `list` clamps `limit ≤ 200`, `offset ≤ 10,000,000` — no memory exhaustion |
74
+
75
+ ## Network Fees (DUST)
76
+
77
+ - DUST is non-transferable — generated over time from NIGHT holdings
78
+ - DUST is used ONLY for Midnight network fees, never for bounty payments
79
+ - Whoever submits the intent pays the DUST fee (payer for postBounty, gateway for complete)
80
+ - Use `transaction.feesWithMargin(params, 1.2)` to estimate with 20% safety margin
81
+ - DUST generation rate: ~1 week to reach full capacity from a NIGHT UTXO
82
+ - 3-hour grace period on DUST timestamps
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
+
129
+ ## Timeout Handling
130
+
131
+ - Every bounty escrow has a configurable timeout (default: 60 minutes)
132
+ - If the hired agent does not return a result within the timeout:
133
+ 1. Masumi escrow is cancelled on Cardano (funds return to gateway)
134
+ 2. Gateway emits a signed NIGHT refund intent for the Midnight contract
135
+ 3. No receipt token is minted
136
+ 4. Bounty commitment stays in the Merkle tree — nullifier set prevents re-claim
137
+
138
+ ## Refund Conditions
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
+
149
+ Automatic refund triggers:
150
+ - Agent returns an error or refuses the job
151
+ - Agent is unreachable (Masumi `/status` returns unavailable)
152
+ - Job result fails validation (output hash mismatch)
153
+ - Escrow timeout exceeded
154
+
155
+ On refund: the operator fee for that bounty is also returned (fee is only collected on success).
156
+
157
+ ## Amount Limits
158
+
159
+ - `maxBountySpecks` caps the maximum single bounty (default: 500M specks)
160
+ - `minBountySpecks` enforces a minimum (default: 1,000 specks) — rejects dust attacks
161
+ - Both limits enforced in gateway before any network call
162
+
163
+ ## Dark Energy Threat Model (Sophisticated Attacker Mitigations)
164
+
165
+ These are attacks that pass all basic validation but exploit deeper system properties.
166
+
167
+ | Attack | Vector | Mitigation |
168
+ |---|---|---|
169
+ | **Gateway address injection** | Caller supplies their own address in tx metadata → all bounty NIGHT routes to them | `gatewayAddress` locked in contract state at `initialize()` — `effects.releaseToAddress(gatewayAddress, net)` uses only the immutable on-chain value |
170
+ | **completedCount overflow griefing** | Attacker posts + completes millions of dust bounties → counter wraps → contract corrupted | `assert completedCount < MAX_COMPLETED` in-circuit before every increment |
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 |
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 |
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 |
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 |
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 |
184
+
185
+ ## Never Do
186
+
187
+ - Never release Masumi escrow before the completeAndReceipt circuit succeeds on Midnight
188
+ - Never hold funds beyond the timeout period
189
+ - Never charge fees on refunded bounties
190
+ - Never split a single bounty across multiple agents without explicit requester consent
191
+ - Never submit intents without sufficient DUST balance (check with `feesWithMargin`)
192
+ - Never run withdraw-fees without OPERATOR_SECRET_KEY set
193
+ - Never store the board in /tmp or any world-readable location
194
+ - Never accept unvalidated commitment hashes — always check 64-char hex format first
@@ -1,51 +1,51 @@
1
- # Privacy-First Rule
2
-
3
- ## Absolute Requirements
4
-
5
- 1. **Never log payer identity** — not in memory, not in daily logs, not in conversation history
6
- 2. **Never associate** a Cardano address with a bounty description in any stored format
7
- 3. **All payment amounts** are private witnesses in the Compact circuit — they never appear in public state
8
- 4. **Agent DIDs** used for completion are also shielded — the public only sees that "a bounty was completed"
9
- 5. **Salt values** are generated per-bounty and discarded after commitment — never reuse salts
10
-
11
- ## What IS Public
12
-
13
- - The total count of completed bounties (aggregate metric only)
14
- - The fact that the receipt contract exists on Midnight
15
- - ZK proofs that can verify a specific receipt is valid (if the holder presents it)
16
-
17
- ## What is NEVER Public
18
-
19
- - Who posted the bounty
20
- - How much was paid
21
- - Which agent completed it
22
- - What the job description was
23
- - The connection between any Cardano payment and any Midnight receipt
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
-
45
- ## Implementation Notes
46
-
47
- - Use `crypto.randomBytes(32)` for salt generation
48
- - Hash payer addresses before even passing them to the circuit as witnesses
49
- - Clear any in-memory payment context after the escrow settles
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
1
+ # Privacy-First Rule
2
+
3
+ ## Absolute Requirements
4
+
5
+ 1. **Never log payer identity** — not in memory, not in daily logs, not in conversation history
6
+ 2. **Never associate** a Cardano address with a bounty description in any stored format
7
+ 3. **All payment amounts** are private witnesses in the Compact circuit — they never appear in public state
8
+ 4. **Agent DIDs** used for completion are also shielded — the public only sees that "a bounty was completed"
9
+ 5. **Salt values** are generated per-bounty and discarded after commitment — never reuse salts
10
+
11
+ ## What IS Public
12
+
13
+ - The total count of completed bounties (aggregate metric only)
14
+ - The fact that the receipt contract exists on Midnight
15
+ - ZK proofs that can verify a specific receipt is valid (if the holder presents it)
16
+
17
+ ## What is NEVER Public
18
+
19
+ - Who posted the bounty
20
+ - How much was paid
21
+ - Which agent completed it
22
+ - What the job description was
23
+ - The connection between any Cardano payment and any Midnight receipt
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
+
45
+ ## Implementation Notes
46
+
47
+ - Use `crypto.randomBytes(32)` for salt generation
48
+ - Hash payer addresses before even passing them to the circuit as witnesses
49
+ - Clear any in-memory payment context after the escrow settles
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
@@ -1,45 +1,45 @@
1
- # ZK Receipt Format
2
-
3
- ## What a Receipt Proves
4
-
5
- A ZK receipt is a Midnight proof that certifies:
6
- - A bounty was posted (commitment existed)
7
- - An agent completed the work (output hash was provided)
8
- - The escrow was settled (receipt was minted)
9
-
10
- Without revealing:
11
- - Who posted the bounty
12
- - How much was paid
13
- - Which agent did the work
14
- - What the work was
15
-
16
- ## Receipt Data Schema
17
-
18
- ```
19
- receipt = {
20
- receiptHash: Bytes32, // The on-chain receipt identifier
21
- commitmentHash: Bytes32, // Which bounty this completes (also opaque)
22
- zkProof: MidnightProof, // Midnight ZK proof of valid completion
23
- timestamp: ISO8601, // When the receipt was minted
24
- contractAddress: string, // Midnight contract that holds the receipt
25
- cardanoTxHash: string // Cardano tx where escrow settled (public, but unlinkable to receipt)
26
- }
27
- ```
28
-
29
- ## Verification
30
-
31
- Anyone can verify a receipt by calling `verifyReceipt(receiptHash)` on the Midnight contract.
32
- This returns `true` or `false` without revealing any details about the bounty.
33
-
34
- ## Use Cases for Receipts
35
-
36
- - **Reputation**: an agent can accumulate receipt count as proof of work completed
37
- - **Dispute resolution**: the payer holds the commitment salt and can prove they posted the bounty
38
- - **Auditing**: the community can see aggregate completion count without individual details
39
- - **Portfolio**: an agent can present receipts as credentials without doxxing their clients
40
-
41
- ## Linking Receipt to Dispute
42
-
43
- If a dispute arises, the payer can reveal their `salt` to prove they own a specific commitment.
44
- The agent can reveal the `outputHash` to prove what was delivered.
45
- Neither party needs to reveal the other's identity.
1
+ # ZK Receipt Format
2
+
3
+ ## What a Receipt Proves
4
+
5
+ A ZK receipt is a Midnight proof that certifies:
6
+ - A bounty was posted (commitment existed)
7
+ - An agent completed the work (output hash was provided)
8
+ - The escrow was settled (receipt was minted)
9
+
10
+ Without revealing:
11
+ - Who posted the bounty
12
+ - How much was paid
13
+ - Which agent did the work
14
+ - What the work was
15
+
16
+ ## Receipt Data Schema
17
+
18
+ ```
19
+ receipt = {
20
+ receiptHash: Bytes32, // The on-chain receipt identifier
21
+ commitmentHash: Bytes32, // Which bounty this completes (also opaque)
22
+ zkProof: MidnightProof, // Midnight ZK proof of valid completion
23
+ timestamp: ISO8601, // When the receipt was minted
24
+ contractAddress: string, // Midnight contract that holds the receipt
25
+ cardanoTxHash: string // Cardano tx where escrow settled (public, but unlinkable to receipt)
26
+ }
27
+ ```
28
+
29
+ ## Verification
30
+
31
+ Anyone can verify a receipt by calling `verifyReceipt(receiptHash)` on the Midnight contract.
32
+ This returns `true` or `false` without revealing any details about the bounty.
33
+
34
+ ## Use Cases for Receipts
35
+
36
+ - **Reputation**: an agent can accumulate receipt count as proof of work completed
37
+ - **Dispute resolution**: the payer holds the commitment salt and can prove they posted the bounty
38
+ - **Auditing**: the community can see aggregate completion count without individual details
39
+ - **Portfolio**: an agent can present receipts as credentials without doxxing their clients
40
+
41
+ ## Linking Receipt to Dispute
42
+
43
+ If a dispute arises, the payer can reveal their `salt` to prove they own a specific commitment.
44
+ The agent can reveal the `outputHash` to prove what was delivered.
45
+ Neither party needs to reveal the other's identity.