MPC Threshold Signing

2-of-3 Feldman VSS + Schnorr-like Signing on secp256k1 — Visual Guide

Alice (party #1)
Bob (party #2)
Carol (party #3)
Private (secret)
Public (visible)
Private mail (1-to-1)
Phase 1: Key Generation (Feldman VSS)
1
Each Friend Picks Their Secret

Each party independently generates a random polynomial f(x) = a0 + a1·x.
Degree = threshold−1 = 1. The constant term a0 is their secret contribution to the group key.

Alice (#1)
  • secret a0A — secret contribution
  • secret a1A — random blinding
fA(x) = a0A + a1A·x
Bob (#2)
  • secret a0B
  • secret a1B
fB(x) = a0B + a1B·x
Carol (#3)
  • secret a0C
  • secret a1C
fC(x) = a0C + a1C·x

Nothing is shared yet. Each polynomial lives only on one machine. Files: alice/secrets.json, bob/secrets.json, carol/secrets.json

2
Post Commitments to the Public Board

Each party multiplies their secret coefficients by the generator point G to create commitments. These go on the public board — everyone sees them, but can't reverse-engineer the secrets (discrete log problem).

Alice
C0A = a0A · G C1A = a1A · G
  • secret a0A, a1A (kept)
  • public C0A, C1A (posted)
Bob
C0B = a0B · G C1B = a1B · G
  • secret a0B, a1B
  • public C0B, C1B
Carol
C0C = a0C · G C1C = a1C · G
  • secret a0C, a1C
  • public C0C, C1C
all commitments flow down
📢 Public Board
  • step2_alice_commit.json — {C0A, C1A} (elliptic curve points)
  • step2_bob_commit.json — {C0B, C1B}
  • step2_carol_commit.json — {C0C, C1C}

Key idea: C0 = a0·G locks in the secret without revealing it. Later, anyone can verify shares against these commitments.

3
Whisper Secret Shares (Private Mail)

Each party evaluates their polynomial at the other parties' indices and sends the result as a secret share via encrypted private mail. Only the recipient can read it.

Alice
fA(2) → Bob fA(3) → Carol
Bob
fB(1) → Alice fB(3) → Carol
Carol
fC(1) → Alice fC(2) → Bob
f_A(2) f_A(3) f_B(1) f_B(3) Alice Bob Carol
📬 Private Mail (encrypted, point-to-point)
  • alice_to_bob.json — fA(2) = a0A + a1A·2 only bob reads
  • alice_to_carol.json — fA(3) = a0A + a1A·3 only carol reads
  • bob_to_alice.json — fB(1) only alice reads
  • bob_to_carol.json — fB(3) only carol reads
  • carol_to_alice.json — fC(1) only alice reads
  • carol_to_bob.json — fC(2) only bob reads

Each share is a single scalar (big number). The polynomial evaluation at index i gives that party their piece of the puzzle. Nobody sees anyone else's mail.

4
Verify Shares & Discover the Group Key

Each party: (1) verifies received shares using Feldman VSS, (2) combines all shares into their final key share, (3) everyone computes the same group public key.

Alice verifies & combines
Verify: fB(1)·G == C0B + 1·C1B ? ✅ fC(1)·G == C0C + 1·C1C ? ✅ Combine: xA = fA(1) + fB(1) + fC(1)
  • secret xA — final key share
Bob verifies & combines
Verify: fA(2)·G == C0A + 2·C1A ? ✅ fC(2)·G == C0C + 2·C1C ? ✅ Combine: xB = fA(2) + fB(2) + fC(2)
  • secret xB — final key share
Carol verifies & combines
Verify: fA(3)·G == C0A + 3·C1A ? ✅ fC(3)·G == C0C + 3·C1C ? ✅ Combine: xC = fA(3) + fB(3) + fC(3)
  • secret xC — final key share
everyone computes the same public key
🔐 Group Public Key
PK = C0A + C0B + C0C = (a0A + a0B + a0C) · G = x · G Ethereum address = last 20 bytes of keccak256(PK) The full private key x = a0A + a0B + a0C ← NOBODY computes this!
  • group_key.json — {public_key, ethereum_address} public

Feldman verification: share·G should equal C0 + index·C1. This catches cheaters without revealing the secret polynomial. If anyone lies about their shares, the check fails and we abort.

Phase 2: Threshold Signing (2-of-3)
5
Alice & Bob Pick Signing Nonces

Only 2-of-3 needed! Alice and Bob cooperate while Carol is offline. Each picks a random nonce (one-time secret) and publishes the corresponding curve point.

Alice (signing)
  • secret kA — random nonce
  • public RA = kA·G
Bob (signing)
  • secret kB — random nonce
  • public RB = kB·G
Carol (offline)

Not needed for 2-of-3

📢 Public Board
  • signing_message.json — "Send 1 ETH to 0xd00d" + hash
  • step5_alice_nonce.json — RA point (x, y)
  • step5_bob_nonce.json — RB point (x, y)
6
Compute Partial Signatures

Each signer uses their private nonce, private key share, and the public challenge to compute a partial signature. The math is Schnorr-style.

Both signers compute (from public data): R = RA + RB (combined nonce point) e = keccak256(R || message) (challenge hash)
Alice's partial sig
λA = Lagrange({1,2}, i=1) = 2 sA = kA - e · λA · xA mod N ↑secret ↑secret
  • public sA (partial signature)
Bob's partial sig
λB = Lagrange({1,2}, i=2) = -1 sB = kB - e · λB · xB mod N ↑secret ↑secret
  • public sB (partial signature)
📢 Public Board
  • step6_alice_partial.json — sA value
  • step6_bob_partial.json — sB value
  • step6_challenge.json — R point + e value

Lagrange coefficients adjust each party's key share for the specific subset signing. For parties {1,2}: λ1 = 2/(2-1) = 2 and λ2 = 1/(1-2) = -1. These ensure λA·xA + λB·xB = x (the full private key), even though nobody computes x directly.

7
Combine Partial Signatures

Anyone (even Carol, even a stranger!) can combine the partial signatures. No secrets needed — just addition on the public board.

s = sA + sB mod N Expanding: s = (kA - e·λA·xA) + (kB - e·λB·xB) = (kA + kB) - e·(λA·xA + λB·xB) = k - e·x That's a standard Schnorr signature! 🎉 k = combined nonce, x = full private key
✨ Final Signature
signature = (R, s) R = (x-coord, y-coord) ← combined nonce point s = scalar value ← combined partial sigs
  • final_signature.json
  • signature_details.json

The combined signature is indistinguishable from a single-signer Schnorr signature. The MPC protocol is completely invisible in the output.

8
Anyone Can Verify

Standard Schnorr verification. No MPC knowledge required. The verifier just checks one equation.

Schnorr verification equation: s·G + e·PK =? R Proof that this works: s·G = (k - e·x)·G = k·G - e·x·G = R - e·PK Therefore: s·G + e·PK = R ✅
✅ SIGNATURE VALID

The blockchain sees a normal signature from one Ethereum address.
No idea that 2 people made it together, or that a 3rd person exists.

Complete Variable Reference
All Variables by Visibility

📢 PUBLIC (anyone can see)

VariableWhat it isStep
C0i, C1iCommitment points (a0·G, a1·G)2
PKGroup public key = ∑C0i4
ETH addrkeccak256(PK)[12:]4
RA, RBNonce points (ki·G)5
RCombined nonce RA+RB6
eChallenge hash(R || msg)6
λA, λBLagrange coefficients6
sA, sBPartial signatures6
sCombined signature7

🔒 PRIVATE (only owner knows)

VariableWhoWhat it is
a0A, a1AAlicePolynomial coefficients
a0B, a1BBobPolynomial coefficients
a0C, a1CCarolPolynomial coefficients
fi(j)Receiver jShare from party i
xAAliceFinal key share
xBBobFinal key share
xCCarolFinal key share
kAAliceSigning nonce
kBBobSigning nonce
xNOBODYFull private key
File System Layout
playground/ ├── alice/ │ └── secrets.json private a0, a1, key_share, nonce ├── bob/ │ └── secrets.json private a0, a1, key_share, nonce ├── carol/ │ └── secrets.json private a0, a1, key_share ├── public_board/ │ ├── step2_*_commit.json public commitment points │ ├── group_key.json public group pubkey + ETH address │ ├── signing_message.json public message + hash │ ├── step5_*_nonce.json public nonce points │ ├── step6_*_partial.json public partial signatures │ ├── step6_challenge.json public R point + challenge e │ └── final_signature.json public combined (R, s) └── private_mail/ ├── alice_to_bob.json secret f_A(2) — only Bob reads ├── alice_to_carol.json secret f_A(3) — only Carol reads ├── bob_to_alice.json secret f_B(1) — only Alice reads ├── bob_to_carol.json secret f_B(3) — only Carol reads ├── carol_to_alice.json secret f_C(1) — only Alice reads └── carol_to_bob.json secret f_C(2) — only Bob reads
The Big Picture
KEY GENERATION SIGNING (2-of-3) Step 1 Pick secret polynomials Step 2 Post commitments Step 3 Whisper shares Step 4 Verify + combine → group key + key shares Step 5 Pick nonces Step 6 Partial signatures Step 7 Combine s = s_A + s_B Step 8: Verify s·G + e·PK = R ? ✅ Valid signature! key shares ready Participants: Alice (#1) Bob (#2) Carol (#3) All 3 participate in key generation Result: each gets a key share x_i and everyone knows the group public key Participants: Alice (#1) Bob (#2) Carol (offline) Only 2-of-3 needed! Any 2 can sign. Result: one signature that looks like it came from a single person

MPC Playground Visual Guide — Open in browser or print to PDF (Cmd+P / Ctrl+P)