MPC Overview
Multi-Party Computation (MPC) enables multiple parties to jointly compute functions over their inputs while keeping those inputs private. In blockchain contexts, MPC is used for threshold signatures, secure key management, and privacy-preserving protocols.
Core Concepts
Text
┌─────────────────────────────────────────────────────────────────┐
│ MPC Architecture │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ MPC Protocol │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Party 1 │ │ Party 2 │ │ Party 3 │ │
│ │ │ │ │ │ │ │
│ │ Input: │ │ Input: │ │ Input: │ │
│ │ Share 1 │ │ Share 2 │ │ Share 3 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Result │ │
│ │ (No party sees │ │
│ │ other inputs) │ │
│ └──────────────────┘ │
│ │
│ Properties: │
│ • Privacy: Inputs remain secret │
│ • Correctness: Result is accurate │
│ • Security: Tolerates t malicious parties │
│ │
└─────────────────────────────────────────────────────────────────┘
Use Cases in Solana
| Use Case | Description | Benefit |
|---|---|---|
| Threshold Wallets | Multiple parties must sign | No single point of failure |
| Custody Solutions | Institutional key management | Regulatory compliance |
| Private Voting | Vote without revealing choice | On-chain governance |
| Cross-chain Bridges | Secure asset transfers | Decentralized security |
| Oracle Networks | Aggregate private data | Privacy-preserving prices |
Threshold Signatures
Threshold ECDSA
TypeScript
import { EcdsaParty, SignSession, KeygenSession } from "tss-lib";
interface ThresholdConfig {
threshold: number; // t: minimum parties to sign
parties: number; // n: total parties
partyId: number; // This party's ID
}
class ThresholdSigner {
private config: ThresholdConfig;
private keyShare: KeyShare | null = null;
constructor(config: ThresholdConfig) {
this.config = config;
}
// Distributed Key Generation
async generateKeyShare(
otherParties: string[], // URLs of other parties
): Promise<{ publicKey: Uint8Array }> {
const session = new KeygenSession(
this.config.partyId,
this.config.parties,
this.config.threshold,
);
// Round 1: Generate and broadcast commitment
const round1Message = session.createRound1Message();
const round1Responses = await this.broadcast(otherParties, round1Message);
session.processRound1Messages(round1Responses);
// Round 2: Generate and send Feldman VSS shares
const round2Messages = session.createRound2Messages();
const round2Responses = await this.sendToParties(
otherParties,
round2Messages,
);
session.processRound2Messages(round2Responses);
// Round 3: Verify shares and compute public key
const round3Message = session.createRound3Message();
const round3Responses = await this.broadcast(otherParties, round3Message);
session.processRound3Messages(round3Responses);
// Store key share
this.keyShare = session.getKeyShare();
return {
publicKey: session.getPublicKey(),
};
}
// Threshold Signing
async signMessage(
message: Uint8Array,
signingParties: string[], // URLs of parties participating in signing
): Promise<Uint8Array> {
if (!this.keyShare) {
throw new Error("No key share available");
}
if (signingParties.length < this.config.threshold) {
throw new Error(`Need at least ${this.config.threshold} parties`);
}
const session = new SignSession(
this.config.partyId,
this.keyShare,
message,
signingParties.map((_, i) => i + 1),
);
// Round 1: Generate signing nonces
const round1Message = session.createRound1Message();
const round1Responses = await this.broadcast(signingParties, round1Message);
session.processRound1Messages(round1Responses);
// Round 2: Generate partial signatures
const round2Message = session.createRound2Message();
const round2Responses = await this.broadcast(signingParties, round2Message);
session.processRound2Messages(round2Responses);
// Round 3: Combine partial signatures
const round3Message = session.createRound3Message();
const round3Responses = await this.broadcast(signingParties, round3Message);
const signature = session.combineSignatures(round3Responses);
return signature;
}
private async broadcast(parties: string[], message: any): Promise<any[]> {
return Promise.all(
parties.map((url) =>
fetch(`${url}/mpc/message`, {
method: "POST",
body: JSON.stringify(message),
}).then((r) => r.json()),
),
);
}
private async sendToParties(
parties: string[],
messages: Map<number, any>,
): Promise<any[]> {
return Promise.all(
parties.map((url, i) =>
fetch(`${url}/mpc/message`, {
method: "POST",
body: JSON.stringify(messages.get(i + 1)),
}).then((r) => r.json()),
),
);
}
}
Threshold EdDSA (Solana-native)
TypeScript
import { PublicKey, Transaction, Keypair } from "@solana/web3.js";
import { FROST } from "frost-ed25519";
interface FROSTConfig {
threshold: number;
maxSigners: number;
participantId: number;
}
class FROSTSigner {
private config: FROSTConfig;
private secretShare: Uint8Array | null = null;
private groupPublicKey: Uint8Array | null = null;
constructor(config: FROSTConfig) {
this.config = config;
}
// Distributed Key Generation using FROST
async distributedKeyGen(
coordinator: string,
): Promise<{ publicKey: PublicKey }> {
// Round 1: Generate commitment
const { commitment, coefficients } = FROST.keygen.round1(
this.config.participantId,
this.config.threshold,
this.config.maxSigners,
);
// Send commitment to coordinator
await fetch(`${coordinator}/frost/round1`, {
method: "POST",
body: JSON.stringify({
participantId: this.config.participantId,
commitment,
}),
});
// Wait for all commitments
const allCommitments = await fetch(`${coordinator}/frost/commitments`).then(
(r) => r.json(),
);
// Round 2: Generate shares for each participant
const shares = FROST.keygen.round2(
this.config.participantId,
coefficients,
allCommitments,
);
// Send shares to coordinator (encrypted for each recipient)
await fetch(`${coordinator}/frost/round2`, {
method: "POST",
body: JSON.stringify({
participantId: this.config.participantId,
shares,
}),
});
// Wait for shares from all participants
const receivedShares = await fetch(
`${coordinator}/frost/shares/${this.config.participantId}`,
).then((r) => r.json());
// Finalize: Compute secret share and group public key
const { secretShare, groupPublicKey } = FROST.keygen.finalize(
this.config.participantId,
receivedShares,
allCommitments,
);
this.secretShare = secretShare;
this.groupPublicKey = groupPublicKey;
return {
publicKey: new PublicKey(groupPublicKey),
};
}
// Sign a Solana transaction with threshold signature
async signTransaction(
transaction: Transaction,
coordinator: string,
signers: number[],
): Promise<Transaction> {
if (!this.secretShare || !this.groupPublicKey) {
throw new Error("No key share available");
}
if (signers.length < this.config.threshold) {
throw new Error(`Need at least ${this.config.threshold} signers`);
}
// Get message to sign
const message = transaction.serializeMessage();
// Round 1: Generate nonces
const { nonces, commitments } = FROST.sign.round1(
this.config.participantId,
);
// Send commitments to coordinator
await fetch(`${coordinator}/frost/sign/round1`, {
method: "POST",
body: JSON.stringify({
participantId: this.config.participantId,
commitments,
transactionId: transaction.recentBlockhash, // Use as session ID
}),
});
// Wait for all signing commitments
const signingCommitments = await fetch(
`${coordinator}/frost/sign/commitments/${transaction.recentBlockhash}`,
).then((r) => r.json());
// Round 2: Generate signature share
const signatureShare = FROST.sign.round2(
this.config.participantId,
this.secretShare,
nonces,
message,
signingCommitments,
this.groupPublicKey,
);
// Send signature share to coordinator
await fetch(`${coordinator}/frost/sign/round2`, {
method: "POST",
body: JSON.stringify({
participantId: this.config.participantId,
signatureShare,
transactionId: transaction.recentBlockhash,
}),
});
// Get final signature from coordinator
const { signature } = await fetch(
`${coordinator}/frost/sign/final/${transaction.recentBlockhash}`,
).then((r) => r.json());
// Add signature to transaction
transaction.addSignature(
new PublicKey(this.groupPublicKey),
Buffer.from(signature),
);
return transaction;
}
}
MPC Coordinator Service
TypeScript
import express from "express";
import { Pool } from "pg";
interface Session {
id: string;
type: "keygen" | "sign";
participants: number[];
threshold: number;
round: number;
messages: Map<number, Map<number, any>>; // round -> participant -> message
result?: any;
}
class MPCCoordinator {
private app: express.Application;
private sessions: Map<string, Session> = new Map();
private db: Pool;
constructor(databaseUrl: string) {
this.app = express();
this.app.use(express.json());
this.db = new Pool({ connectionString: databaseUrl });
this.setupRoutes();
}
private setupRoutes() {
// Create new MPC session
this.app.post("/sessions", async (req, res) => {
const { type, participants, threshold } = req.body;
const sessionId = crypto.randomUUID();
const session: Session = {
id: sessionId,
type,
participants,
threshold,
round: 0,
messages: new Map(),
};
this.sessions.set(sessionId, session);
// Persist to database
await this.db.query(
`INSERT INTO mpc_sessions (id, type, participants, threshold, created_at)
VALUES ($1, $2, $3, $4, NOW())`,
[sessionId, type, participants, threshold],
);
res.json({ sessionId });
});
// Submit round message
this.app.post("/sessions/:sessionId/messages", async (req, res) => {
const { sessionId } = req.params;
const { participantId, round, message } = req.body;
const session = this.sessions.get(sessionId);
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
if (!session.participants.includes(participantId)) {
return res.status(403).json({ error: "Not a participant" });
}
// Store message for this round
if (!session.messages.has(round)) {
session.messages.set(round, new Map());
}
session.messages.get(round)!.set(participantId, message);
// Persist to database
await this.db.query(
`INSERT INTO mpc_messages (session_id, participant_id, round, message, created_at)
VALUES ($1, $2, $3, $4, NOW())`,
[sessionId, participantId, round, JSON.stringify(message)],
);
// Check if round is complete
const roundMessages = session.messages.get(round)!;
if (roundMessages.size === session.participants.length) {
session.round = round + 1;
}
res.json({
success: true,
roundComplete: roundMessages.size === session.participants.length,
});
});
// Get messages for a round
this.app.get("/sessions/:sessionId/messages/:round", async (req, res) => {
const { sessionId, round } = req.params;
const { participantId } = req.query;
const session = this.sessions.get(sessionId);
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
const roundMessages = session.messages.get(parseInt(round));
if (!roundMessages) {
return res.json({ messages: [], complete: false });
}
// Filter messages (don't send participant's own message)
const messages: any[] = [];
for (const [pid, msg] of roundMessages) {
if (pid !== parseInt(participantId as string)) {
messages.push({ participantId: pid, message: msg });
}
}
res.json({
messages,
complete: roundMessages.size === session.participants.length,
});
});
// Submit final result (signature)
this.app.post("/sessions/:sessionId/result", async (req, res) => {
const { sessionId } = req.params;
const { result } = req.body;
const session = this.sessions.get(sessionId);
if (!session) {
return res.status(404).json({ error: "Session not found" });
}
session.result = result;
// Persist to database
await this.db.query(
`UPDATE mpc_sessions SET result = $1, completed_at = NOW() WHERE id = $2`,
[JSON.stringify(result), sessionId],
);
res.json({ success: true });
});
// Get session status and result
this.app.get("/sessions/:sessionId", async (req, res) => {
const { sessionId } = req.params;
const session = this.sessions.get(sessionId);
if (!session) {
// Try loading from database
const dbResult = await this.db.query(
"SELECT * FROM mpc_sessions WHERE id = $1",
[sessionId],
);
if (dbResult.rows.length === 0) {
return res.status(404).json({ error: "Session not found" });
}
return res.json(dbResult.rows[0]);
}
res.json({
id: session.id,
type: session.type,
participants: session.participants,
threshold: session.threshold,
currentRound: session.round,
result: session.result,
});
});
}
start(port: number) {
this.app.listen(port, () => {
console.log(`MPC Coordinator listening on port ${port}`);
});
}
}
Security Considerations
| Threat | Mitigation |
|---|---|
| Malicious majority | Use (t, n) threshold where t > n/2 |
| Network attacks | Encrypted channels, authentication |
| Key leakage | Key refresh protocols |
| Denial of service | Timeout and fallback mechanisms |
| Replay attacks | Session IDs, nonce management |
Comparison of Schemes
| Scheme | Signature | Setup | Rounds | Trust |
|---|---|---|---|---|
| Shamir SS | Any | Simple | 1 | Dealer |
| FROST | EdDSA | DKG | 2 | None |
| GG20 | ECDSA | DKG | 9+ | None |
| CGGMP | ECDSA | DKG | 4 | None |
Next: Secret Sharing - Implementing Shamir's Secret Sharing.