Merge pull request #14 from privacy-scaling-explorations/update-circuit-type

Updates for summon io and new circuit type
This commit is contained in:
Andrew Morris
2025-05-05 16:50:26 +10:00
committed by GitHub
14 changed files with 121 additions and 244 deletions

View File

@@ -2,7 +2,7 @@
A framework that makes MPC easy in TypeScript.
Choose from multiple existing circuit generators and MPC backends, or create
Choose from multiple existing circuit generators and MPC engines, or create
your own.
## What is MPC?
@@ -36,12 +36,12 @@ For a more technical introduction, see [Computerphile's video on Garbled Circuit
In addition to `mpc-framework`, you will need:
- a circuit generator to turn your MPC program into a circuit (or byo precompiled or handwritten circuit)
- an mpc-framework backend to do the underlying cryptography
- an mpc-framework engine to do the underlying cryptography
```sh
npm install mpc-framework
npm install summon-ts # circuit generator
npm install emp-wasm-backend # backend
npm install emp-wasm-engine # engine
```
### Step 1: Create a Circuit
@@ -51,13 +51,19 @@ circuit. This is a special simplified program in the form of a fixed tree
specifying how to combine values together. Regular programs allow the CPU to
branch into different code paths, and circuits can't do that. It's possible to
write these circuits by hand (or using third party tools), but you might find it
easier to use [summon](https://github.com/voltrevo/summon/):
easier to use [summon](https://github.com/privacy-scaling-explorations/summon/):
```ts
// This isn't exactly TypeScript, but it uses the same syntax and has enough in
// common that you can use the .ts extension and get useful intellisense
export default function main(a: number, b: number) {
export default (io: Summon.IO) => {
// Alice provides a number called 'a'
const a = io.input('alice', 'a', summon.number());
// Bob provides a number called 'b'
const b = io.input('bob', 'b', summon.number());
let result;
// This seems like a branch that I just said is not allowed, but this is just
@@ -69,7 +75,8 @@ export default function main(a: number, b: number) {
result = b;
}
return result;
// Everyone gets an output called 'result'
io.outputPublic('result', result);
}
// We could inline this, but we're just demonstrating that summon supports
@@ -88,19 +95,19 @@ import * as summon from 'summon-ts';
await summon.init();
const { circuit } = summon.compileBoolean(
const { circuit } = summon.compile({
// Specify the entry point, similar to the `main` field of package.json
'circuit/main.ts',
path: 'circuit/main.ts',
// This is the bit width of numbers in your summon program. You can use any
// width you like, but all numbers in the program will be the same. You can
// achieve smaller bit widths within the program using masking (the unused
// gates will be optimized away). It's also possible to define classes for
// matrices/floats/etc.
16,
boolifyWidth: 8,
// File tree to compile
{
files: {
'circuit/main.ts': `
// Include code from step 1
// This can be inlined or you can use build tools to just include a
@@ -109,34 +116,18 @@ const { circuit } = summon.compileBoolean(
`,
// Other files can be specified here
},
);
});
```
### Step 3: Set up your Protocol
```ts
import { Protocol } from 'mpc-framework';
import { EmpWasmBackend } from 'emp-wasm-backend';
import { EmpWasmEngine } from 'emp-wasm-engine';
// ...
// Specify who provides each input, and who receives each output
// (Our chosen backend currently requires that everyone gets all outputs)
const mpcSettings = [
{
name: 'alice',
inputs: ['a'],
outputs: ['main'],
},
{
name: 'bob',
inputs: ['b'],
outputs: ['main'],
},
// You can have more than 2 parties. We're just keeping it simple here.
];
const protocol = new Protocol(circuit, mpcSettings, new EmpWasmBackend());
const protocol = new Protocol(circuit, new EmpWasmEngine());
```
### Step 4: Run the Protocol
@@ -169,15 +160,15 @@ For clarity, a complete version of the example above is provided as
| Name | Similar to | Related Repos |
| ----------------------------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`summon-ts`](https://github.com/voltrevo/summon-ts/) | TypeScript | [`summon`](https://github.com/voltrevo/summon/), [`boolify`](https://github.com/voltrevo/boolify/), [`ValueScript`](https://github.com/voltrevo/ValueScript/) |
| [`circom-2-arithc-ts`](https://github.com/voltrevo/circom-2-arithc-ts/) | Circom | [`circom-2-arithc`](https://github.com/namnc/circom-2-arithc/), [`circom`](https://github.com/iden3/circom/) |
| [`summon-ts`](https://github.com/privacy-scaling-explorations/summon-ts/) | TypeScript | [`summon`](https://github.com/privacy-scaling-explorations/summon/), [`boolify`](https://github.com/privacy-scaling-explorations/boolify/), [`ValueScript`](https://github.com/voltrevo/ValueScript/) |
| [`circom-2-arithc-ts`](https://github.com/privacy-scaling-explorations/circom-2-arithc-ts/) | Circom | [`circom-2-arithc`](https://github.com/namnc/circom-2-arithc/), [`circom`](https://github.com/iden3/circom/) |
## **Backends**
## **Engines**
| Name | Description | Related Repos |
| ------------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`emp-wasm-backend`](https://github.com/voltrevo/emp-wasm-backend/) | Secure MPC using authenticated garbling | [`emp-wasm`](https://github.com/voltrevo/emp-wasm), [`emp-ag2pc`](https://github.com/emp-toolkit/emp-ag2pc/), [`emp-agmpc`](https://github.com/emp-toolkit/emp-agmpc/) |
| [`mpz-ts`](https://github.com/voltrevo/mpz-ts) | Semi-honest 2PC | [`mpz`](https://github.com/privacy-scaling-explorations/mpz) |
| [`emp-wasm-engine`](https://github.com/privacy-scaling-explorations/emp-wasm-engine/) | Secure MPC using authenticated garbling | [`emp-wasm`](https://github.com/privacy-scaling-explorations/emp-wasm), [`emp-ag2pc`](https://github.com/emp-toolkit/emp-ag2pc/), [`emp-agmpc`](https://github.com/emp-toolkit/emp-agmpc/) |
| [`mpz-ts`](https://github.com/privacy-scaling-explorations/mpz-ts) | Semi-honest 2PC | [`mpz`](https://github.com/privacy-scaling-explorations/mpz) |
## Example Projects

27
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "mpc-framework",
"version": "0.2.1",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mpc-framework",
"version": "0.2.1",
"version": "0.3.0",
"license": "MIT",
"dependencies": {
"ee-typed": "^0.1.1",
"mpc-framework-common": "^0.1.1",
"mpc-framework-common": "^0.3.0",
"msgpackr": "^1.11.0",
"zod": "^3.23.8"
},
@@ -29,7 +29,7 @@
"eslint-plugin-react-refresh": "^0.4.6",
"mocha": "^10.7.0",
"prettier": "^3.4.2",
"summon-ts": "^0.2.5",
"summon-ts": "^0.6.0",
"tsx": "^4.16.5",
"typescript": "^5.4.5"
}
@@ -1366,6 +1366,13 @@
"sha3": "^2.1.4"
}
},
"node_modules/emp-wasm-backend/node_modules/mpc-framework-common": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/mpc-framework-common/-/mpc-framework-common-0.1.1.tgz",
"integrity": "sha512-D+o1vSdFPl9v55c0kgM2dGcUR0IZQn3teeQHA5pWiwjokbMt7uiNHHqQKyrzsY2Y4TfBbVKjnS9yjM6szIO5Ng==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -2278,9 +2285,9 @@
}
},
"node_modules/mpc-framework-common": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/mpc-framework-common/-/mpc-framework-common-0.1.1.tgz",
"integrity": "sha512-D+o1vSdFPl9v55c0kgM2dGcUR0IZQn3teeQHA5pWiwjokbMt7uiNHHqQKyrzsY2Y4TfBbVKjnS9yjM6szIO5Ng==",
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/mpc-framework-common/-/mpc-framework-common-0.3.0.tgz",
"integrity": "sha512-LHKnvcUyVx3Ct3rUJNmkOegApRXl7NtR2Ezi+PbJCqkTdwOca3K8F7JgMOhXyvIqtRW62biETIYWordzCeDVBw==",
"license": "MIT"
},
"node_modules/ms": {
@@ -2805,9 +2812,9 @@
}
},
"node_modules/summon-ts": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/summon-ts/-/summon-ts-0.2.5.tgz",
"integrity": "sha512-VtsdFLyvctiYzgfXFH18UlD5u9Do8aNU0sBxiiDT0Tqdru1vAT+LfBwLSRfisry5avEUheYWMFL/5Hix3rIo5Q==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/summon-ts/-/summon-ts-0.6.0.tgz",
"integrity": "sha512-mY0Z5gHovzT9Htscj+gBswiLr9ADSMpQplqybuEPvsdCde85Uz2dpCDG8ucffl/6xpVzDEG/JoXbH+SXKzjCqg==",
"dev": true,
"license": "MIT"
},

View File

@@ -1,6 +1,6 @@
{
"name": "mpc-framework",
"version": "0.2.1",
"version": "0.3.0",
"description": "MPC framework supporting a variety of circuit generators and backends",
"type": "module",
"main": "dist/src/index.js",
@@ -40,13 +40,13 @@
"prettier": "^3.4.2",
"emp-wasm-backend": "^0.2.1",
"mocha": "^10.7.0",
"summon-ts": "^0.2.5",
"summon-ts": "^0.6.0",
"tsx": "^4.16.5",
"typescript": "^5.4.5"
},
"dependencies": {
"ee-typed": "^0.1.1",
"mpc-framework-common": "^0.1.1",
"mpc-framework-common": "^0.3.0",
"msgpackr": "^1.11.0",
"zod": "^3.23.8"
}

View File

@@ -1,39 +0,0 @@
import PlaintextBackendHostSession from './PlaintextBackendHostSession.js';
import PlaintextBackendClientSession from './PlaintextBackendClientSession.js';
import {
Backend,
BackendSession,
Circuit,
MpcSettings,
} from 'mpc-framework-common';
export default class PlaintextBackend implements Backend {
run(
circuit: Circuit,
mpcSettings: MpcSettings,
name: string,
input: Record<string, unknown>,
send: (to: string, msg: Uint8Array) => void,
): BackendSession {
const hostName = mpcSettings[0].name ?? '0';
if (name === hostName) {
return new PlaintextBackendHostSession(
circuit,
mpcSettings,
name,
input,
send,
);
}
return new PlaintextBackendClientSession(
circuit,
mpcSettings,
name,
input,
send,
hostName,
);
}
}

View File

@@ -0,0 +1,26 @@
import PlaintextEngineHostSession from './PlaintextEngineHostSession.js';
import PlaintextEngineClientSession from './PlaintextEngineClientSession.js';
import { Engine, EngineSession, Circuit } from 'mpc-framework-common';
export default class PlaintextEngine implements Engine {
run(
circuit: Circuit,
name: string,
input: Record<string, unknown>,
send: (to: string, msg: Uint8Array) => void,
): EngineSession {
const hostName = circuit.mpcSettings[0].name ?? '0';
if (name === hostName) {
return new PlaintextEngineHostSession(circuit, name, input, send);
}
return new PlaintextEngineClientSession(
circuit,
name,
input,
send,
hostName,
);
}
}

View File

@@ -1,15 +1,14 @@
import { pack, unpack } from 'msgpackr';
import defer from '../helpers/defer.js';
import { z } from 'zod';
import { BackendSession, Circuit, MpcSettings } from 'mpc-framework-common';
import { EngineSession, Circuit } from 'mpc-framework-common';
export default class PlaintextBackendClientSession implements BackendSession {
export default class PlaintextEngineClientSession implements EngineSession {
outputReceived = defer<Record<string, unknown>>();
inputsSent = false;
constructor(
public circuit: Circuit,
public mpcSettings: MpcSettings,
public name: string,
public input: Record<string, unknown>,
public send: (to: string, msg: Uint8Array) => void,

View File

@@ -5,9 +5,9 @@ import delay from '../helpers/delay.js';
import defer from '../helpers/defer.js';
import evaluate, { u32Arithmetic } from '../helpers/evaluate.js';
import errorToString from '../helpers/errorToString.js';
import { BackendSession, Circuit, MpcSettings } from 'mpc-framework-common';
import { EngineSession, Circuit } from 'mpc-framework-common';
export default class PlaintextBackendHostSession implements BackendSession {
export default class PlaintextEngineHostSession implements EngineSession {
outputPromise: Promise<Record<string, unknown>>;
combinedInputs = defer<Record<string, unknown>>();
partialCombinedInputs: Record<string, unknown>;
@@ -15,7 +15,6 @@ export default class PlaintextBackendHostSession implements BackendSession {
constructor(
public circuit: Circuit,
public mpcSettings: MpcSettings,
public name: string,
public input: Record<string, unknown>,
public send: (to: string, msg: Uint8Array) => void,
@@ -30,8 +29,8 @@ export default class PlaintextBackendHostSession implements BackendSession {
(async () => {
// eslint-disable-next-line no-unmodified-loop-condition
while (shouldPing) {
for (let i = 1; i < this.mpcSettings.length; i++) {
const to = this.mpcSettings[i].name ?? i.toString();
for (let i = 1; i < this.circuit.mpcSettings.length; i++) {
const to = this.circuit.mpcSettings[i].name ?? i.toString();
this.send(to, pack('ping'));
}
@@ -50,8 +49,8 @@ export default class PlaintextBackendHostSession implements BackendSession {
const fullResult = evaluate(this.circuit, combinedInputs, u32Arithmetic);
let selfResult: Record<string, unknown> = {};
for (let i = 0; i < this.mpcSettings.length; i++) {
const { name = i.toString(), outputs } = this.mpcSettings[i];
for (let i = 0; i < this.circuit.mpcSettings.length; i++) {
const { name = i.toString(), outputs } = this.circuit.mpcSettings[i];
const result: Record<string, unknown> = {};
for (const outputName of outputs) {
@@ -74,7 +73,7 @@ export default class PlaintextBackendHostSession implements BackendSession {
throw new Error('Already received');
}
const peerInfo = this.mpcSettings.find(
const peerInfo = this.circuit.mpcSettings.find(
(s, i) => from === (s.name ?? i.toString()),
);
@@ -94,7 +93,10 @@ export default class PlaintextBackendHostSession implements BackendSession {
this.peerInputsReceived.add(from);
if (this.peerInputsReceived.size === this.mpcSettings.length - 1) {
if (
this.peerInputsReceived.size ===
this.circuit.mpcSettings.length - 1
) {
this.combinedInputs.resolve(this.partialCombinedInputs);
}
} catch (e) {

View File

@@ -1,11 +1,10 @@
import { Backend, Circuit, MpcSettings } from 'mpc-framework-common';
import { Engine, Circuit } from 'mpc-framework-common';
import Session from './Session.js';
export default class Protocol {
constructor(
public circuit: Circuit,
public mpcSettings: MpcSettings,
public backend: Backend,
public engine: Engine,
) {}
join(
@@ -13,13 +12,6 @@ export default class Protocol {
input: Record<string, unknown>,
send: (to: string, msg: Uint8Array) => void,
): Session {
return new Session(
this.circuit,
this.mpcSettings,
this.backend,
name,
input,
send,
);
return new Session(this.circuit, this.engine, name, input, send);
}
}

View File

@@ -1,29 +1,23 @@
import {
Backend,
BackendSession,
Circuit,
MpcSettings,
} from 'mpc-framework-common';
import { Engine, EngineSession, Circuit } from 'mpc-framework-common';
export default class Session {
backendSession: BackendSession;
engineSession: EngineSession;
constructor(
public circuit: Circuit,
public mpcSettings: MpcSettings,
public backend: Backend,
public engine: Engine,
public name: string,
public input: Record<string, unknown>,
public send: (to: string, msg: Uint8Array) => void,
) {
this.backendSession = backend.run(circuit, mpcSettings, name, input, send);
this.engineSession = engine.run(circuit, name, input, send);
}
handleMessage(from: string, msg: Uint8Array) {
this.backendSession.handleMessage(from, msg);
this.engineSession.handleMessage(from, msg);
}
async output(): Promise<Record<string, unknown>> {
return await this.backendSession.output();
return await this.engineSession.output();
}
}

View File

@@ -1,4 +1,3 @@
/* eslint-disable camelcase */
import { Circuit } from 'mpc-framework-common';
export default function evaluate<T>(
@@ -15,25 +14,23 @@ export default function evaluate<T>(
const wires = new Array<T>(wireCount).fill(arithmetic.init(0));
for (const { value, wire_index } of Object.values(circuit.info.constants)) {
wires[wire_index] = arithmetic.init(value);
for (const { value, address } of circuit.info.constants) {
wires[address] = arithmetic.init(value);
}
if (
Object.keys(inputs).length !==
Object.keys(circuit.info.input_name_to_wire_index).length
) {
if (Object.keys(inputs).length !== circuit.info.inputs.length) {
throw new Error('Mismatch between input len and required input len');
}
for (const [name, value] of Object.entries(inputs)) {
const wireIndex = circuit.info.input_name_to_wire_index[name];
const inputInfo = circuit.info.inputs.find(i => i.name === name);
const address = inputInfo?.address;
if (wireIndex === undefined) {
if (address === undefined) {
throw new Error(`Couldn't map input ${name} to a wire`);
}
wires[wireIndex] = arithmetic.init(value);
wires[address] = arithmetic.init(value);
}
let i = 1;
@@ -79,10 +76,8 @@ export default function evaluate<T>(
const res: Record<string, T> = {};
for (const [name, wireIndex] of Object.entries(
circuit.info.output_name_to_wire_index,
)) {
res[name] = wires[wireIndex];
for (const { name, address } of circuit.info.outputs) {
res[name] = wires[address];
}
return res;

View File

@@ -2,8 +2,8 @@ export { default as Protocol } from './Protocol.js';
export { default as Session } from './Session.js';
export {
type Circuit,
type Backend,
type BackendSession,
type Engine,
type EngineSession,
type MpcParticipantSettings,
type MpcSettings,
} from 'mpc-framework-common';
@@ -11,4 +11,4 @@ export {
// This is NOT exported because it is almost never wanted and depends
// unnecessarily on msgpackr. Importing msgpackr creates a lot of permissions
// noise in Deno on startup.
// export { default as PlaintextBackend } from './PlaintextBackend/PlaintextBackend.js';
// export { default as PlaintextEngine } from './PlaintextEngine/PlaintextEngine.js';

View File

@@ -1,83 +0,0 @@
import * as summon from 'summon-ts';
import { EmpWasmBackend } from 'emp-wasm-backend';
import * as mpcf from '../src';
import { LocalComms, makeLocalCommsPair } from './helpers/LocalComms';
import assert from '../src/helpers/assert';
import { expect } from 'chai';
describe('EmpWasmBackend', () => {
it('3 + 5', async () => {
const outputs = await demo();
expect(outputs).to.deep.eq([{ main: 8 }, { main: 8 }]);
});
});
async function demo() {
await summon.init();
const [aliceComms, bobComms] = makeLocalCommsPair();
const circuit = summon.compileBoolean('/src/main.ts', 4, {
'/src/main.ts': `
export default function main(a: number, b: number) {
return a + b;
}
`,
});
const mpcSettings = [
{
name: 'alice',
inputs: ['a'],
outputs: ['main'],
},
{
name: 'bob',
inputs: ['b'],
outputs: ['main'],
},
];
const protocol = new mpcf.Protocol(
circuit,
mpcSettings,
new EmpWasmBackend(),
);
const outputs = await Promise.all([
runParty('alice', protocol, aliceComms),
runParty('bob', protocol, bobComms),
]);
return outputs;
}
async function runParty(
party: 'alice' | 'bob',
protocol: mpcf.Protocol,
comms: LocalComms,
) {
const otherParty = party === 'alice' ? 'bob' : 'alice';
const session = protocol.join(
party,
party === 'alice' ? { a: 3 } : { b: 5 },
(to, msg) => {
assert(to === otherParty);
comms.send(msg);
},
);
const buffered = comms.recv();
if (buffered.length > 0) {
session.handleMessage(otherParty, buffered);
}
comms.recvBuf.on('data', data => session.handleMessage(otherParty, data));
const output = await session.output();
return output;
}

View File

@@ -5,13 +5,21 @@ import once from '../../src/helpers/once';
const aPlusB = once(async () => {
await summon.init();
return summon.compile('/src/main.ts', {
'/src/main.ts': `
export default function c(a: number, b: number) {
return a + b;
}
`,
const { circuit } = summon.compile({
path: '/src/main.ts',
files: {
'/src/main.ts': `
export default (io: Summon.IO) => {
const a = io.input('alice', 'a', summon.number());
const b = io.input('bob', 'b', summon.number());
io.outputPublic('c', a + b);
}
`,
},
});
return circuit;
});
export default aPlusB;

View File

@@ -1,28 +1,13 @@
import { expect } from 'chai';
import Protocol from '../src/Protocol';
import aPlusB from './circuits/aPlusB';
import PlaintextBackend from '../src/PlaintextBackend/PlaintextBackend';
import PlaintextEngine from '../src/PlaintextEngine/PlaintextEngine';
import { EventEmitter } from 'ee-typed';
import assert from '../src/helpers/assert';
describe('plaintext', () => {
it('3 + 5', async () => {
const protocol = new Protocol(
await aPlusB(),
[
{
name: 'alice',
inputs: ['a'],
outputs: ['c'],
},
{
name: 'bob',
inputs: ['b'],
outputs: ['c'],
},
],
new PlaintextBackend(),
);
const protocol = new Protocol(await aPlusB(), new PlaintextEngine());
const messageEvents = new EventEmitter<{
aliceToBob(msg: Uint8Array): void;