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 CaseDescriptionBenefit
Threshold WalletsMultiple parties must signNo single point of failure
Custody SolutionsInstitutional key managementRegulatory compliance
Private VotingVote without revealing choiceOn-chain governance
Cross-chain BridgesSecure asset transfersDecentralized security
Oracle NetworksAggregate private dataPrivacy-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

ThreatMitigation
Malicious majorityUse (t, n) threshold where t > n/2
Network attacksEncrypted channels, authentication
Key leakageKey refresh protocols
Denial of serviceTimeout and fallback mechanisms
Replay attacksSession IDs, nonce management

Comparison of Schemes

SchemeSignatureSetupRoundsTrust
Shamir SSAnySimple1Dealer
FROSTEdDSADKG2None
GG20ECDSADKG9+None
CGGMPECDSADKG4None

Next: Secret Sharing - Implementing Shamir's Secret Sharing.