@zill-protocol/client 4.1.2
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.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +58 -0
- package/dist/src/NocturneClient.d.ts +68 -0
- package/dist/src/NocturneClient.d.ts.map +1 -0
- package/dist/src/NocturneClient.js +264 -0
- package/dist/src/NocturneClient.js.map +1 -0
- package/dist/src/NocturneDB.d.ts +100 -0
- package/dist/src/NocturneDB.d.ts.map +1 -0
- package/dist/src/NocturneDB.js +525 -0
- package/dist/src/NocturneDB.js.map +1 -0
- package/dist/src/OpTracker.d.ts +13 -0
- package/dist/src/OpTracker.d.ts.map +1 -0
- package/dist/src/OpTracker.js +34 -0
- package/dist/src/OpTracker.js.map +1 -0
- package/dist/src/conversion/converter.d.ts +5 -0
- package/dist/src/conversion/converter.d.ts.map +1 -0
- package/dist/src/conversion/converter.js +15 -0
- package/dist/src/conversion/converter.js.map +1 -0
- package/dist/src/conversion/index.d.ts +3 -0
- package/dist/src/conversion/index.d.ts.map +1 -0
- package/dist/src/conversion/index.js +21 -0
- package/dist/src/conversion/index.js.map +1 -0
- package/dist/src/conversion/mock.d.ts +6 -0
- package/dist/src/conversion/mock.d.ts.map +1 -0
- package/dist/src/conversion/mock.js +14 -0
- package/dist/src/conversion/mock.js.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/opRequestGas.d.ts +20 -0
- package/dist/src/opRequestGas.d.ts.map +1 -0
- package/dist/src/opRequestGas.js +321 -0
- package/dist/src/opRequestGas.js.map +1 -0
- package/dist/src/operationRequest/builder.d.ts +40 -0
- package/dist/src/operationRequest/builder.d.ts.map +1 -0
- package/dist/src/operationRequest/builder.js +192 -0
- package/dist/src/operationRequest/builder.js.map +1 -0
- package/dist/src/operationRequest/index.d.ts +3 -0
- package/dist/src/operationRequest/index.d.ts.map +1 -0
- package/dist/src/operationRequest/index.js +6 -0
- package/dist/src/operationRequest/index.js.map +1 -0
- package/dist/src/operationRequest/operationRequest.d.ts +50 -0
- package/dist/src/operationRequest/operationRequest.d.ts.map +1 -0
- package/dist/src/operationRequest/operationRequest.js +16 -0
- package/dist/src/operationRequest/operationRequest.js.map +1 -0
- package/dist/src/prepareOperation.d.ts +21 -0
- package/dist/src/prepareOperation.d.ts.map +1 -0
- package/dist/src/prepareOperation.js +256 -0
- package/dist/src/prepareOperation.js.map +1 -0
- package/dist/src/proveOperation.d.ts +7 -0
- package/dist/src/proveOperation.d.ts.map +1 -0
- package/dist/src/proveOperation.js +79 -0
- package/dist/src/proveOperation.js.map +1 -0
- package/dist/src/signOperation.d.ts +3 -0
- package/dist/src/signOperation.d.ts.map +1 -0
- package/dist/src/signOperation.js +61 -0
- package/dist/src/signOperation.js.map +1 -0
- package/dist/src/snapJsonRpc.d.ts +55 -0
- package/dist/src/snapJsonRpc.d.ts.map +1 -0
- package/dist/src/snapJsonRpc.js +63 -0
- package/dist/src/snapJsonRpc.js.map +1 -0
- package/dist/src/syncSDK.d.ts +17 -0
- package/dist/src/syncSDK.d.ts.map +1 -0
- package/dist/src/syncSDK.js +188 -0
- package/dist/src/syncSDK.js.map +1 -0
- package/dist/src/types.d.ts +60 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/constants.d.ts +3 -0
- package/dist/src/utils/constants.d.ts.map +1 -0
- package/dist/src/utils/constants.js +20 -0
- package/dist/src/utils/constants.js.map +1 -0
- package/dist/src/utils/index.d.ts +3 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +19 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/misc.d.ts +13 -0
- package/dist/src/utils/misc.d.ts.map +1 -0
- package/dist/src/utils/misc.js +77 -0
- package/dist/src/utils/misc.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/NocturneClient.ts +415 -0
- package/src/NocturneDB.ts +761 -0
- package/src/OpTracker.ts +44 -0
- package/src/conversion/converter.ts +22 -0
- package/src/conversion/index.ts +2 -0
- package/src/conversion/mock.ts +11 -0
- package/src/index.ts +14 -0
- package/src/opRequestGas.ts +487 -0
- package/src/operationRequest/builder.ts +359 -0
- package/src/operationRequest/index.ts +16 -0
- package/src/operationRequest/operationRequest.ts +87 -0
- package/src/prepareOperation.ts +420 -0
- package/src/proveOperation.ts +124 -0
- package/src/signOperation.ts +116 -0
- package/src/snapJsonRpc.ts +109 -0
- package/src/syncSDK.ts +285 -0
- package/src/types.ts +83 -0
- package/src/utils/constants.ts +16 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/misc.ts +107 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { NocturneDB } from "./NocturneDB";
|
|
2
|
+
import {
|
|
3
|
+
JoinSplitRequest,
|
|
4
|
+
GasAccountedOperationRequest,
|
|
5
|
+
} from "./operationRequest/operationRequest";
|
|
6
|
+
import {
|
|
7
|
+
NocturneViewer,
|
|
8
|
+
CanonAddress,
|
|
9
|
+
StealthAddressTrait,
|
|
10
|
+
randomFr,
|
|
11
|
+
CompressedStealthAddress,
|
|
12
|
+
} from "@zill-protocol/crypto";
|
|
13
|
+
import {
|
|
14
|
+
PreSignJoinSplit,
|
|
15
|
+
Note,
|
|
16
|
+
NoteTrait,
|
|
17
|
+
IncludedNote,
|
|
18
|
+
Asset,
|
|
19
|
+
AssetTrait,
|
|
20
|
+
PreSignOperation,
|
|
21
|
+
encryptNote,
|
|
22
|
+
min,
|
|
23
|
+
iterChunks,
|
|
24
|
+
groupByArr,
|
|
25
|
+
SparseMerkleProver,
|
|
26
|
+
MerkleProofInput,
|
|
27
|
+
computeJoinSplitInfoCommitment,
|
|
28
|
+
computeSenderCommitment,
|
|
29
|
+
} from "@zill-protocol/core";
|
|
30
|
+
import { sortNotesByValue, getJoinSplitRequestTotalValue } from "./utils";
|
|
31
|
+
|
|
32
|
+
export const __private = {
|
|
33
|
+
gatherNotes,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface PrepareOperationDeps {
|
|
37
|
+
db: NocturneDB;
|
|
38
|
+
viewer: NocturneViewer;
|
|
39
|
+
merkle: SparseMerkleProver;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function prepareOperation(
|
|
43
|
+
deps: PrepareOperationDeps,
|
|
44
|
+
opRequest: GasAccountedOperationRequest
|
|
45
|
+
): Promise<PreSignOperation> {
|
|
46
|
+
const { refunds, joinSplitRequests, chainId, tellerContract, deadline } =
|
|
47
|
+
opRequest;
|
|
48
|
+
const encodedGasAsset = AssetTrait.encode(opRequest.gasAsset);
|
|
49
|
+
|
|
50
|
+
// if refundAddr is not set, generate a random one
|
|
51
|
+
const refundAddr = StealthAddressTrait.compress(
|
|
52
|
+
opRequest.refundAddr ?? deps.viewer.generateRandomStealthAddress()
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// prepare joinSplits
|
|
56
|
+
let joinSplits: PreSignJoinSplit[] = [];
|
|
57
|
+
const usedMerkleIndices = new Set<number>();
|
|
58
|
+
for (const joinSplitRequest of joinSplitRequests) {
|
|
59
|
+
console.log("preparing joinSplits for request: ", joinSplitRequest);
|
|
60
|
+
const newJoinSplits = await prepareJoinSplits(
|
|
61
|
+
deps,
|
|
62
|
+
joinSplitRequest,
|
|
63
|
+
refundAddr,
|
|
64
|
+
usedMerkleIndices
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
newJoinSplits.forEach((js) => {
|
|
68
|
+
// If note value == 0, its just a dummy and we don't want to count in used merkle indices
|
|
69
|
+
if (js.oldNoteA.value !== 0n) {
|
|
70
|
+
usedMerkleIndices.add(js.oldNoteA.merkleIndex);
|
|
71
|
+
}
|
|
72
|
+
if (js.oldNoteB.value !== 0n) {
|
|
73
|
+
usedMerkleIndices.add(js.oldNoteB.merkleIndex);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
joinSplits.push(...newJoinSplits);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
joinSplits = groupByArr(joinSplits, (joinSplit) =>
|
|
80
|
+
AssetTrait.encodedAssetToString(joinSplit.encodedAsset)
|
|
81
|
+
).flat();
|
|
82
|
+
|
|
83
|
+
// construct op
|
|
84
|
+
const op: PreSignOperation = {
|
|
85
|
+
networkInfo: { chainId, tellerContract },
|
|
86
|
+
refundAddr,
|
|
87
|
+
joinSplits,
|
|
88
|
+
actions: opRequest.actions,
|
|
89
|
+
refunds,
|
|
90
|
+
encodedGasAsset,
|
|
91
|
+
gasAssetRefundThreshold: opRequest.gasAssetRefundThreshold,
|
|
92
|
+
executionGasLimit: opRequest.executionGasLimit,
|
|
93
|
+
gasPrice: opRequest.gasPrice,
|
|
94
|
+
deadline,
|
|
95
|
+
atomicActions: true, // always default to atomic until we find reason not to
|
|
96
|
+
|
|
97
|
+
gasFeeEstimate: opRequest.totalGasLimit * opRequest.gasPrice,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return op;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function prepareJoinSplits(
|
|
104
|
+
{ db, viewer, merkle }: PrepareOperationDeps,
|
|
105
|
+
joinSplitRequest: JoinSplitRequest,
|
|
106
|
+
refundAddr: CompressedStealthAddress,
|
|
107
|
+
alreadyUsedNoteMerkleIndices: Set<number> = new Set()
|
|
108
|
+
): Promise<PreSignJoinSplit[]> {
|
|
109
|
+
let notes: IncludedNote[];
|
|
110
|
+
if (joinSplitRequest.notes && joinSplitRequest.notes.length > 0) {
|
|
111
|
+
notes = joinSplitRequest.notes;
|
|
112
|
+
const mismatchedAsset = notes.find(
|
|
113
|
+
(note) => !AssetTrait.isSameAsset(note.asset, joinSplitRequest.asset)
|
|
114
|
+
);
|
|
115
|
+
if (mismatchedAsset) {
|
|
116
|
+
throw new Error("join split request notes asset mismatch");
|
|
117
|
+
}
|
|
118
|
+
const reusedNote = notes.find((note) =>
|
|
119
|
+
alreadyUsedNoteMerkleIndices.has(note.merkleIndex)
|
|
120
|
+
);
|
|
121
|
+
if (reusedNote) {
|
|
122
|
+
throw new Error("join split request notes already used");
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
notes = await gatherNotes(
|
|
126
|
+
db,
|
|
127
|
+
getJoinSplitRequestTotalValue(joinSplitRequest),
|
|
128
|
+
joinSplitRequest.asset,
|
|
129
|
+
alreadyUsedNoteMerkleIndices
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const unwrapAmount = joinSplitRequest.unwrapValue;
|
|
134
|
+
const paymentAmount = joinSplitRequest.payment?.value ?? 0n;
|
|
135
|
+
|
|
136
|
+
const totalNotesValue = notes.reduce((acc, note) => acc + note.value, 0n);
|
|
137
|
+
const amountToReturn = totalNotesValue - unwrapAmount - paymentAmount;
|
|
138
|
+
|
|
139
|
+
const receiver = joinSplitRequest.payment?.receiver;
|
|
140
|
+
|
|
141
|
+
console.log(`getting joinsplits from notes. Num notes: ${notes.length}`);
|
|
142
|
+
return getJoinSplitsFromNotes(
|
|
143
|
+
viewer,
|
|
144
|
+
merkle,
|
|
145
|
+
notes,
|
|
146
|
+
paymentAmount,
|
|
147
|
+
amountToReturn,
|
|
148
|
+
refundAddr,
|
|
149
|
+
receiver
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class NotEnoughFundsError extends Error {
|
|
154
|
+
constructor(
|
|
155
|
+
public readonly requestedAmount: bigint,
|
|
156
|
+
public readonly ownedAmount: bigint,
|
|
157
|
+
public readonly asset: Asset
|
|
158
|
+
) {
|
|
159
|
+
super(
|
|
160
|
+
`attempted to spend more funds than owned. Address: ${asset.assetAddr}. Attempted: ${requestedAmount}. Owned: ${ownedAmount}.`
|
|
161
|
+
);
|
|
162
|
+
this.name = "NotEnoughFundsError";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function gatherNotes(
|
|
167
|
+
db: NocturneDB,
|
|
168
|
+
requestedAmount: bigint,
|
|
169
|
+
asset: Asset,
|
|
170
|
+
noteMerkleIndicesToIgnore: Set<number> = new Set()
|
|
171
|
+
): Promise<IncludedNote[]> {
|
|
172
|
+
console.log("indices to ignore", noteMerkleIndicesToIgnore);
|
|
173
|
+
|
|
174
|
+
// check that the user has enough notes to cover the request
|
|
175
|
+
const notes = (await db.getNotesForAsset(asset)).filter(
|
|
176
|
+
(n) => !noteMerkleIndicesToIgnore.has(n.merkleIndex)
|
|
177
|
+
);
|
|
178
|
+
const balance = notes.reduce((acc, note) => acc + note.value, 0n);
|
|
179
|
+
if (balance < requestedAmount) {
|
|
180
|
+
// TODO: have a better way to handle following edge case:
|
|
181
|
+
// 1. there are multiple JS requests for the same asset
|
|
182
|
+
// 2. the user has enough notes to cover the total amount
|
|
183
|
+
// 3. the user does *not* have enough to cover each request individually.
|
|
184
|
+
throw new NotEnoughFundsError(requestedAmount, balance, asset);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Goal: want to utilize small notes so they don't pile up.
|
|
188
|
+
// But we also don't want to use too many notes because that will increase the gas cost.
|
|
189
|
+
// So we take the following approach that strikes a good balance
|
|
190
|
+
// 1. sort notes from small to large
|
|
191
|
+
// 2. compute the sums of each sequence of notes starting from the smallest.
|
|
192
|
+
// Stop when the sum is >= to the requested amount.
|
|
193
|
+
// 3. until we've gathered notes totalling at least the requested amount, repeat the following:
|
|
194
|
+
// a. find the smallest subsequence sum that is >= to the remaining amount to gather
|
|
195
|
+
// b. add the largest note of that subsequence to the set of notes to use.
|
|
196
|
+
// 4. If this process results in an odd number of notes to spend and there is still room, grab the smallest unused note for dust collection
|
|
197
|
+
|
|
198
|
+
// 1. Sort notes from small to large
|
|
199
|
+
const sortedNotes = sortNotesByValue(notes);
|
|
200
|
+
|
|
201
|
+
// 2. compute the subsequence sums
|
|
202
|
+
const subsequenceSums: bigint[] = [];
|
|
203
|
+
let curr = 0n;
|
|
204
|
+
for (const note of sortedNotes) {
|
|
205
|
+
curr += note.value;
|
|
206
|
+
subsequenceSums.push(curr);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 3. Construct the set of notes to use.
|
|
210
|
+
const notesToUse: IncludedNote[] = [];
|
|
211
|
+
const usedNoteIndexes: Set<number> = new Set();
|
|
212
|
+
let remainingAmount = requestedAmount;
|
|
213
|
+
let subseqIndex = subsequenceSums.length - 1;
|
|
214
|
+
while (remainingAmount > 0n) {
|
|
215
|
+
// find the index of smallest subsequence sum >= remaining amount to gather
|
|
216
|
+
// the note at that index is the next note to add
|
|
217
|
+
while (
|
|
218
|
+
subseqIndex > 0 &&
|
|
219
|
+
subsequenceSums[subseqIndex - 1] >= remainingAmount
|
|
220
|
+
) {
|
|
221
|
+
subseqIndex--;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const note = sortedNotes[subseqIndex];
|
|
225
|
+
notesToUse.push(note);
|
|
226
|
+
remainingAmount -= note.value;
|
|
227
|
+
|
|
228
|
+
usedNoteIndexes.add(subseqIndex);
|
|
229
|
+
// Skip to next note
|
|
230
|
+
subseqIndex--;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 4. If this process results in an odd number of notes to spend and there is still room, grab the smallest unused note for dust collection
|
|
234
|
+
if (notesToUse.length % 2 == 1 && notesToUse.length < notes.length) {
|
|
235
|
+
const smallestUnusedNote = sortedNotes.find(
|
|
236
|
+
(note, i) => !usedNoteIndexes.has(i)
|
|
237
|
+
);
|
|
238
|
+
if (smallestUnusedNote) {
|
|
239
|
+
notesToUse.push(smallestUnusedNote);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log(
|
|
244
|
+
`gathered notes to satisfy request for ${requestedAmount} of assest ${asset.assetAddr}`,
|
|
245
|
+
{ notesToUse, requestedAmount, asset }
|
|
246
|
+
);
|
|
247
|
+
return notesToUse;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function getJoinSplitsFromNotes(
|
|
251
|
+
viewer: NocturneViewer,
|
|
252
|
+
merkle: SparseMerkleProver,
|
|
253
|
+
notes: IncludedNote[],
|
|
254
|
+
paymentAmount: bigint,
|
|
255
|
+
amountLeftOver: bigint,
|
|
256
|
+
refundAddr: CompressedStealthAddress,
|
|
257
|
+
receiver?: CanonAddress
|
|
258
|
+
): Promise<PreSignJoinSplit[]> {
|
|
259
|
+
// add a dummy note if there are an odd number of notes.
|
|
260
|
+
if (notes.length % 2 == 1) {
|
|
261
|
+
const newAddr = viewer.generateRandomStealthAddress();
|
|
262
|
+
const nonce = randomFr();
|
|
263
|
+
notes.push({
|
|
264
|
+
owner: newAddr,
|
|
265
|
+
nonce,
|
|
266
|
+
asset: notes[0].asset,
|
|
267
|
+
value: 0n,
|
|
268
|
+
merkleIndex: 0,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// for each pair of notes, create a JoinSplit with the maximum possible value transfer
|
|
273
|
+
const res = [];
|
|
274
|
+
let remainingPayment = paymentAmount;
|
|
275
|
+
let remainingAmountLeftOver = amountLeftOver;
|
|
276
|
+
for (const [noteA, noteB] of iterChunks(notes, 2)) {
|
|
277
|
+
const pairTotalValue = noteA.value + noteB.value;
|
|
278
|
+
const amountToReturn = min(remainingAmountLeftOver, pairTotalValue);
|
|
279
|
+
remainingAmountLeftOver -= amountToReturn;
|
|
280
|
+
|
|
281
|
+
const remainingPairValue = pairTotalValue - amountToReturn;
|
|
282
|
+
const paymentAmount = min(remainingPairValue, remainingPayment);
|
|
283
|
+
remainingPayment -= paymentAmount;
|
|
284
|
+
|
|
285
|
+
const joinSplit = await makeJoinSplit(
|
|
286
|
+
viewer,
|
|
287
|
+
merkle,
|
|
288
|
+
noteA,
|
|
289
|
+
noteB,
|
|
290
|
+
paymentAmount,
|
|
291
|
+
amountToReturn,
|
|
292
|
+
refundAddr,
|
|
293
|
+
receiver
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
res.push(joinSplit);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return res;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function makeJoinSplit(
|
|
303
|
+
viewer: NocturneViewer,
|
|
304
|
+
merkle: SparseMerkleProver,
|
|
305
|
+
oldNoteA: IncludedNote,
|
|
306
|
+
oldNoteB: IncludedNote,
|
|
307
|
+
paymentAmount: bigint,
|
|
308
|
+
amountToReturn: bigint,
|
|
309
|
+
refundAddr: CompressedStealthAddress,
|
|
310
|
+
receiver?: CanonAddress
|
|
311
|
+
): Promise<PreSignJoinSplit> {
|
|
312
|
+
const sender = viewer.canonicalAddress();
|
|
313
|
+
// if receiver not given, assumme the sender is the receiver
|
|
314
|
+
receiver = receiver ?? sender;
|
|
315
|
+
|
|
316
|
+
const encodedAsset = AssetTrait.encode(oldNoteA.asset);
|
|
317
|
+
|
|
318
|
+
// whatever isn't being sent to the receiver or ourselves is unwrapped and spent in cleartext (presumably as part of an action)
|
|
319
|
+
const totalValue = oldNoteA.value + oldNoteB.value;
|
|
320
|
+
const publicSpend = totalValue - amountToReturn - paymentAmount;
|
|
321
|
+
|
|
322
|
+
const nullifierA = NoteTrait.createNullifier(viewer, oldNoteA);
|
|
323
|
+
const nullifierB = NoteTrait.createNullifier(viewer, oldNoteB);
|
|
324
|
+
|
|
325
|
+
// first note contains the leftovers - return to sender
|
|
326
|
+
const newNoteA: Note = {
|
|
327
|
+
owner: StealthAddressTrait.fromCanonAddress(sender),
|
|
328
|
+
nonce: NoteTrait.generateNewNonce(viewer, nullifierA),
|
|
329
|
+
asset: oldNoteA.asset,
|
|
330
|
+
value: amountToReturn,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// the second note contains the confidential payment
|
|
334
|
+
const newNoteB: Note = {
|
|
335
|
+
owner: StealthAddressTrait.fromCanonAddress(receiver),
|
|
336
|
+
nonce: NoteTrait.generateNewNonce(viewer, nullifierB),
|
|
337
|
+
asset: oldNoteA.asset,
|
|
338
|
+
value: paymentAmount,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const newNoteACommitment = NoteTrait.toCommitment(newNoteA);
|
|
342
|
+
const newNoteBCommitment = NoteTrait.toCommitment(newNoteB);
|
|
343
|
+
|
|
344
|
+
const newNoteAEncrypted = encryptNote(sender, { ...newNoteA, sender });
|
|
345
|
+
const newNoteBEncrypted = encryptNote(receiver, { ...newNoteB, sender });
|
|
346
|
+
|
|
347
|
+
const membershipProof = merkle.getProof(oldNoteA.merkleIndex);
|
|
348
|
+
const commitmentTreeRoot = membershipProof.root;
|
|
349
|
+
const merkleProofA: MerkleProofInput = {
|
|
350
|
+
path: membershipProof.pathIndices.map((n) => BigInt(n)),
|
|
351
|
+
siblings: membershipProof.siblings,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// noteB could have been a dummy note. If it is, we simply duplicate the merkle proof for noteA
|
|
355
|
+
// the circuit will ignore the merkle proof for noteB if it has a value of 0
|
|
356
|
+
const noteBIsDummy = oldNoteB.value === 0n;
|
|
357
|
+
let merkleProofB: MerkleProofInput;
|
|
358
|
+
if (noteBIsDummy) {
|
|
359
|
+
oldNoteB.merkleIndex = oldNoteA.merkleIndex;
|
|
360
|
+
merkleProofB = merkleProofA;
|
|
361
|
+
} else {
|
|
362
|
+
const membershipProof = merkle.getProof(oldNoteB.merkleIndex);
|
|
363
|
+
|
|
364
|
+
// ! merkle tree could be asynchronously updated between us getting the first and second merkle proofs
|
|
365
|
+
// TODO: add a `merkle.getManyProofs` method that does it in one go
|
|
366
|
+
if (membershipProof.root !== commitmentTreeRoot) {
|
|
367
|
+
throw Error(
|
|
368
|
+
"merkleProver was updated between getting the first and second merkle proofs!"
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
merkleProofB = {
|
|
373
|
+
path: membershipProof.pathIndices.map((n) => BigInt(n)),
|
|
374
|
+
siblings: membershipProof.siblings,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// commit to the sender's canonical address
|
|
379
|
+
const senderCanonAddr = viewer.canonicalAddress();
|
|
380
|
+
const senderCommitment = computeSenderCommitment(
|
|
381
|
+
senderCanonAddr,
|
|
382
|
+
newNoteB.nonce
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// compute joinsplit info commitment
|
|
386
|
+
const joinSplitInfoCommitment = computeJoinSplitInfoCommitment(
|
|
387
|
+
viewer,
|
|
388
|
+
receiver,
|
|
389
|
+
oldNoteA,
|
|
390
|
+
oldNoteB,
|
|
391
|
+
newNoteA,
|
|
392
|
+
newNoteB
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
receiver,
|
|
397
|
+
encodedAsset,
|
|
398
|
+
publicSpend,
|
|
399
|
+
|
|
400
|
+
nullifierA,
|
|
401
|
+
nullifierB,
|
|
402
|
+
oldNoteA,
|
|
403
|
+
oldNoteB,
|
|
404
|
+
|
|
405
|
+
newNoteA,
|
|
406
|
+
newNoteB,
|
|
407
|
+
newNoteAEncrypted,
|
|
408
|
+
newNoteBEncrypted,
|
|
409
|
+
|
|
410
|
+
commitmentTreeRoot,
|
|
411
|
+
newNoteACommitment,
|
|
412
|
+
newNoteBCommitment,
|
|
413
|
+
merkleProofA,
|
|
414
|
+
merkleProofB,
|
|
415
|
+
|
|
416
|
+
senderCommitment,
|
|
417
|
+
joinSplitInfoCommitment,
|
|
418
|
+
refundAddr,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { decomposeCompressedPoint } from "@zill-protocol/crypto";
|
|
2
|
+
import {
|
|
3
|
+
PreProofJoinSplit,
|
|
4
|
+
SignedOperation,
|
|
5
|
+
ProvenJoinSplit,
|
|
6
|
+
ProvenOperation,
|
|
7
|
+
OperationTrait,
|
|
8
|
+
SubmittableOperationWithNetworkInfo,
|
|
9
|
+
JoinSplitProver,
|
|
10
|
+
joinSplitPublicSignalsFromArray,
|
|
11
|
+
packToSolidityProof,
|
|
12
|
+
iterChunks,
|
|
13
|
+
} from "@zill-protocol/core";
|
|
14
|
+
|
|
15
|
+
// SDK will fire off at most this many provers in parallel
|
|
16
|
+
const DEFAULT_MAX_PARALLEL_PROVERS = 4;
|
|
17
|
+
|
|
18
|
+
export type ProveOperationOptions = {
|
|
19
|
+
maxParallel?: number;
|
|
20
|
+
debug?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function proveOperation(
|
|
24
|
+
prover: JoinSplitProver,
|
|
25
|
+
op: SignedOperation,
|
|
26
|
+
opts?: ProveOperationOptions
|
|
27
|
+
): Promise<SubmittableOperationWithNetworkInfo> {
|
|
28
|
+
const joinSplits: ProvenJoinSplit[] = [];
|
|
29
|
+
const maxParallel = opts?.maxParallel ?? DEFAULT_MAX_PARALLEL_PROVERS;
|
|
30
|
+
for (const batch of iterChunks(op.joinSplits, maxParallel)) {
|
|
31
|
+
const provenBatch = await Promise.all(
|
|
32
|
+
batch.map((joinSplit) => proveJoinSplit(prover, joinSplit, opts))
|
|
33
|
+
);
|
|
34
|
+
joinSplits.push(...provenBatch);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const operation: ProvenOperation = {
|
|
38
|
+
...op,
|
|
39
|
+
joinSplits,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return OperationTrait.toSubmittable(operation);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function proveJoinSplit(
|
|
46
|
+
prover: JoinSplitProver,
|
|
47
|
+
signedJoinSplit: PreProofJoinSplit,
|
|
48
|
+
opts?: ProveOperationOptions
|
|
49
|
+
): Promise<ProvenJoinSplit> {
|
|
50
|
+
const {
|
|
51
|
+
opDigest,
|
|
52
|
+
proofInputs,
|
|
53
|
+
refundAddr,
|
|
54
|
+
senderCommitment,
|
|
55
|
+
joinSplitInfoCommitment,
|
|
56
|
+
...baseJoinSplit
|
|
57
|
+
} = signedJoinSplit;
|
|
58
|
+
if (opts?.debug) {
|
|
59
|
+
console.log("proving joinSplit", {
|
|
60
|
+
proofInputs,
|
|
61
|
+
merkleProofA: proofInputs.merkleProofA,
|
|
62
|
+
merkleProofB: proofInputs.merkleProofB,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const proof = await prover.proveJoinSplit(proofInputs);
|
|
67
|
+
|
|
68
|
+
const [, refundAddrH1CompressedY] = decomposeCompressedPoint(refundAddr.h1);
|
|
69
|
+
const [, refundAddrH2CompressedY] = decomposeCompressedPoint(refundAddr.h2);
|
|
70
|
+
|
|
71
|
+
// Check that snarkjs output is consistent with our precomputed joinsplit values
|
|
72
|
+
const publicSignals = joinSplitPublicSignalsFromArray(proof.publicSignals);
|
|
73
|
+
if (
|
|
74
|
+
baseJoinSplit.newNoteACommitment !== publicSignals.newNoteACommitment ||
|
|
75
|
+
baseJoinSplit.newNoteBCommitment !== publicSignals.newNoteBCommitment ||
|
|
76
|
+
baseJoinSplit.commitmentTreeRoot !== publicSignals.commitmentTreeRoot ||
|
|
77
|
+
baseJoinSplit.publicSpend !== publicSignals.publicSpend ||
|
|
78
|
+
baseJoinSplit.nullifierA !== publicSignals.nullifierA ||
|
|
79
|
+
baseJoinSplit.nullifierB !== publicSignals.nullifierB ||
|
|
80
|
+
opDigest !== publicSignals.opDigest ||
|
|
81
|
+
refundAddrH1CompressedY !== publicSignals.refundAddrH1CompressedY ||
|
|
82
|
+
refundAddrH2CompressedY !== publicSignals.refundAddrH2CompressedY ||
|
|
83
|
+
senderCommitment !== publicSignals.senderCommitment ||
|
|
84
|
+
joinSplitInfoCommitment !== publicSignals.joinSplitInfoCommitment
|
|
85
|
+
) {
|
|
86
|
+
console.error("successfully generated proof, but PIs don't match", {
|
|
87
|
+
publicSignalsFromProof: publicSignals,
|
|
88
|
+
publicSignalsExpected: {
|
|
89
|
+
newNoteACommitment: baseJoinSplit.newNoteACommitment,
|
|
90
|
+
newNoteBCommitment: baseJoinSplit.newNoteBCommitment,
|
|
91
|
+
commitmentTreeRoot: baseJoinSplit.commitmentTreeRoot,
|
|
92
|
+
publicSpend: baseJoinSplit.publicSpend,
|
|
93
|
+
nullifierA: baseJoinSplit.nullifierA,
|
|
94
|
+
nullifierB: baseJoinSplit.nullifierB,
|
|
95
|
+
senderCommitment,
|
|
96
|
+
joinSplitInfoCommitment,
|
|
97
|
+
opDigest,
|
|
98
|
+
pubEncododedAssetAddrWithSignBits:
|
|
99
|
+
proofInputs.pubEncodedAssetAddrWithSignBits,
|
|
100
|
+
pubEncodedAssetID: proofInputs.pubEncodedAssetId,
|
|
101
|
+
refundAddrH1CompressedY: refundAddrH1CompressedY,
|
|
102
|
+
refundAddrH2CompressedY: refundAddrH2CompressedY,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
throw new Error(
|
|
107
|
+
`snarkjs generated public input differs from precomputed ones`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (opts?.debug) {
|
|
112
|
+
console.log("successfully generated proofs", {
|
|
113
|
+
proofWithPublicSignals: proof,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const solidityProof = packToSolidityProof(proof.proof);
|
|
118
|
+
return {
|
|
119
|
+
proof: solidityProof,
|
|
120
|
+
senderCommitment,
|
|
121
|
+
joinSplitInfoCommitment,
|
|
122
|
+
...baseJoinSplit,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PreSignJoinSplit,
|
|
3
|
+
NoteTrait,
|
|
4
|
+
PreSignOperation,
|
|
5
|
+
PreProofJoinSplit,
|
|
6
|
+
SignedOperation,
|
|
7
|
+
JoinSplitInputs,
|
|
8
|
+
NocturneSigner,
|
|
9
|
+
encodeEncodedAssetAddrWithSignBitsPI,
|
|
10
|
+
NocturneSignature,
|
|
11
|
+
OperationTrait,
|
|
12
|
+
} from "@zill-protocol/core";
|
|
13
|
+
|
|
14
|
+
export function signOperation(
|
|
15
|
+
signer: NocturneSigner,
|
|
16
|
+
op: PreSignOperation
|
|
17
|
+
): SignedOperation {
|
|
18
|
+
const opDigest = OperationTrait.computeDigest(OperationTrait.toSignable(op));
|
|
19
|
+
const opSig = signer.sign(opDigest);
|
|
20
|
+
|
|
21
|
+
const joinSplits: PreProofJoinSplit[] = op.joinSplits.map((joinSplit) =>
|
|
22
|
+
makePreProofJoinSplit(signer, joinSplit, opDigest, opSig)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
networkInfo,
|
|
27
|
+
actions,
|
|
28
|
+
refundAddr,
|
|
29
|
+
refunds,
|
|
30
|
+
encodedGasAsset,
|
|
31
|
+
gasAssetRefundThreshold,
|
|
32
|
+
executionGasLimit,
|
|
33
|
+
gasPrice,
|
|
34
|
+
deadline,
|
|
35
|
+
atomicActions,
|
|
36
|
+
} = op;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
networkInfo,
|
|
40
|
+
joinSplits,
|
|
41
|
+
refundAddr,
|
|
42
|
+
refunds,
|
|
43
|
+
actions,
|
|
44
|
+
encodedGasAsset,
|
|
45
|
+
gasAssetRefundThreshold,
|
|
46
|
+
executionGasLimit,
|
|
47
|
+
gasPrice,
|
|
48
|
+
deadline,
|
|
49
|
+
atomicActions,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makePreProofJoinSplit(
|
|
54
|
+
signer: NocturneSigner,
|
|
55
|
+
preProofJoinSplit: PreSignJoinSplit,
|
|
56
|
+
opDigest: bigint,
|
|
57
|
+
opSig: NocturneSignature
|
|
58
|
+
): PreProofJoinSplit {
|
|
59
|
+
const {
|
|
60
|
+
merkleProofA,
|
|
61
|
+
merkleProofB,
|
|
62
|
+
oldNoteA,
|
|
63
|
+
oldNoteB,
|
|
64
|
+
newNoteA,
|
|
65
|
+
newNoteB,
|
|
66
|
+
receiver,
|
|
67
|
+
refundAddr,
|
|
68
|
+
senderCommitment,
|
|
69
|
+
publicSpend,
|
|
70
|
+
...baseJoinSplit
|
|
71
|
+
} = preProofJoinSplit;
|
|
72
|
+
|
|
73
|
+
const { c, z } = opSig;
|
|
74
|
+
|
|
75
|
+
const { x, y } = signer.spendPk;
|
|
76
|
+
|
|
77
|
+
const encodedOldNoteA = NoteTrait.encode(oldNoteA);
|
|
78
|
+
const encodedOldNoteB = NoteTrait.encode(oldNoteB);
|
|
79
|
+
const encodedNewNoteA = NoteTrait.encode(newNoteA);
|
|
80
|
+
const encodedNewNoteB = NoteTrait.encode(newNoteB);
|
|
81
|
+
|
|
82
|
+
// if publicSpend is 0, hide the asset info by masking it to 0
|
|
83
|
+
const pubEncodedAssetAddrWithSignBits = encodeEncodedAssetAddrWithSignBitsPI(
|
|
84
|
+
publicSpend === 0n ? 0n : encodedNewNoteA.encodedAssetAddr,
|
|
85
|
+
refundAddr
|
|
86
|
+
);
|
|
87
|
+
const pubEncodedAssetId =
|
|
88
|
+
publicSpend === 0n ? 0n : encodedNewNoteA.encodedAssetId;
|
|
89
|
+
|
|
90
|
+
const proofInputs: JoinSplitInputs = {
|
|
91
|
+
vk: signer.vk,
|
|
92
|
+
vkNonce: signer.vkNonce,
|
|
93
|
+
spendPk: [x, y],
|
|
94
|
+
c,
|
|
95
|
+
z,
|
|
96
|
+
merkleProofA,
|
|
97
|
+
merkleProofB,
|
|
98
|
+
operationDigest: opDigest,
|
|
99
|
+
oldNoteA: encodedOldNoteA,
|
|
100
|
+
oldNoteB: encodedOldNoteB,
|
|
101
|
+
newNoteA: encodedNewNoteA,
|
|
102
|
+
newNoteB: encodedNewNoteB,
|
|
103
|
+
refundAddr,
|
|
104
|
+
pubEncodedAssetAddrWithSignBits,
|
|
105
|
+
pubEncodedAssetId,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
opDigest,
|
|
110
|
+
proofInputs,
|
|
111
|
+
refundAddr,
|
|
112
|
+
senderCommitment,
|
|
113
|
+
publicSpend,
|
|
114
|
+
...baseJoinSplit,
|
|
115
|
+
};
|
|
116
|
+
}
|