Skip to content

Commit 0cab5e7

Browse files
committed
maximally split everything
1 parent ff5eb1d commit 0cab5e7

File tree

5 files changed

+289
-4
lines changed

5 files changed

+289
-4
lines changed

packages/contracts/sdk/LobService.ts

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { type AsyncOrSync } from "ts-essentials";
1+
import { uniq } from "lodash";
2+
import { assert, type AsyncOrSync } from "ts-essentials";
23
import { type PoolERC20 } from "../typechain-types";
34
import { NoteInputStruct } from "../typechain-types/contracts/PoolERC20";
4-
import { MpcProverService } from "./mpc/MpcNetworkService.js";
5-
import { splitInput } from "./mpc/utils";
5+
import { MpcProverService, type Side } from "./mpc/MpcNetworkService.js";
6+
import { splitInput, splitInput2 } from "./mpc/utils.js";
67
import { type ITreesService } from "./RemoteTreesService.js";
78
import {
89
CompleteWaAddress,
@@ -125,4 +126,108 @@ export class LobService {
125126
const receipt = await tx.wait();
126127
console.log("swap gas used", receipt?.gasUsed);
127128
}
129+
130+
async requestSwap(params: {
131+
secretKey: string;
132+
note: Erc20Note;
133+
sellAmount: TokenAmount;
134+
buyAmount: TokenAmount;
135+
}) {
136+
const swapCircuit = (await this.circuits).swap;
137+
const randomness = await getRandomness();
138+
139+
const changeNote = await Erc20Note.from({
140+
owner: await CompleteWaAddress.fromSecretKey(params.secretKey),
141+
amount: params.note.amount.sub(params.sellAmount),
142+
randomness,
143+
});
144+
const swapNote = await Erc20Note.from({
145+
owner: await CompleteWaAddress.fromSecretKey(params.secretKey),
146+
amount: params.buyAmount,
147+
randomness,
148+
});
149+
150+
const order = {
151+
sell_amount: await params.sellAmount.toNoir(),
152+
buy_amount: await params.buyAmount.toNoir(),
153+
randomness,
154+
};
155+
156+
// deterministic side
157+
const side: Side =
158+
params.sellAmount.token.toLowerCase() <
159+
params.buyAmount.token.toLowerCase()
160+
? "seller"
161+
: "buyer";
162+
const input = {
163+
[`${side}_secret_key`]: params.secretKey,
164+
[`${side}_note`]: await this.poolErc20.toNoteConsumptionInputs(
165+
params.secretKey,
166+
params.note,
167+
),
168+
[`${side}_order`]: order,
169+
[`${side}_randomness`]: randomness,
170+
};
171+
console.log("side", side, randomness);
172+
// only one trading party need to provide public inputs
173+
const inputPublic =
174+
side === "seller"
175+
? {
176+
tree_roots: await this.trees.getTreeRoots(),
177+
}
178+
: undefined;
179+
const inputsShared = await splitInput2(swapCircuit.circuit, {
180+
// merge public inputs into first input because it does not matter how public inputs are passed
181+
...input,
182+
...inputPublic,
183+
});
184+
const orderId = randomness; // TODO: is randomness a good order id?
185+
const proofs = await Promise.all(
186+
inputsShared.map(({ partyIndex, inputShared }) => {
187+
return this.mpcProver.requestProveAsParty({
188+
orderId,
189+
inputShared,
190+
partyIndex,
191+
circuit: swapCircuit.circuit,
192+
numPublicInputs: 8,
193+
side,
194+
});
195+
}),
196+
);
197+
console.log("got proofs", proofs.length);
198+
assert(uniq(proofs).length === 1, "proofs mismatch");
199+
const proof = proofs[0]!;
200+
return {
201+
proof,
202+
side,
203+
changeNote: await changeNote.toSolidityNoteInput(),
204+
swapNote: await swapNote.toSolidityNoteInput(),
205+
nullifier: (
206+
await params.note.computeNullifier(params.secretKey)
207+
).toString(),
208+
};
209+
}
210+
211+
async commitSwap(sellerSwap: SwapResult, buyerSwap: SwapResult) {
212+
assert(
213+
sellerSwap.proof === buyerSwap.proof,
214+
"seller & buyer proof mismatch",
215+
);
216+
const proof = sellerSwap.proof;
217+
218+
const tx = await this.contract.swap(
219+
proof,
220+
[
221+
sellerSwap.changeNote,
222+
buyerSwap.swapNote,
223+
buyerSwap.changeNote,
224+
sellerSwap.swapNote,
225+
],
226+
[sellerSwap.nullifier, buyerSwap.nullifier],
227+
);
228+
const receipt = await tx.wait();
229+
console.log("swap gas used", receipt?.gasUsed);
230+
}
128231
}
232+
233+
type SwapResult = Awaited<ReturnType<LobService["requestSwap"]>>;

