Experimenting with Homomorphic Encryption on PVM

Sharing some R&D from MontaQ Labs. Started as an experiment to see if homomorphic encryption was even practical on PVM. Turned out it is. We have a working contract on Passet Hub testnet. Figured this would be a good place to get feedback.


The Problem (you already know this)

Public blockchains have a privacy problem. Not a controversial take.

Your token balances? Public. Your salary from a DAO? Public. How you voted? Public. This is fine until it isn’t and for a lot of real-world use cases, it really isn’t.

The usual solutions are either:

  • ZK rollups (complex, latency, separate execution environment)

  • TEEs like SGX (trusted hardware = trusted Intel, which… yeah)

  • Privacy-focused chains (great, but now you’re not on Polkadot)

We wanted to try something different: what if regular PVM contracts could just do math on encrypted data?


What We Built

Summa is a Somewhat Homomorphic Encryption library for PVM. The key idea:

Encrypt(100) + Encrypt(50) = Encrypt(150)

The contract does the addition. It never sees 100, 50, or 150. Only the key holder can decrypt the result.

That’s it. That’s the trick.

We’re using Twisted ElGamal on JubJub. The “twisted” part means values are encoded in the exponent, which gives you additive homomorphism. You can add encrypted values, subtract them, multiply by public scalars all on-chain, all without decryption.

What you CAN’T do: multiply two encrypted values together. That would require fully homomorphic encryption, which is expensive and slow. We’re intentionally staying in “somewhat homomorphic” territory because it’s actually practical.


Cool, But Does It Work?

Yeah. We deployed a confidential token contract on Passet Hub testnet.

Contract: 0x68d64d2b645ff6da083ef35a90f3b3931ea20b29

Network: Passet Hub Testnet

What we tested:

  • Deployed the contract (~48KB, fits fine)

  • Registered public keys

  • Minted encrypted tokens

  • Did homomorphic balance updates

  • Decrypted client-side and got correct values

The moment where Enc(500) + Enc(100) decrypted to 600 was genuinely satisfying. Math works.


What Can You Actually Build With This?

Confidential Tokens

The obvious one. ERC-20 but balances are encrypted. Only you can see your balance. Transfers update encrypted values. Nobody watching the chain knows how much you have or how much you’re sending.

DAO Payroll

Your DAO pays contributors. Currently everyone can see who gets what. With Summa, the treasury contract maintains encrypted balances per contributor. Individual salaries are hidden, total spend is still auditable.

Sealed-Bid Voting

Submit Encrypt(votes) during voting period. Contract sums everything homomorphically. Decrypt the total only after deadline. No more watching how others vote and strategically following the herd.

Dark Pool Settlement

Combine with off-chain ZK for order matching. Summa handles the encrypted balance updates on-chain. Trade sizes stay private.


The Catch (There’s Always a Catch)

Let’s be honest about what this doesn’t do:

Still public:

  • Transaction graph — who transacts with whom is visible

  • Account existence

  • Total supply (though you could encrypt this too)

The range proof tax:

Here’s a fun attack: what if someone transfers Encrypt(-1000000) to themselves? Their balance goes up, yours wraps around. Classic underflow.

Solution: Bulletproofs-style range proofs. Every transfer proves 0 ≤ amount < 2^64 without revealing the amount.

Problem: range proof verification is expensive. We’re looking at ~500k gas per verification. Not cheap. This is probably the biggest blocker to making this practical for high-frequency use cases.

Decryption is… interesting:

Twisted ElGamal has a quirk. To decrypt, you solve a discrete log. For small values (like token balances), this is fast with Baby-Step Giant-Step lookup tables. For arbitrary values? Not happening.

We’ve optimized for 64-bit integers, which covers most financial use cases. Decryption happens client-side in milliseconds. But it’s worth knowing this constraint exists.


Why PVM?

Honest answer: because we wanted to see if it was possible.

PVM is new. The tooling is maturing. We wanted to push on it and see where it breaks. Turns out, it doesn’t break JubJub curve operations, SCALE-encoded ciphertexts, storage/retrieval of complex encrypted types, all works.

Also, there’s something appealing about privacy as a library rather than a separate chain. You’re still on Polkadot. You still get shared security. Your contracts just happen to work on encrypted data now.


What’s Next

Some things we’re thinking about:

  • Pallet-revive precompiles for range proof verification — could cut gas costs 10x

  • Multi-asset support — one contract, multiple confidential token types

  • Better decryption — precomputed BSGS tables for production

  • Maybe a ZK-rollup hybrid — Summa for state storage, Plonky2 for complex verification

But honestly, we’re at the “share it and see what happens” stage.


The Code

Everything’s open source:

Repo: GitHub - MontaQLabs/summa: Privacy-Preserving Smart Contracts on PVM via Twisted ElGamal Encryption

Structure:

  • summa/ — core crypto library (Twisted ElGamal, JubJub, etc.)

  • contracts/confidential-asset/ — the deployed token contract

  • tools/gen-ciphertext/ — CLI for encrypt/decrypt/keygen


