Trinity is a two-party computation (2PC) protocol designed to minimize interaction rounds, enable input verifiability, and facilitate reusability. It combines three key cryptographic concepts: Extractable Witness Encryption for Laconic Oblivious Transfer (LOT), Garbled Circuits, and PLONK.
This project is a collaboration with the Cursive team. They researched the topic and designed the scheme, while we helped on its development.
The core mechanism unifying these concepts is the KZG polynomial commitment scheme. Trinity offers a novel approach to handling user data by placing data objects at the center of computation. Users can commit to structured data (e.g., dictionaries), generate verifiable proofs about their contents, and privately interact with other parties.
Trinity redefines the role of ZK credentials, extending their use beyond traditional settings (e.g., authentication) to serve as verified private inputs within secure two-party computations (2PC).
We've just added it as a new template in mpc-hello, and it's time to show what it brings to the table.
To understand Trinity's novelty, let's first revisit how traditional secure two-party computation (2PC) works.
Let's start with a traditional secure two-party computation (2PC). Say Alice and Bob want to compute a + b
, without revealing their private inputs a
and b
to each other.
This is typically done using:
In most traditional protocols, OT is built using public key cryptography.
Let's say Alice has an input bit a â {0,1}
:
Together, these steps let two parties jointly compute a function without revealing their inputs.
Trinity follows the same broad structure â but introduces key innovations to make the flow more modular, more verifiable, and better aligned with ZK tooling.
Specifically, Trinity leverages:
In short, Trinity lets you garble circuits in the usual way, but with reusable, verifiable inputs â perfect for privacy-preserving apps, on-chain protocols, or even MPC-enabled credentials.
Now let's see how to build with it.
We need to write our 2PC circuit first, thanks to Summon is going to be super easy. Here we are going with a very simple add circuit, adding Alice and Bob numbers.
export default function main(a: number, b: number) { return a + b }
In the next snippet, we're going to initialise the trinity library and parse our circuit, so it can be consumed by both the garbler and the evaluator.
import * as summon from "summon-ts" import getCircuitFiles from "./getCircuitFiles" import { initTrinity, parseCircuit } from "@trinity-2pc/core" export default async function generateProtocol() { await summon.init() const trinityModule = await initTrinity() const circuit = summon.compileBoolean( "circuit/main.ts", 16, await getCircuitFiles() ) const circuit_parsed = parseCircuit(circuit.circuit.bristol, 16, 16, 16) return { trinityModule, circuit_parsed } }
The first phase of the protocol requires the evaluator, here Alice, to commit to its input. For the sake of the example we're going to use the Plain
setup (vs. Halo2, who's using full purpose ZK), performing a plain KZG commit.
// Call to the protocol generator we described above const protocol = await generateProtocol() // Create a Setup for our KZG protocol. // Note: For a production setting, these keys should be generated and published. // It should not be generated on the fly like in this example. const trinitySetup = protocol.trinityModule.TrinityWasmSetup("Plain") // Create an instance of a Trinity Evaluator // and will generate the commitment to Alice's input. // number: Alice's number a const evaluator = protocol.trinityModule.TrinityEvaluator( trinitySetup, intToUint8Array2(number) )
Now Alice can send both the parameters for the KZG setup and the commitment to her input.
const newSocket = await connect(code, "alice") newSocket?.send( JSON.stringify({ type: "setup", setupObj: Array.from(trinitySetup.to_sender_setup() || new Uint8Array()), }) ) newSocket?.send( JSON.stringify({ type: "commitment", commitment: evaluator.commitment_serialized, }) )
Bob has now received both the setup parameters and the commitment. He can now garble the circuit to encrypt his own input.
// Instantiate the Trinity wasm library const protocol = await generateProtocol() // Connect sockets await connect(joiningCode, "bob") // Get the messages from Alice const setupMessage = (await msgQueue.shift()) as SetupMessage const setupObj = new Uint8Array(setupMessage.setupObj) const commitmentMessage = (await msgQueue.shift()) as CommitmentMessage const commitment = commitmentMessage.commitment
// Instantiate KZG setup from evaluator's setup const garblerSetup = TrinityWasmSetup.from_sender_setup(setupObjValue) // Instantiate a Trinity Garbler from the commitment and Bob's input const garblerBundle = protocol?.trinityModule.TrinityGarbler( commitmentValue, garblerSetup, intToUint8Array2(number), protocol.circuit_parsed ) const serializedBundle = Array.from(new Uint8Array(garblerBundle?.bundle || [])) // Send back the garbled data socket?.send( JSON.stringify({ type: "garblerBundle", garblerBundle: serializedBundle, }) )
Now Alice can receive the garbeld data from the Bob and evaluate the circuit. Note that the process is asymmetric, and only Alice evaluate the circuit on here side and can send back the result to Bob.
// Receiving serialized Garbled data const bundleArray = new Uint8Array(parsedMsg.garblerBundle) // Reconstructing Garbled data const bundle = TrinityGarbler.from_bundle(bundleArray) // Evaluate our circuit const resultBytes = currentEvaluator.evaluate( bundle, currentProtocol?.circuit_parsed ) // Get the result const result = booleanArrayToInteger(resultBytes)
This is a minimal example â but Trinity supports:
Trinity represents a significant step forward in secure computation. It combines modularity, verifiability, and reusability, making it ideal for privacy-preserving applications. Check out mpc-hello and trinity to dive deeper, or come chat with us in PSE Discord!
Create secure MPC apps easily in TypeScript.
A safe, performant, modular and portable multi-party computation (MPC) library.
PSE-Halo2 is a re-architected, KZG-backended fork of Zcash's Halo2, an instantiation of PLONK, with support for more curves and other features.