packages/contracts/sdk/mpc/MpcNetworkService.ts

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,112 @@
11
import type { CompiledCircuit } from "@noir-lang/noir_js";
22
import { ethers } from "ethers";
3-
import { range } from "lodash";
3+
import { omit, range } from "lodash";
44
import fs from "node:fs";
55
import path from "node:path";
66
import { assert } from "ts-essentials";
77
import { z } from "zod";
8+
import { promiseWithResolvers } from "../utils.js";
89
import { inWorkingDir, makeRunCommand } from "./utils.js";
910

11+
export type OrderId = string & { __brand: "OrderId" };
12+
export type PartyIndex = 0 | 1 | 2;
13+
export type Side = "seller" | "buyer";
14+
15+
type Order = {
16+
side: Side;
17+
id: OrderId;
18+
inputShared: string;
19+
result: ReturnType<typeof promiseWithResolvers<string>>;
20+
};
21+
1022
export class MpcProverService {
23+
// TODO: split this service into per party service to manage storage easier
24+
#storage: Record<PartyIndex, Map<OrderId, Order>> = {
25+
0: new Map(),
26+
1: new Map(),
27+
2: new Map(),
28+
};
29+
async requestProveAsParty(params: {
30+
orderId: OrderId;
31+
side: Side;
32+
partyIndex: PartyIndex;
33+
inputShared: string;
34+
circuit: CompiledCircuit;
35+
// TODO: infer number of public inputs
36+
numPublicInputs: number;
37+
}) {
38+
// TODO(security): authorization
39+
if (this.#storage[params.partyIndex].has(params.orderId)) {
40+
throw new Error(`order already exists ${params.orderId}`);
41+
}
42+
const order: Order = {
43+
id: params.orderId,
44+
inputShared: params.inputShared,
45+
side: params.side,
46+
result: promiseWithResolvers(),
47+
};
48+
this.#storage[params.partyIndex].set(params.orderId, order);
49+
50+
this.#tryExecuteOrder(params.orderId, {
51+
partyIndex: params.partyIndex,
52+
circuit: params.circuit,
53+
numPublicInputs: params.numPublicInputs,
54+
});
55+
56+
return await order.result.promise;
57+
}
58+
59+
async #tryExecuteOrder(
60+
orderId: OrderId,
61+
62+
params: {
63+
partyIndex: PartyIndex;
64+
circuit: CompiledCircuit;
65+
numPublicInputs: number;
66+
},
67+
) {
68+
const order = this.#storage[params.partyIndex].get(orderId);
69+
if (!order) {
70+
throw new Error(
71+
`order not found in party storage ${params.partyIndex}: ${orderId}`,
72+
);
73+
}
74+
75+
const otherOrders = Array.from(
76+
this.#storage[params.partyIndex].values(),
77+
).filter((o) => o.id !== order.id && o.side !== order.side);
78+
if (otherOrders.length === 0) {
79+
return;
80+
}
81+
const otherOrder = otherOrders[0]!;
82+
const inputsShared =
83+
order.side === "seller"
84+
? ([order.inputShared, otherOrder.inputShared] as const)
85+
: ([otherOrder.inputShared, order.inputShared] as const);
86+
console.log(
87+
"executing orders",
88+
params.partyIndex,
89+
omit(order, "inputShared"),
90+
omit(otherOrder, "inputShared"),
91+
);
92+
try {
93+
const { proof } = await this.proveAsParty({
94+
partyIndex: params.partyIndex,
95+
circuit: params.circuit,
96+
input0Shared: inputsShared[0],
97+
input1Shared: inputsShared[1],
98+
numPublicInputs: params.numPublicInputs,
99+
});
100+
console.log("got proof", orderId, params.partyIndex, proof.length);
101+
const proofHex = ethers.hexlify(proof);
102+
order.result.resolve(proofHex);
103+
otherOrder.result.resolve(proofHex);
104+
} catch (error) {
105+
order.result.reject(error);
106+
otherOrder.result.reject(error);
107+
}
108+
}
109+
11110
async proveAsParty(params: {
12111
partyIndex: number;
13112
circuit: CompiledCircuit;
@@ -16,6 +115,7 @@ export class MpcProverService {
16115
// TODO: infer number of public inputs
17116
numPublicInputs: number;
18117
}) {
118+
console.log("proving as party", params.partyIndex);
19119
return await inWorkingDir(async (workingDir) => {
20120
for (const [traderIndex, inputShared] of [
21121
params.input0Shared,

packages/contracts/sdk/mpc/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { range } from "lodash";
44
import fs from "node:fs";
55
import path from "node:path";
66
import toml from "smol-toml";
7+
import type { PartyIndex } from "./MpcNetworkService.js";
78

9+
/**
10+
* @deprecated use {@link splitInput2} instead
11+
*/
812
export async function splitInput(circuit: CompiledCircuit, input: InputMap) {
913
return await inWorkingDir(async (workingDir) => {
1014
const proverPath = path.join(workingDir, "ProverX.toml");
@@ -21,6 +25,14 @@ export async function splitInput(circuit: CompiledCircuit, input: InputMap) {
2125
});
2226
}
2327

28+
export async function splitInput2(circuit: CompiledCircuit, input: InputMap) {
29+
const shared = await splitInput(circuit, input);
30+
return Array.from(shared.entries()).map(([partyIndex, inputShared]) => ({
31+
partyIndex: partyIndex as PartyIndex,
32+
inputShared,
33+
}));
34+
}
35+
2436
export async function inWorkingDir<T>(f: (workingDir: string) => Promise<T>) {
2537
const id = crypto.randomUUID();
2638
const workingDir = path.join(__dirname, "work-dirs", id);

packages/contracts/sdk/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,16 @@ export async function prove(
7878
proof = proof.slice(4); // remove length
7979
return { proof, witness, returnValue, publicInputs };
8080
}
81+
82+
export function promiseWithResolvers<T>(): {
83+
promise: Promise<T>;
84+
resolve: (value: T) => void;
85+
reject: (reason: unknown) => void;
86+
} {
87+
const ret: any = {};
88+
ret.promise = new Promise((resolve, reject) => {
89+
ret.resolve = resolve;
90+
ret.reject = reject;
91+
});
92+
return ret;
93+
}

packages/contracts/test/PoolERC20.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,4 +408,59 @@ describe("PoolERC20", () => {
408408
expect(await sdk.poolErc20.balanceOf(usdc, bobSecretKey)).to.equal(70n);
409409
expect(await sdk.poolErc20.balanceOf(btc, bobSecretKey)).to.equal(8n);
410410
});
411+
412+
it("swaps mpc", async () => {
413+
const { note: aliceNote } = await sdk.poolErc20.shield({
414+
account: alice,
415+
token: usdc,
416+
amount: 100n,
417+
secretKey: aliceSecretKey,
418+
});
419+
const { note: bobNote } = await sdk.poolErc20.shield({
420+
account: bob,
421+
token: btc,
422+
amount: 10n,
423+
secretKey: bobSecretKey,
424+
});
425+
426+
await backendSdk.rollup.rollup();
427+
428+
const sellerAmount = await TokenAmount.from({
429+
token: await usdc.getAddress(),
430+
amount: 70n,
431+
});
432+
const buyerAmount = await TokenAmount.from({
433+
token: await btc.getAddress(),
434+
amount: 2n,
435+
});
436+
437+
const swapAlicePromise = sdk.lob.requestSwap({
438+
secretKey: aliceSecretKey,
439+
note: aliceNote,
440+
sellAmount: sellerAmount,
441+
buyAmount: buyerAmount,
442+
});
443+
const swapBobPromise = sdk.lob.requestSwap({
444+
secretKey: bobSecretKey,
445+
note: bobNote,
446+
sellAmount: buyerAmount,
447+
buyAmount: sellerAmount,
448+
});
449+
const [swapAlice, swapBob] = await Promise.all([
450+
swapAlicePromise,
451+
swapBobPromise,
452+
]);
453+
const args =
454+
swapAlice.side === "seller"
455+
? ([swapAlice, swapBob] as const)
456+
: ([swapBob, swapAlice] as const);
457+
await sdk.lob.commitSwap(...args);
458+
459+
await backendSdk.rollup.rollup();
460+
461+
expect(await sdk.poolErc20.balanceOf(usdc, aliceSecretKey)).to.equal(30n);
462+
expect(await sdk.poolErc20.balanceOf(btc, aliceSecretKey)).to.equal(2n);
463+
expect(await sdk.poolErc20.balanceOf(usdc, bobSecretKey)).to.equal(70n);
464+
expect(await sdk.poolErc20.balanceOf(btc, bobSecretKey)).to.equal(8n);
465+
});
411466
});

0 commit comments

Comments
 (0)