Questions / Looking for Feedback

A few things we’d genuinely love input on:

  1. Alternative schemes? We went with Twisted ElGamal because it’s simple and well-understood. Anyone explored other options that might work better on PVM?

  2. Range proof costs. ~500k gas is rough. Ideas for bringing this down without sacrificing security?

  3. Use cases we’re not thinking of? What would you build with homomorphic operations on-chain?


That’s Summa. It’s not going to solve every privacy problem, but for the subset of use cases where you need balances hidden and math done on-chain, it works today. On PVM. Without trusted hardware.

9 Likes

I think the problem is we don’t have the PVM JIT yet? This should get a lot cheaper with the JIT (to the point a pre-compile wouldn’t add big gains anymore).

3 Likes

JIT will definitely make this faster, and I also wouldn’t discount future extensions to the instruction set to make these kinds of operations faster.

Once we’re done with JAM 1.0 and people build more stuff on PVM we’ll definitely be looking at how to make everything faster, and if we find that certain widely used operations can be made significantly more efficient with the addition of new instructions (e.g. SIMD) to the ISA then we’ll certainly add them.

Also, I certainly wouldn’t mind e.g. having a range proof benchmark added to my benchmarks here (hint hint) if someone would like to make a PR. Adding this benchmark won’t guarantee that I’d necessarily do any work to make it faster right now, but it could help to make it (and similar code) faster in the future.

4 Likes

Appreciate the context. JIT + potential ISA extensions makes sense as the path forward.

Would love to put together a range proof benchmark PR, our current implementation is Bulletproofs-style on JubJub, mostly scalar muls and point additions. Should be a decent stress test for curve arithmetic.

1 Like

There is obviously no such thing as a range proof verifier, only a verifier for some proof system, in which one circuit amalgamates multiple gadgets like a range proofs. It’s stupid to have hostcalls for whole proofs that do only one gadget of course.

In fact, individual gadgets have little impact upon the final proof system, in groth16 the cricuit only impact the SRS not the verifier, in a Chaum-Pedersen the circuit alters the verifier dramatically, and plonk lies in between.

Although verifiers can elide the circuit, there is never any reason to have hostcalls for individual proof systems either, because each proof system needs multiple different batch verifiers.

Groth16 has a single verifier, a direct batch verifier (10x faster or more), and snarkpack (even faster), but then Groth16 could be part of some zk continuation, which merges arbitrary other structure, and even other proof systems. Plonk or simple Chaum-Pedersen proofs would always have single and batch verifiers, but their MSMs could become highly specilized.

Also, folks would like proof systems that run on many different elliptic curves too, so any hostcall you provide quickly turns into like 20, which incurs maintenance costs, so..

We propose having hostcalls for MSMs and pairings across several elliptic curves (TODOs). Among elliptic curve based SNARKs, any verifier spends almost all its CPU time in a small constant or logarithmic number of such operations, so this gives us basically everything. As an example, single or batched Groth16 or Plonk would be one or two MSM, one miller loop, and one final exponentiation, so 3-4 host calls vs 1, but almost the same running time.

Now there is plenty more here that needs the JIT, like deserialisation requires field arithmetic, and even some divisions remain in PVM.

Is the JIT sufficient though? It’s imho unlikely.

Arkworks MSMs and pairings use both SIMD and multi-threading. We’d spend the CPU time elsewhere without the multi-threading, so if invoked from a contract then I’m unsure how much this matters, since contracts would not do SNARKs well anyways.

Assuming you need speed though, then you’ll always want a dedicate parachain aka service that integrates a batch verifier cleverly. in other words, you work around the FRAME macros by exploiting that runtimes are single threaded, use unsafe code to write into static muts, and do the batch verification in on_finalize. I suspect multi-threading within the hostcalls makes such code more cache friendly.

In brief, we’ve already plans to capture many layers of optimisations that exist in native code, while retaining much of the flexibility of native code. It’s not contract friendly of course, but that’s expected.

As I understand it, we shall deploy PVM in polkadot for both contact and runtimes, probably several years ahead of JAM ever becoming viable. JAM services avoiding the FRAME macros should make crypto integration simpler though.

1 Like

We should not call this “confidential tokens” since you’ve only hidden the payment values using El Gammal, but leave all the payment metadata visible. Instead like zcash etc, one should replace the parachain’s storage by a snark friendly Merkle tree, like Poseidon.

It’s painful since this digs deep into the FRAME macros, but so does NOMT integration. Rob H warned against those macros from early on. And sure enough they have chased away most teams who try building anything advanced. We could bit-the-bullet write a macro-free alternative for FRAME of course.

There are however many fun simpler things one does using El Gammal, especially in games: https://github.com/w3f/mental-poker

1 Like

Either that references a private repo, or the link is broken. If the former, could you please make it public? :folded_hands:

FRAME is operating on a key/value storage and doesn’t care about the underlying merkle tree.