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.
- package/LICENSE +666 -666
- package/README.md +456 -408
- package/bin/cli.js +558 -558
- package/nightpay_sdk.py +398 -398
- package/openclaw.plugin.json +10 -10
- package/package.json +50 -51
- package/plugin.js +712 -379
- package/skills/nightpay/AGENTS.md +302 -302
- package/skills/nightpay/HEARTBEAT.md +55 -55
- package/skills/nightpay/SKILL.md +471 -447
- package/skills/nightpay/contracts/receipt.compact +456 -390
- package/skills/nightpay/ontology/ontology.jsonld +395 -395
- package/skills/nightpay/ontology/ontology.md +243 -224
- package/skills/nightpay/openclaw-fragment.json +21 -21
- package/skills/nightpay/rules/content-safety.md +187 -187
- package/skills/nightpay/rules/escrow-safety.md +194 -194
- package/skills/nightpay/rules/privacy-first.md +51 -51
- package/skills/nightpay/rules/receipt-format.md +45 -45
- package/skills/nightpay/scripts/bounty-board.sh +325 -325
- package/skills/nightpay/scripts/gateway.sh +1474 -1474
- package/skills/nightpay/scripts/mip003-server.sh +4820 -4625
- package/skills/nightpay/scripts/update-blocklist.sh +194 -194
- package/bin/deploy-hetzner-ci.sh +0 -428
- package/scripts/agent-playground-setup.sh +0 -384
- package/scripts/load-sim.sh +0 -1324
- package/scripts/server-sync-start.sh +0 -237
|
@@ -1,390 +1,456 @@
|
|
|
1
|
-
// NightPay — Midnight Compact contract (ledger-compatible)
|
|
2
|
-
//
|
|
3
|
-
// Built against: midnightntwrk/midnight-ledger spec.
|
|
4
|
-
// Aligns with Midnight concepts: public/secret ledger state, UTXO-style nullifier set,
|
|
5
|
-
// commitment/nullifier pattern (see docs.midnight.network/concepts and
|
|
6
|
-
// docs.midnight.network/concepts/how-midnight-works/keeping-data-private).
|
|
7
|
-
//
|
|
8
|
-
// SECURITY MODEL:
|
|
9
|
-
// - initialize() can only be called once — locked forever after
|
|
10
|
-
// - withdrawFees() is gated to the operator address set at init
|
|
11
|
-
// - operatorFeeBps and operatorAddress are IMMUTABLE after initialization
|
|
12
|
-
// - Fee split is enforced in-circuit with constrained balance effects
|
|
13
|
-
// - activeCount is underflow-guarded, completedCount is overflow-guarded
|
|
14
|
-
// - All domain-separated hashes prevent cross-namespace collisions
|
|
15
|
-
// - Gateway address is locked at init — cannot be injected via transaction metadata
|
|
16
|
-
// - Total bounty throughput capped to prevent counter exhaustion griefing
|
|
17
|
-
// - Pool funding uses equal contributions — each funder pays exactly contributionAmount
|
|
18
|
-
// - Refunds are funder-initiated — funder proves their contribution via funding tree
|
|
19
|
-
// - Pool expiry is off-chain (gateway sets expired flag) — Compact has no time primitives
|
|
20
|
-
// - Double-funding, double-refund prevented by nullifier set
|
|
21
|
-
|
|
22
|
-
pragma language_version >= 0.19;
|
|
23
|
-
|
|
24
|
-
import CompactStandardLibrary;
|
|
25
|
-
|
|
26
|
-
module NightPay {
|
|
27
|
-
|
|
28
|
-
// ─── Public ledger state ─────────────────────────────────────────────────────
|
|
29
|
-
ledger completedCount: Counter;
|
|
30
|
-
ledger activeCount: Counter;
|
|
31
|
-
ledger poolCount: Counter;
|
|
32
|
-
ledger operatorFeeBps: Field;
|
|
33
|
-
ledger operatorAddress: Bytes<32>;
|
|
34
|
-
|
|
35
|
-
// SECURITY: one-time init lock — set to 1 after initialize() runs
|
|
36
|
-
ledger initialized: Field;
|
|
37
|
-
|
|
38
|
-
// DARK ENERGY: gateway address locked at init — cannot be injected per-transaction.
|
|
39
|
-
ledger gatewayAddress: Bytes<32>;
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
ledger
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
assert
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
witness
|
|
143
|
-
witness
|
|
144
|
-
witness
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
assert
|
|
149
|
-
assert
|
|
150
|
-
assert
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
// SECURITY:
|
|
163
|
-
assert !nullifiers.contains(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
assert
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// SECURITY:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
assert
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
txCounter.increment(1);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
assert
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
witness
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
assert
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1
|
+
// NightPay — Midnight Compact contract (ledger-compatible)
|
|
2
|
+
//
|
|
3
|
+
// Built against: midnightntwrk/midnight-ledger spec.
|
|
4
|
+
// Aligns with Midnight concepts: public/secret ledger state, UTXO-style nullifier set,
|
|
5
|
+
// commitment/nullifier pattern (see docs.midnight.network/concepts and
|
|
6
|
+
// docs.midnight.network/concepts/how-midnight-works/keeping-data-private).
|
|
7
|
+
//
|
|
8
|
+
// SECURITY MODEL:
|
|
9
|
+
// - initialize() can only be called once — locked forever after
|
|
10
|
+
// - withdrawFees() is gated to the operator address set at init
|
|
11
|
+
// - operatorFeeBps and operatorAddress are IMMUTABLE after initialization
|
|
12
|
+
// - Fee split is enforced in-circuit with constrained balance effects
|
|
13
|
+
// - activeCount is underflow-guarded, completedCount is overflow-guarded
|
|
14
|
+
// - All domain-separated hashes prevent cross-namespace collisions
|
|
15
|
+
// - Gateway address is locked at init — cannot be injected via transaction metadata
|
|
16
|
+
// - Total bounty throughput capped to prevent counter exhaustion griefing
|
|
17
|
+
// - Pool funding uses equal contributions — each funder pays exactly contributionAmount
|
|
18
|
+
// - Refunds are funder-initiated — funder proves their contribution via funding tree
|
|
19
|
+
// - Pool expiry is off-chain (gateway sets expired flag) — Compact has no time primitives
|
|
20
|
+
// - Double-funding, double-refund prevented by nullifier set
|
|
21
|
+
|
|
22
|
+
pragma language_version >= 0.19;
|
|
23
|
+
|
|
24
|
+
import CompactStandardLibrary;
|
|
25
|
+
|
|
26
|
+
module NightPay {
|
|
27
|
+
|
|
28
|
+
// ─── Public ledger state ─────────────────────────────────────────────────────
|
|
29
|
+
ledger completedCount: Counter;
|
|
30
|
+
ledger activeCount: Counter;
|
|
31
|
+
ledger poolCount: Counter;
|
|
32
|
+
ledger operatorFeeBps: Field;
|
|
33
|
+
ledger operatorAddress: Bytes<32>;
|
|
34
|
+
|
|
35
|
+
// SECURITY: one-time init lock — set to 1 after initialize() runs
|
|
36
|
+
ledger initialized: Field;
|
|
37
|
+
|
|
38
|
+
// DARK ENERGY: gateway address locked at init — cannot be injected per-transaction.
|
|
39
|
+
ledger gatewayAddress: Bytes<32>;
|
|
40
|
+
|
|
41
|
+
// Total number of individual funding contributions — used for fundingTree capacity guard.
|
|
42
|
+
ledger fundingCount: Counter;
|
|
43
|
+
|
|
44
|
+
// H-1: per-pool running total of contributions — activatePool verifies totalFunded against this.
|
|
45
|
+
ledger poolFundedAmounts: Map<Bytes<32>, Field>;
|
|
46
|
+
|
|
47
|
+
// H-2: running total of infrastructure fees retained — caps what the operator can withdraw.
|
|
48
|
+
ledger accumulatedFees: Counter;
|
|
49
|
+
|
|
50
|
+
// H-3: gateway authentication key (bboard pattern) — derived from gateway secret at init.
|
|
51
|
+
// expirePool and activatePool verify the caller knows the corresponding secret key.
|
|
52
|
+
ledger gatewayAuthKey: Bytes<32>;
|
|
53
|
+
|
|
54
|
+
// FAILSAFE: monotonic transaction counter — incremented on every state-changing call.
|
|
55
|
+
// Used as a crude on-chain clock for emergency refunds when the gateway disappears.
|
|
56
|
+
// Compact has no block/time primitives, so this is the best proxy: after enough
|
|
57
|
+
// contract interactions have occurred, funders can self-rescue without the gateway.
|
|
58
|
+
ledger txCounter: Counter;
|
|
59
|
+
|
|
60
|
+
// ─── Private state (shielded via ZK) ────────────────────────────────────────
|
|
61
|
+
secret ledger bountyTree: MerkleTree<25>;
|
|
62
|
+
secret ledger receiptTree: MerkleTree<25>;
|
|
63
|
+
secret ledger poolTree: MerkleTree<25>; // pool commitment records
|
|
64
|
+
secret ledger fundingTree: MerkleTree<25>; // individual funding records (for refund proofs)
|
|
65
|
+
secret ledger nullifiers: Set<Bytes<32>>;
|
|
66
|
+
|
|
67
|
+
// ─── Domain separation prefixes ─────────────────────────────────────────────
|
|
68
|
+
const DOMAIN_BOUNTY: Bytes<9> = 0x626f756e7479303030; // "bounty000"
|
|
69
|
+
const DOMAIN_RECEIPT: Bytes<9> = 0x726563656970743030; // "receipt00"
|
|
70
|
+
const DOMAIN_NULLIFIER: Bytes<9> = 0x6e756c6c6966696572; // "nullifier"
|
|
71
|
+
const DOMAIN_POOL: Bytes<8> = 0x706f6f6c30303030; // "pool0000"
|
|
72
|
+
const DOMAIN_FUNDING: Bytes<9> = 0x66756e64696e673030; // "funding00"
|
|
73
|
+
const DOMAIN_REFUND: Bytes<9> = 0x726566756e64303030; // "refund000"
|
|
74
|
+
const DOMAIN_EMERGENCY: Bytes<9> = 0x656d657267656e6379; // "emergency"
|
|
75
|
+
|
|
76
|
+
// ─── Capacity limits ─────────────────────────────────────────────────────────
|
|
77
|
+
const MAX_TREE_ENTRIES: Field = 30199000; // ~90% of 2^25
|
|
78
|
+
const MAX_COMPLETED: Field = 30199000;
|
|
79
|
+
|
|
80
|
+
// SECURITY: Field arithmetic overflow guard.
|
|
81
|
+
const MAX_AMOUNT: Field = 9007199254740991; // 2^53 - 1
|
|
82
|
+
|
|
83
|
+
// SECURITY: max funders per pool — prevents gas griefing on activate/refund
|
|
84
|
+
const MAX_POOL_FUNDERS: Field = 1000;
|
|
85
|
+
|
|
86
|
+
// FAILSAFE: emergency refund threshold — if txCounter advances by this many
|
|
87
|
+
// transactions beyond the fundedAtTx recorded in the funding record,
|
|
88
|
+
// the funder can self-rescue without waiting for expirePool.
|
|
89
|
+
// ~500 contract calls ≈ days/weeks of normal usage — long enough for the
|
|
90
|
+
// gateway to act, short enough that funds aren't locked forever.
|
|
91
|
+
const EMERGENCY_TX_THRESHOLD: Field = 500;
|
|
92
|
+
|
|
93
|
+
// ─── Gateway authentication (H-3: bboard pattern) ───────────────────────────
|
|
94
|
+
// The bridge implements localGatewaySecretKey() in witnesses.ts, returning its
|
|
95
|
+
// stored secret key from private state (LevelDB). The public key is derived
|
|
96
|
+
// in-circuit — the secret never leaves the prover.
|
|
97
|
+
witness localGatewaySecretKey(): Bytes<32>;
|
|
98
|
+
|
|
99
|
+
pure circuit gatewayPublicKey(sk: Bytes<32>): Bytes<32> {
|
|
100
|
+
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "nightpay:gateway:v1"), sk]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Internal guard — call at the top of any gateway-only circuit.
|
|
104
|
+
circuit assertGateway(): [] {
|
|
105
|
+
const sk = localGatewaySecretKey();
|
|
106
|
+
const derived = gatewayPublicKey(sk);
|
|
107
|
+
assert disclose(derived == gatewayAuthKey);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Circuits ───────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
// SECURITY: guarded by initialized flag — reverts if called twice.
|
|
113
|
+
export circuit initialize(
|
|
114
|
+
operatorAddr: Bytes<32>,
|
|
115
|
+
gatewayAddr: Bytes<32>,
|
|
116
|
+
feeBps: Field
|
|
117
|
+
): [] {
|
|
118
|
+
assert initialized == 0;
|
|
119
|
+
assert feeBps <= 500;
|
|
120
|
+
assert feeBps >= 0;
|
|
121
|
+
|
|
122
|
+
operatorAddress = operatorAddr;
|
|
123
|
+
gatewayAddress = gatewayAddr;
|
|
124
|
+
operatorFeeBps = feeBps;
|
|
125
|
+
initialized = 1;
|
|
126
|
+
|
|
127
|
+
// H-3: derive and store the gateway's auth key from its secret key.
|
|
128
|
+
// The bridge calls initialize, so localGatewaySecretKey() returns the
|
|
129
|
+
// bridge's secret. From this point on, expirePool + activatePool require
|
|
130
|
+
// proof of knowing this secret.
|
|
131
|
+
gatewayAuthKey = gatewayPublicKey(localGatewaySecretKey());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Pool Lifecycle ───────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
// createPool: creates a bounty pool with a funding goal and fixed contribution amount.
|
|
137
|
+
// No funds move yet — this just records the pool parameters in the pool tree.
|
|
138
|
+
// SECURITY: contribution × maxFunders must equal fundingGoal (exact division enforced).
|
|
139
|
+
export circuit createPool(
|
|
140
|
+
witness jobHash: Bytes<32>,
|
|
141
|
+
witness fundingGoal: Field,
|
|
142
|
+
witness contributionAmount: Field,
|
|
143
|
+
witness maxFunders: Field,
|
|
144
|
+
witness nonce: Bytes<32>
|
|
145
|
+
): Bytes<32> {
|
|
146
|
+
assert initialized == 1;
|
|
147
|
+
assert fundingGoal > 0;
|
|
148
|
+
assert contributionAmount > 0;
|
|
149
|
+
assert maxFunders > 0;
|
|
150
|
+
assert maxFunders <= MAX_POOL_FUNDERS;
|
|
151
|
+
assert fundingGoal <= MAX_AMOUNT;
|
|
152
|
+
assert contributionAmount <= MAX_AMOUNT;
|
|
153
|
+
|
|
154
|
+
// SECURITY: enforce exact division — no rounding dust
|
|
155
|
+
assert contributionAmount * maxFunders == fundingGoal;
|
|
156
|
+
|
|
157
|
+
// SECURITY: tree capacity check
|
|
158
|
+
assert poolCount < MAX_TREE_ENTRIES;
|
|
159
|
+
|
|
160
|
+
const poolCommitment = hash(DOMAIN_POOL, jobHash, fundingGoal, contributionAmount, maxFunders, nonce);
|
|
161
|
+
|
|
162
|
+
// SECURITY: pool commitment must be fresh
|
|
163
|
+
assert !nullifiers.contains(poolCommitment);
|
|
164
|
+
|
|
165
|
+
poolTree.insert(poolCommitment);
|
|
166
|
+
poolCount.increment(1);
|
|
167
|
+
txCounter.increment(1);
|
|
168
|
+
|
|
169
|
+
return poolCommitment;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// fundPool: funder contributes exactly contributionAmount NIGHT to a pool.
|
|
173
|
+
// Records a funding entry in fundingTree (used for refund proofs later).
|
|
174
|
+
// The current txCounter is baked into the funding record hash — this lets
|
|
175
|
+
// emergencyRefund verify that enough contract interactions have passed.
|
|
176
|
+
// SECURITY: funder sends shielded NIGHT — identity destroyed by nullifier model.
|
|
177
|
+
export circuit fundPool(
|
|
178
|
+
witness funderNullifier: Bytes<32>,
|
|
179
|
+
witness poolCommitment: Bytes<32>,
|
|
180
|
+
witness poolMerkleProof: Proof<25>,
|
|
181
|
+
witness contributionAmount: Field,
|
|
182
|
+
witness nonce: Bytes<32>
|
|
183
|
+
): Bytes<32> {
|
|
184
|
+
assert initialized == 1;
|
|
185
|
+
assert contributionAmount > 0;
|
|
186
|
+
assert contributionAmount <= MAX_AMOUNT;
|
|
187
|
+
|
|
188
|
+
// SECURITY: pool must exist
|
|
189
|
+
assert poolTree.verify(poolCommitment, poolMerkleProof);
|
|
190
|
+
|
|
191
|
+
// Capture txCounter at fund time — baked into the funding record for emergency refund
|
|
192
|
+
const fundedAtTx = txCounter;
|
|
193
|
+
|
|
194
|
+
// SECURITY: domain-separated funding record — links funder to pool for refund proofs.
|
|
195
|
+
// Includes fundedAtTx so emergencyRefund can verify time-passage without the gateway.
|
|
196
|
+
const fundingRecord = hash(DOMAIN_FUNDING, funderNullifier, poolCommitment, contributionAmount, fundedAtTx, nonce);
|
|
197
|
+
|
|
198
|
+
// SECURITY: prevent double-funding by the same funder with same nullifier
|
|
199
|
+
assert !nullifiers.contains(fundingRecord);
|
|
200
|
+
nullifiers.insert(fundingRecord);
|
|
201
|
+
|
|
202
|
+
// SECURITY: tree capacity check (M-2 fix — use fundingCount, not activeCount)
|
|
203
|
+
assert fundingCount < MAX_TREE_ENTRIES;
|
|
204
|
+
|
|
205
|
+
fundingTree.insert(fundingRecord);
|
|
206
|
+
fundingCount.increment(1);
|
|
207
|
+
|
|
208
|
+
// H-1: track running total funded per pool — activatePool will verify against this.
|
|
209
|
+
const prevFunded = poolFundedAmounts.lookup(poolCommitment);
|
|
210
|
+
poolFundedAmounts.insert(poolCommitment, prevFunded + contributionAmount);
|
|
211
|
+
|
|
212
|
+
// Hold funds in contract — no release until pool activates
|
|
213
|
+
effects.retainInContract(contributionAmount);
|
|
214
|
+
|
|
215
|
+
txCounter.increment(1);
|
|
216
|
+
|
|
217
|
+
return fundingRecord;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// activatePool: called by gateway when funding goal is met.
|
|
221
|
+
// Deducts infrastructure fee and releases net funds to gateway for Masumi escrow.
|
|
222
|
+
// SECURITY: pool must exist. Gateway triggers this off-chain when goal is confirmed.
|
|
223
|
+
export circuit activatePool(
|
|
224
|
+
witness poolCommitment: Bytes<32>,
|
|
225
|
+
witness poolMerkleProof: Proof<25>,
|
|
226
|
+
witness totalFunded: Field
|
|
227
|
+
): [] {
|
|
228
|
+
assert initialized == 1;
|
|
229
|
+
assert totalFunded > 0;
|
|
230
|
+
assert totalFunded <= MAX_AMOUNT;
|
|
231
|
+
|
|
232
|
+
// H-3: only the gateway can activate pools
|
|
233
|
+
assertGateway();
|
|
234
|
+
|
|
235
|
+
// SECURITY: pool must exist
|
|
236
|
+
assert poolTree.verify(poolCommitment, poolMerkleProof);
|
|
237
|
+
|
|
238
|
+
// SECURITY: prevent double-activation
|
|
239
|
+
assert !nullifiers.contains(poolCommitment);
|
|
240
|
+
|
|
241
|
+
// SECURITY: prevent activating an expired pool (C-1 fix)
|
|
242
|
+
// expirePool inserts hash(DOMAIN_REFUND, poolCommitment) — if that exists,
|
|
243
|
+
// funders have already been allowed to refund. Activating now would be a double-spend.
|
|
244
|
+
const expiredMarker = hash(DOMAIN_REFUND, poolCommitment);
|
|
245
|
+
assert !nullifiers.contains(expiredMarker);
|
|
246
|
+
|
|
247
|
+
nullifiers.insert(poolCommitment);
|
|
248
|
+
|
|
249
|
+
// H-1: verify totalFunded matches the on-chain sum of contributions
|
|
250
|
+
const onChainTotal = poolFundedAmounts.lookup(poolCommitment);
|
|
251
|
+
assert disclose(totalFunded == onChainTotal);
|
|
252
|
+
|
|
253
|
+
// Fee split
|
|
254
|
+
const fee = totalFunded * operatorFeeBps / 10000;
|
|
255
|
+
const netAmount = totalFunded - fee;
|
|
256
|
+
assert fee + netAmount == totalFunded;
|
|
257
|
+
|
|
258
|
+
// H-2: record accumulated fees so withdrawFees can enforce a cap
|
|
259
|
+
accumulatedFees.increment(fee);
|
|
260
|
+
|
|
261
|
+
effects.retainInContract(fee);
|
|
262
|
+
effects.releaseToAddress(gatewayAddress, netAmount);
|
|
263
|
+
|
|
264
|
+
activeCount.increment(1);
|
|
265
|
+
txCounter.increment(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// claimRefund: funder reclaims their contribution from an expired pool.
|
|
269
|
+
// SECURITY: funder proves they funded the pool via fundingTree proof.
|
|
270
|
+
// Gateway must have marked the pool as expired (expirePool nullifies the pool commitment
|
|
271
|
+
// with a refund-domain hash, which this circuit checks).
|
|
272
|
+
export circuit claimRefund(
|
|
273
|
+
witness fundingRecord: Bytes<32>,
|
|
274
|
+
witness fundingMerkleProof: Proof<25>,
|
|
275
|
+
witness poolCommitment: Bytes<32>,
|
|
276
|
+
witness contributionAmount: Field,
|
|
277
|
+
witness funderAddress: Bytes<32>
|
|
278
|
+
): [] {
|
|
279
|
+
assert initialized == 1;
|
|
280
|
+
assert contributionAmount > 0;
|
|
281
|
+
assert contributionAmount <= MAX_AMOUNT;
|
|
282
|
+
|
|
283
|
+
// SECURITY: funding record must exist — proves this funder actually contributed
|
|
284
|
+
assert fundingTree.verify(fundingRecord, fundingMerkleProof);
|
|
285
|
+
|
|
286
|
+
// SECURITY: pool must be expired — checked via refund-domain nullifier
|
|
287
|
+
// The gateway calls expirePool which inserts hash(DOMAIN_REFUND, poolCommitment)
|
|
288
|
+
// into the nullifier set. If this doesn't exist, the pool isn't expired yet.
|
|
289
|
+
const expiredMarker = hash(DOMAIN_REFUND, poolCommitment);
|
|
290
|
+
assert nullifiers.contains(expiredMarker);
|
|
291
|
+
|
|
292
|
+
// SECURITY: prevent double-refund — nullify the funding record
|
|
293
|
+
const refundNullifier = hash(DOMAIN_NULLIFIER, fundingRecord);
|
|
294
|
+
assert !nullifiers.contains(refundNullifier);
|
|
295
|
+
nullifiers.insert(refundNullifier);
|
|
296
|
+
|
|
297
|
+
// Return full contribution — no fee on expired pools
|
|
298
|
+
effects.releaseToAddress(funderAddress, contributionAmount);
|
|
299
|
+
|
|
300
|
+
txCounter.increment(1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// expirePool: gateway marks a pool as expired (deadline passed, goal not met).
|
|
304
|
+
// After this, funders can call claimRefund.
|
|
305
|
+
// SECURITY: only the gateway can call this (same trust model as escrow timeout).
|
|
306
|
+
export circuit expirePool(
|
|
307
|
+
witness poolCommitment: Bytes<32>,
|
|
308
|
+
witness poolMerkleProof: Proof<25>
|
|
309
|
+
): [] {
|
|
310
|
+
assert initialized == 1;
|
|
311
|
+
|
|
312
|
+
// H-3: only the gateway can expire pools
|
|
313
|
+
assertGateway();
|
|
314
|
+
|
|
315
|
+
// SECURITY: pool must exist
|
|
316
|
+
assert poolTree.verify(poolCommitment, poolMerkleProof);
|
|
317
|
+
|
|
318
|
+
// SECURITY: pool must not already be activated or expired
|
|
319
|
+
assert !nullifiers.contains(poolCommitment);
|
|
320
|
+
|
|
321
|
+
// Insert refund-domain marker — claimRefund checks for this
|
|
322
|
+
const expiredMarker = hash(DOMAIN_REFUND, poolCommitment);
|
|
323
|
+
assert !nullifiers.contains(expiredMarker);
|
|
324
|
+
nullifiers.insert(expiredMarker);
|
|
325
|
+
|
|
326
|
+
txCounter.increment(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// emergencyRefund: FAILSAFE — funder can self-rescue if the gateway disappears.
|
|
330
|
+
// Does NOT require expirePool to have been called. Instead, checks that enough
|
|
331
|
+
// contract interactions (txCounter) have passed since the funder's contribution.
|
|
332
|
+
// The funder must supply the same witness values used at fundPool time so the
|
|
333
|
+
// circuit can recompute the funding record hash and verify it exists in the tree.
|
|
334
|
+
//
|
|
335
|
+
// SECURITY: this is a last-resort escape hatch. Under normal operation, the
|
|
336
|
+
// gateway calls expirePool and funders use claimRefund (cheaper, faster).
|
|
337
|
+
// emergencyRefund exists so that funds are NEVER permanently locked.
|
|
338
|
+
export circuit emergencyRefund(
|
|
339
|
+
witness funderNullifier: Bytes<32>,
|
|
340
|
+
witness poolCommitment: Bytes<32>,
|
|
341
|
+
witness contributionAmount: Field,
|
|
342
|
+
witness fundedAtTx: Field,
|
|
343
|
+
witness nonce: Bytes<32>,
|
|
344
|
+
witness fundingMerkleProof: Proof<25>,
|
|
345
|
+
witness funderAddress: Bytes<32>
|
|
346
|
+
): [] {
|
|
347
|
+
assert initialized == 1;
|
|
348
|
+
assert contributionAmount > 0;
|
|
349
|
+
assert contributionAmount <= MAX_AMOUNT;
|
|
350
|
+
|
|
351
|
+
// Recompute the funding record — must match what fundPool produced
|
|
352
|
+
const fundingRecord = hash(DOMAIN_FUNDING, funderNullifier, poolCommitment, contributionAmount, fundedAtTx, nonce);
|
|
353
|
+
|
|
354
|
+
// SECURITY: funding record must exist in the tree
|
|
355
|
+
assert fundingTree.verify(fundingRecord, fundingMerkleProof);
|
|
356
|
+
|
|
357
|
+
// FAILSAFE: enough contract interactions must have passed
|
|
358
|
+
assert txCounter >= fundedAtTx + EMERGENCY_TX_THRESHOLD;
|
|
359
|
+
|
|
360
|
+
// SECURITY: pool must NOT have been activated — if it was activated, funds
|
|
361
|
+
// were already released to gateway and cannot be double-claimed
|
|
362
|
+
assert !nullifiers.contains(poolCommitment);
|
|
363
|
+
|
|
364
|
+
// SECURITY: prevent double-refund — same nullifier as claimRefund
|
|
365
|
+
const refundNullifier = hash(DOMAIN_NULLIFIER, fundingRecord);
|
|
366
|
+
assert !nullifiers.contains(refundNullifier);
|
|
367
|
+
nullifiers.insert(refundNullifier);
|
|
368
|
+
|
|
369
|
+
// Return full contribution — no fee on emergency refunds
|
|
370
|
+
effects.releaseToAddress(funderAddress, contributionAmount);
|
|
371
|
+
|
|
372
|
+
txCounter.increment(1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ─── Bounty Lifecycle (linked to pools) ───────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
// postBounty: posts a bounty linked to an activated pool.
|
|
378
|
+
// SECURITY: fee already deducted during activatePool — no fee split here.
|
|
379
|
+
export circuit postBounty(
|
|
380
|
+
witness payerNullifier: Bytes<32>,
|
|
381
|
+
witness amount: Field,
|
|
382
|
+
witness jobHash: Bytes<32>,
|
|
383
|
+
witness poolCommitment: Bytes<32>,
|
|
384
|
+
witness nonce: Bytes<32>
|
|
385
|
+
): Bytes<32> {
|
|
386
|
+
assert initialized == 1;
|
|
387
|
+
assert amount > 0;
|
|
388
|
+
assert amount <= MAX_AMOUNT;
|
|
389
|
+
assert activeCount < MAX_TREE_ENTRIES;
|
|
390
|
+
|
|
391
|
+
// SECURITY: pool must have been activated (its commitment is in the nullifier set)
|
|
392
|
+
assert nullifiers.contains(poolCommitment);
|
|
393
|
+
|
|
394
|
+
const commitment = hash(DOMAIN_BOUNTY, payerNullifier, amount, jobHash, poolCommitment, nonce);
|
|
395
|
+
assert !nullifiers.contains(commitment);
|
|
396
|
+
|
|
397
|
+
bountyTree.insert(commitment);
|
|
398
|
+
|
|
399
|
+
txCounter.increment(1);
|
|
400
|
+
|
|
401
|
+
return commitment;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// completeAndReceipt: nullifies bounty, mints ZK receipt, releases payment.
|
|
405
|
+
export circuit completeAndReceipt(
|
|
406
|
+
witness bountyCommitment: Bytes<32>,
|
|
407
|
+
witness bountyMerkleProof: Proof<25>,
|
|
408
|
+
witness outputHash: Bytes<32>,
|
|
409
|
+
witness completionNonce: Bytes<32>
|
|
410
|
+
): Bytes<32> {
|
|
411
|
+
assert initialized == 1;
|
|
412
|
+
assert bountyTree.verify(bountyCommitment, bountyMerkleProof);
|
|
413
|
+
assert !nullifiers.contains(bountyCommitment);
|
|
414
|
+
nullifiers.insert(bountyCommitment);
|
|
415
|
+
|
|
416
|
+
const receipt = hash(DOMAIN_RECEIPT, bountyCommitment, outputHash, completionNonce);
|
|
417
|
+
receiptTree.insert(receipt);
|
|
418
|
+
|
|
419
|
+
assert activeCount > 0;
|
|
420
|
+
assert completedCount < MAX_COMPLETED;
|
|
421
|
+
completedCount.increment(1);
|
|
422
|
+
activeCount.decrement(1);
|
|
423
|
+
txCounter.increment(1);
|
|
424
|
+
|
|
425
|
+
return receipt;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// verifyReceipt: anyone can verify a receipt is valid — reveals nothing about the bounty.
|
|
429
|
+
export circuit verifyReceipt(
|
|
430
|
+
witness receiptCommitment: Bytes<32>,
|
|
431
|
+
witness receiptMerkleProof: Proof<25>
|
|
432
|
+
): Boolean {
|
|
433
|
+
return receiptTree.verify(receiptCommitment, receiptMerkleProof);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// withdrawFees: operator-only — gated to the address set at initialization.
|
|
437
|
+
export circuit withdrawFees(
|
|
438
|
+
caller: Bytes<32>,
|
|
439
|
+
withdrawAmount: Field
|
|
440
|
+
): [] {
|
|
441
|
+
assert initialized == 1;
|
|
442
|
+
assert caller == operatorAddress;
|
|
443
|
+
assert withdrawAmount > 0;
|
|
444
|
+
|
|
445
|
+
// H-2: cap withdrawals to accumulated fees — operator cannot drain funder/bounty funds
|
|
446
|
+
assert withdrawAmount <= accumulatedFees.read();
|
|
447
|
+
accumulatedFees.decrement(withdrawAmount);
|
|
448
|
+
|
|
449
|
+
effects.releaseToAddress(caller, withdrawAmount);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// getStats: public read-only view.
|
|
453
|
+
export circuit getStats(): [Field, Field, Field, Field, Field] {
|
|
454
|
+
return [completedCount, activeCount, poolCount, operatorFeeBps, txCounter];
|
|
455
|
+
}
|
|
456
|
+
}
|