nightpay 0.1.2 → 0.2.1
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/LICENCE +1 -0
- package/LICENSE +201 -21
- package/README.md +213 -203
- package/bin/cli.js +56 -56
- package/package.json +39 -39
- package/skills/nightpay/SKILL.md +141 -105
- package/skills/nightpay/contracts/receipt.compact +197 -195
- package/skills/nightpay/openclaw-fragment.json +20 -20
- package/skills/nightpay/rules/content-safety.md +187 -187
- package/skills/nightpay/rules/escrow-safety.md +140 -132
- package/skills/nightpay/rules/privacy-first.md +30 -30
- package/skills/nightpay/rules/receipt-format.md +45 -45
- package/skills/nightpay/scripts/bounty-board.sh +325 -325
- package/skills/nightpay/scripts/gateway.sh +282 -25
- package/skills/nightpay/scripts/mip003-server.sh +532 -32
- package/skills/nightpay/scripts/update-blocklist.sh +194 -194
|
@@ -1,195 +1,197 @@
|
|
|
1
|
-
// NightPay — Midnight Compact contract (ledger-compatible)
|
|
2
|
-
//
|
|
3
|
-
// Built against: midnightntwrk/midnight-ledger spec
|
|
4
|
-
//
|
|
5
|
-
// SECURITY MODEL:
|
|
6
|
-
// - initialize() can only be called once — locked forever after
|
|
7
|
-
// - withdrawFees() is gated to the operator address set at init
|
|
8
|
-
// - operatorFeeBps and operatorAddress are IMMUTABLE after initialization
|
|
9
|
-
// - Fee split is enforced in-circuit with constrained balance effects
|
|
10
|
-
// - activeCount is underflow-guarded, completedCount is overflow-guarded
|
|
11
|
-
// - All domain-separated hashes prevent cross-namespace collisions
|
|
12
|
-
// - Gateway address is locked at init — cannot be injected via transaction metadata
|
|
13
|
-
// - Total bounty throughput capped to prevent counter exhaustion griefing
|
|
14
|
-
|
|
15
|
-
pragma
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
state
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
secret
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
assert
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
witness
|
|
89
|
-
witness
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
assert
|
|
94
|
-
|
|
95
|
-
// SECURITY:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
witness
|
|
131
|
-
witness
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// SECURITY:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// SECURITY:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
1
|
+
// NightPay — Midnight Compact contract (ledger-compatible)
|
|
2
|
+
//
|
|
3
|
+
// Built against: midnightntwrk/midnight-ledger spec
|
|
4
|
+
//
|
|
5
|
+
// SECURITY MODEL:
|
|
6
|
+
// - initialize() can only be called once — locked forever after
|
|
7
|
+
// - withdrawFees() is gated to the operator address set at init
|
|
8
|
+
// - operatorFeeBps and operatorAddress are IMMUTABLE after initialization
|
|
9
|
+
// - Fee split is enforced in-circuit with constrained balance effects
|
|
10
|
+
// - activeCount is underflow-guarded, completedCount is overflow-guarded
|
|
11
|
+
// - All domain-separated hashes prevent cross-namespace collisions
|
|
12
|
+
// - Gateway address is locked at init — cannot be injected via transaction metadata
|
|
13
|
+
// - Total bounty throughput capped to prevent counter exhaustion griefing
|
|
14
|
+
|
|
15
|
+
pragma language_version >= 0.19;
|
|
16
|
+
|
|
17
|
+
import CompactStandardLibrary;
|
|
18
|
+
|
|
19
|
+
module NightPay {
|
|
20
|
+
|
|
21
|
+
// ─── Public ledger state ─────────────────────────────────────────────────────
|
|
22
|
+
ledger completedCount: Counter;
|
|
23
|
+
ledger activeCount: Counter;
|
|
24
|
+
ledger operatorFeeBps: Field;
|
|
25
|
+
ledger operatorAddress: Bytes<32>;
|
|
26
|
+
|
|
27
|
+
// SECURITY: one-time init lock — set to 1 after initialize() runs
|
|
28
|
+
ledger initialized: Field;
|
|
29
|
+
|
|
30
|
+
// DARK ENERGY: gateway address locked at init — cannot be injected per-transaction.
|
|
31
|
+
// Without this, a malicious caller could supply a custom "gateway" address in
|
|
32
|
+
// transaction metadata and redirect all bounty payments to themselves.
|
|
33
|
+
ledger gatewayAddress: Bytes<32>;
|
|
34
|
+
|
|
35
|
+
// ─── Private state (shielded via ZK) ────────────────────────────────────────
|
|
36
|
+
secret ledger bountyTree: MerkleTree<25>;
|
|
37
|
+
secret ledger receiptTree: MerkleTree<25>;
|
|
38
|
+
secret ledger nullifiers: Set<Bytes<32>>;
|
|
39
|
+
|
|
40
|
+
// ─── Domain separation prefixes ─────────────────────────────────────────────
|
|
41
|
+
// Prevents hash collisions between bounty commitments and receipt hashes.
|
|
42
|
+
const DOMAIN_BOUNTY: Bytes<8> = 0x626f756e7479303030; // "bounty000"
|
|
43
|
+
const DOMAIN_RECEIPT: Bytes<8> = 0x726563656970743030; // "receipt00"
|
|
44
|
+
const DOMAIN_NULLIFIER: Bytes<8> = 0x6e756c6c6966696572; // "nullifier"
|
|
45
|
+
|
|
46
|
+
// ─── Capacity limits ─────────────────────────────────────────────────────────
|
|
47
|
+
// SECURITY: Merkle tree depth 25 = 2^25 = 33,554,432 max entries.
|
|
48
|
+
// We cap at 90% to leave headroom and reject spam before overflow.
|
|
49
|
+
// At 1 bounty/second this lasts ~348 days.
|
|
50
|
+
const MAX_TREE_ENTRIES: Field = 30199000; // ~90% of 2^25
|
|
51
|
+
|
|
52
|
+
// DARK ENERGY: completedCount overflow griefing guard.
|
|
53
|
+
// An attacker who posts + completes millions of dust bounties could overflow
|
|
54
|
+
// the Counter if Compact's Counter is bounded. We cap total lifetime completions
|
|
55
|
+
// to the same tree limit — a completed bounty always consumed a tree slot first,
|
|
56
|
+
// so this is never a tighter constraint than MAX_TREE_ENTRIES.
|
|
57
|
+
const MAX_COMPLETED: Field = 30199000; // matches MAX_TREE_ENTRIES
|
|
58
|
+
|
|
59
|
+
// SECURITY: Field arithmetic overflow guard.
|
|
60
|
+
// Midnight's Field is a 256-bit prime. We cap amount at 2^53 - 1
|
|
61
|
+
// (max safe JavaScript integer) so amount * operatorFeeBps (max 500)
|
|
62
|
+
// stays within safe range: 2^53 * 500 << 2^256.
|
|
63
|
+
const MAX_AMOUNT: Field = 9007199254740991; // 2^53 - 1
|
|
64
|
+
|
|
65
|
+
// ─── Circuits ───────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
// SECURITY: guarded by initialized flag — reverts if called twice.
|
|
68
|
+
// DARK ENERGY: gatewayAddress is set at init and locked — prevents per-transaction
|
|
69
|
+
// gateway address injection that would redirect bounty payments to an attacker.
|
|
70
|
+
export circuit initialize(
|
|
71
|
+
operatorAddr: Bytes<32>,
|
|
72
|
+
gatewayAddr: Bytes<32>, // DARK ENERGY: locked here, used in postBounty
|
|
73
|
+
feeBps: Field
|
|
74
|
+
): Void {
|
|
75
|
+
assert initialized == 0; // SECURITY: one-time only
|
|
76
|
+
assert feeBps <= 500; // SECURITY: max 5% fee cap, enforced on-chain
|
|
77
|
+
assert feeBps >= 0; // SECURITY: no negative fee abuse
|
|
78
|
+
|
|
79
|
+
operatorAddress = operatorAddr;
|
|
80
|
+
gatewayAddress = gatewayAddr; // DARK ENERGY: immutable after init
|
|
81
|
+
operatorFeeBps = feeBps;
|
|
82
|
+
initialized = 1; // SECURITY: lock forever
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// postBounty: funder deposits shielded NIGHT, commitment stored in Merkle tree.
|
|
86
|
+
// SECURITY: fee split enforced in-circuit via effects API.
|
|
87
|
+
export circuit postBounty(
|
|
88
|
+
witness payerNullifier: Bytes<32>,
|
|
89
|
+
witness amount: Field,
|
|
90
|
+
witness jobHash: Bytes<32>,
|
|
91
|
+
witness nonce: Bytes<32>
|
|
92
|
+
): Bytes<32> {
|
|
93
|
+
assert initialized == 1; // SECURITY: contract must be initialized
|
|
94
|
+
assert amount > 0; // SECURITY: no zero-value bounties
|
|
95
|
+
assert amount <= MAX_AMOUNT; // SECURITY: Field overflow guard
|
|
96
|
+
|
|
97
|
+
// SECURITY: reject when tree is at 90% capacity — prevents overflow DoS
|
|
98
|
+
assert activeCount < MAX_TREE_ENTRIES;
|
|
99
|
+
|
|
100
|
+
// SECURITY: domain-separated hash — bounty commitments can never collide
|
|
101
|
+
// with receipt hashes or nullifiers even if inputs are identical
|
|
102
|
+
const commitment = hash(DOMAIN_BOUNTY, payerNullifier, amount, jobHash, nonce);
|
|
103
|
+
|
|
104
|
+
// SECURITY: commitment must be fresh — prevents replay of old deposits
|
|
105
|
+
assert !nullifiers.contains(commitment);
|
|
106
|
+
|
|
107
|
+
bountyTree.insert(commitment);
|
|
108
|
+
activeCount.increment(1);
|
|
109
|
+
|
|
110
|
+
// SECURITY: fee split enforced on-chain via Zswap effects
|
|
111
|
+
// Both transfers are constrained — operator cannot take more than feeBps
|
|
112
|
+
const fee = amount * operatorFeeBps / 10000;
|
|
113
|
+
const netAmount = amount - fee;
|
|
114
|
+
|
|
115
|
+
assert fee + netAmount == amount; // SECURITY: no rounding loss or gain
|
|
116
|
+
|
|
117
|
+
// Retain fee in contract balance (operator withdraws later)
|
|
118
|
+
effects.retainInContract(fee);
|
|
119
|
+
// DARK ENERGY: use the locked gatewayAddress from state, not a caller-supplied
|
|
120
|
+
// value. Without this, a malicious caller injects their own address in the
|
|
121
|
+
// transaction and redirects all bounty funds to themselves.
|
|
122
|
+
effects.releaseToAddress(gatewayAddress, netAmount);
|
|
123
|
+
|
|
124
|
+
return commitment;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// completeAndReceipt: nullifies bounty, mints ZK receipt, releases payment.
|
|
128
|
+
// SECURITY: double-completion prevented by nullifier set.
|
|
129
|
+
export circuit completeAndReceipt(
|
|
130
|
+
witness bountyCommitment: Bytes<32>,
|
|
131
|
+
witness bountyMerkleProof: Proof<25>,
|
|
132
|
+
witness outputHash: Bytes<32>,
|
|
133
|
+
witness completionNonce: Bytes<32>
|
|
134
|
+
): Bytes<32> {
|
|
135
|
+
assert initialized == 1; // SECURITY: contract must be initialized
|
|
136
|
+
|
|
137
|
+
// SECURITY: bounty must exist in tree — proves it was legitimately posted
|
|
138
|
+
assert bountyTree.verify(bountyCommitment, bountyMerkleProof);
|
|
139
|
+
|
|
140
|
+
// SECURITY: nullifier check prevents double-claim / double-payment
|
|
141
|
+
assert !nullifiers.contains(bountyCommitment);
|
|
142
|
+
nullifiers.insert(bountyCommitment);
|
|
143
|
+
|
|
144
|
+
// SECURITY: domain-separated receipt hash — cannot be forged from bounty inputs
|
|
145
|
+
const receipt = hash(DOMAIN_RECEIPT, bountyCommitment, outputHash, completionNonce);
|
|
146
|
+
|
|
147
|
+
// SECURITY: receipt deduplication uses a dedicated receiptProof, not the
|
|
148
|
+
// bountyMerkleProof — these are separate trees with separate proofs.
|
|
149
|
+
// Reusing bountyMerkleProof here would be a logic error (different tree).
|
|
150
|
+
// The nullifier insertion above already prevents double-completion;
|
|
151
|
+
// this insert is the canonical record.
|
|
152
|
+
receiptTree.insert(receipt);
|
|
153
|
+
|
|
154
|
+
// SECURITY: counters stay consistent — activeCount only decrements if > 0
|
|
155
|
+
assert activeCount > 0;
|
|
156
|
+
// DARK ENERGY: completedCount overflow guard — an attacker who posts and
|
|
157
|
+
// completes millions of dust bounties could wrap the counter if it is bounded.
|
|
158
|
+
// Since every completion consumed a tree slot, this cap is never tighter
|
|
159
|
+
// than MAX_TREE_ENTRIES; it just makes the invariant explicit in-circuit.
|
|
160
|
+
assert completedCount < MAX_COMPLETED;
|
|
161
|
+
completedCount.increment(1);
|
|
162
|
+
activeCount.decrement(1);
|
|
163
|
+
|
|
164
|
+
return receipt;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// verifyReceipt: anyone can verify a receipt is valid — reveals nothing about the bounty.
|
|
168
|
+
export circuit verifyReceipt(
|
|
169
|
+
witness receiptCommitment: Bytes<32>,
|
|
170
|
+
witness receiptMerkleProof: Proof<25>
|
|
171
|
+
): Boolean {
|
|
172
|
+
return receiptTree.verify(receiptCommitment, receiptMerkleProof);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// withdrawFees: operator-only — gated to the address set at initialization.
|
|
176
|
+
// SECURITY: caller identity is constrained — no other address can withdraw.
|
|
177
|
+
export circuit withdrawFees(
|
|
178
|
+
caller: Bytes<32>, // must match operatorAddress
|
|
179
|
+
withdrawAmount: Field
|
|
180
|
+
): Void {
|
|
181
|
+
assert initialized == 1; // SECURITY: contract must be initialized
|
|
182
|
+
|
|
183
|
+
// SECURITY: only the operator set at init can withdraw
|
|
184
|
+
assert caller == operatorAddress;
|
|
185
|
+
|
|
186
|
+
// SECURITY: cannot withdraw more than contract holds — ledger enforces balance
|
|
187
|
+
assert withdrawAmount > 0;
|
|
188
|
+
|
|
189
|
+
// Ledger's apply() rejects if contract balance < withdrawAmount — no overdraft
|
|
190
|
+
effects.releaseToAddress(caller, withdrawAmount);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// getStats: public read-only view — no sensitive data exposed.
|
|
194
|
+
export circuit getStats(): [Field, Field, Field] {
|
|
195
|
+
return [completedCount, activeCount, operatorFeeBps];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$comment": "Merge this into your ~/.openclaw/openclaw.json under 'skills.entries' to enable the nightpay skill. OpenClaw discovers the skill automatically once it is installed into ./skills/nightpay — this fragment just supplies the required env vars.",
|
|
3
|
-
"skills": {
|
|
4
|
-
"entries": {
|
|
5
|
-
"nightpay": {
|
|
6
|
-
"enabled": true,
|
|
7
|
-
"env": {
|
|
8
|
-
"MASUMI_API_KEY": "MASUMI_API_KEY",
|
|
9
|
-
"OPERATOR_ADDRESS": "OPERATOR_ADDRESS",
|
|
10
|
-
"MIDNIGHT_NETWORK": "MIDNIGHT_NETWORK",
|
|
11
|
-
"OPERATOR_FEE_BPS": "OPERATOR_FEE_BPS",
|
|
12
|
-
"RECEIPT_CONTRACT_ADDRESS": "RECEIPT_CONTRACT_ADDRESS",
|
|
13
|
-
"OPERATOR_SECRET_KEY": "OPERATOR_SECRET_KEY",
|
|
14
|
-
"CONTENT_SAFETY_URL": "CONTENT_SAFETY_URL",
|
|
15
|
-
"BRIDGE_URL": "BRIDGE_URL"
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Merge this into your ~/.openclaw/openclaw.json under 'skills.entries' to enable the nightpay skill. OpenClaw discovers the skill automatically once it is installed into ./skills/nightpay — this fragment just supplies the required env vars.",
|
|
3
|
+
"skills": {
|
|
4
|
+
"entries": {
|
|
5
|
+
"nightpay": {
|
|
6
|
+
"enabled": true,
|
|
7
|
+
"env": {
|
|
8
|
+
"MASUMI_API_KEY": "MASUMI_API_KEY",
|
|
9
|
+
"OPERATOR_ADDRESS": "OPERATOR_ADDRESS",
|
|
10
|
+
"MIDNIGHT_NETWORK": "MIDNIGHT_NETWORK",
|
|
11
|
+
"OPERATOR_FEE_BPS": "OPERATOR_FEE_BPS",
|
|
12
|
+
"RECEIPT_CONTRACT_ADDRESS": "RECEIPT_CONTRACT_ADDRESS",
|
|
13
|
+
"OPERATOR_SECRET_KEY": "OPERATOR_SECRET_KEY",
|
|
14
|
+
"CONTENT_SAFETY_URL": "CONTENT_SAFETY_URL",
|
|
15
|
+
"BRIDGE_URL": "BRIDGE_URL"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|