| Bank | ID | Spend key · pkspend | View key · pkview |
|---|
| Institution | Balance commitment · v·G + r·H | Amount |
|---|
Enygma Institutional Payments lets banks hold and move value confidentially on a shared ledger. It rests on the keys each institution mints at registration — and on a shared secret any two of them can derive without ever revealing it. Here is how the pieces fit together.
At registration every bank creates two independent keypairs with very different jobs. Keeping them separate is the whole idea: one keypair authorizes money, the other reveals transactions — and never the reverse.
Because visibility and control are different powers. A bank can hand over its view key — or encrypt just its view secret to the auditor at registration — granting full sight of its transactions while keeping spend authority entirely to itself. Separating the keys is what makes compliant privacy possible: auditable, never custodial.
Each bank calls register(pk^spend, pk^view) on the UserRegistry contract, building a public record layer — a table mapping every registered party to its two public keys. Only public keys touch the chain; the secrets never leave the institution.
| Index | Registered party | pk^view (ML-KEM ek) | pk^spend (hash) |
|---|
To open a private channel, one bank runs an ML-KEM encapsulation against the other's view key. It produces a ciphertext — the "capsule" — and a shared secret. The counterparty decapsulates the capsule with its view secret and recovers the exact same secret. Post-quantum key agreement, with no secret ever transmitted.
Every bank posts the hash of its shared secret with each counterparty into the on-chain matrix. Because both parties derive the identical secret, the matrix is symmetric: M[A][B] = M[B][A]. Each cell proves a consistent channel exists — while the one-way hash leaks nothing about the secret itself.
An optional auditor holds its own ML-KEM keypair. At registration a bank can encrypt its view secret to the auditor, granting complete transaction visibility. The auditor independently recomputes each shared-secret commitment from the public view keys and checks it against the matrix — flagging any inconsistency. It never receives a spend key, so it can see everything and move nothing.
Every account balance on the ledger is a Pedersen commitment C = v·G + r·H — a single elliptic-curve point that binds the value v under a random blinding factor r. The chain stores only the point; the amount is hidden. To pay, a bank posts k output commitments with a zero-knowledge proof, and the contract updates balances by adding commitments — never learning a value.
Every payment carries a fixed k = 6 commitments plus one proof — one commitment per account in the anonymity set. The sender's commits to −amount, the recipient's to +amount, and the rest to zero; because all six are Pedersen points with random blinding, they are indistinguishable. The contract verifies the proof, checks the nullifier is unseen, confirms the six commitments sum to the identity (so no value was created), and only then applies them.
check proves all balances still sum to the issued supply, without revealing one of them.Confidentiality is the default — but a bank can open a precise window for a regulator, at its own discretion. Because visibility is keyed, disclosure is granular: a bank chooses the smallest key that answers the question.
Privacy is not free: every confidential transaction needs a zero-knowledge proof. Enygma reaches financial-infrastructure throughput by pushing that work off the critical path and settling in batches — trading proving effort for on-chain verification that stays cheap and constant.
Answers at the level of the specification: primitives, envelope structure, what leaks and what does not, and where this build deliberately diverges from production.
C = v·G + r·H on the proving curve — BN254 in the spec, secp256k1 in this page. Additively homomorphic, perfectly hiding, computationally binding.pk^spend = H(sk^spend) over a 256-bit preimage. Spending is a zero-knowledge preimage proof, never a signature.C = H(pk^spend, salt, amount, tokenId) in a Merkle tree — not Pedersen.Everything protecting confidentiality is post-quantum. Everything protecting integrity is not. The asymmetry is deliberate, and it is the right way round.
Post-quantum: pairwise secrets come from ML-KEM-768, a lattice KEM with no known quantum attack — so an adversary recording the entire chain today cannot decrypt it after building a quantum computer. Harvest now, decrypt later does not work here. Everything downstream is symmetric or hash-based: r_i, t_i and K^n_ij are HKDF-SHA256 outputs and the payloads are AES-256-GCM. Spend authorization is a hash preimage, where Grover leaves roughly 2128 work against a 256-bit sk^spend. And a Pedersen commitment is perfectly hiding — for uniform r, C is uniform in the group and carries no information about v to any adversary of any power. (Enygma derives r from the pair secret rather than sampling it, so hiding against an outsider rests on HKDF pseudorandomness plus KEM secrecy — both post-quantum, but computational rather than perfect.)
Not post-quantum: Pedersen binding reduces to discrete log and Groth16 soundness to pairing assumptions on BN254. Shor breaks both. A quantum adversary could open a commitment to a value it never held, or forge a proof — a counterfeit-supply break, not a privacy break. It would learn nothing about anyone's amounts by doing so.
Closing that gap means replacing the commitment scheme and the proof system with lattice- or hash-based equivalents. That is a contained migration precisely because the key material, the discovery mechanism and the payload encryption are already post-quantum — the parts that leak history if broken later are the parts that are already safe.
If H = d·G with d known, then v·G + r·H = (v + rd)·G and whoever holds d can reopen any commitment to any value — binding collapses and they can mint at will.
So H is never chosen. It is derived nothing-up-my-sleeve, by hashing a fixed public constant onto the curve: H = Hash-To-Curve(00…00). Anyone can rerun the derivation and confirm it; no party gets a trapdoor, and there is no ceremony to trust. This page derives H the same way, hashing a SHA-512 IV constant to a point on secp256k1.
Circuit cost is counted in R1CS constraints, and SHA-256 is hostile to arithmetic circuits: its bitwise rotations and XORs have to be bit-decomposed, costing tens of thousands of constraints per compression. Poseidon is defined directly over the proving field with an algebraic S-box, so a permutation costs a couple of hundred constraints instead.
Every hash on a proven path is therefore Poseidon — pk^spend = H(sk^spend), the nullifier, the messaging tags, DvP note commitments, Merkle nodes. Hashes no circuit sees (HKDF derivation, AEAD) stay on standard SHA-256, where hardware acceleration is the thing that matters.
This build substitutes SHA-256 for Poseidon everywhere, because WebCrypto ships SHA-256 and not Poseidon. Every structural property demonstrated here — binding, derivation, tag matching, Merkle membership — is unchanged by that swap.
Groth16 has the smallest proofs and the cheapest constant-time verifier of the practical pairing SNARKs. That is the property that matters when a contract verifies a proof on every single transaction: verification cost does not grow with the circuit, so gas per transfer stays flat as the anonymity set and the constraint count grow.
The cost is a per-circuit trusted setup producing a Common Reference String. If the setup randomness ("toxic waste") survives, its holder can forge proofs — which breaks soundness and therefore supply integrity, but never confidentiality: a forged proof does not open anyone's commitments. Enygma runs the setup as an MPC ceremony among the Privacy Nodes, so soundness survives as long as one participant destroys its contribution. Since every Privacy Node is a regulated institution, that is a defensible assumption.
One fixed-shape envelope, identical no matter how many banks were actually paid:
Commit₁ … Commit_k ‖ nullifier ‖ t₁ … t_k ‖ π ‖ ctxt₁ … ctxt_k
k is the anonymity-set size (6 in this build). One commitment, one messaging tag and one ciphertext per slot; one nullifier and one proof per transaction. Because k commitments allow the sender to credit up to k−1 counterparties at once, and each ciphertext can itself carry per-client detail, a single transaction batches at two levels.
The contract does three things: check the nullifier has not been seen, verify π, and check Σ Commit_i = 𝒪. Then it applies the commitments homomorphically, all or nothing.
Because the recipient has to reproduce it. With r_i = Hash(s_ij, n_block), the recipient re-derives r_i from the pairwise secret and the block number, subtracts r_i·H from its commitment and is left with v_i·G. A freshly sampled r_i would have to be transmitted out of band; a derived one makes the commitment self-opening to exactly one party and opaque to everyone else.
The sender takes the only remaining degree of freedom: r_j = −Σ_{i≠j} r_i. That forces Σ r_i ≡ 0 (mod N), which together with Σ v_i = 0 makes the whole commitment vector sum to the curve identity — the property the contract checks in place of ever seeing an amount.
A sender has no shared secret with itself, so t_j needs some other input — and a careless choice is a fingerprint. Anything public, constant or reused across blocks would let an observer pick the sender's slot out of the k, which is precisely the fact the anonymity set exists to hide.
Enygma uses t_j = Hash(r_j^prev, n_block): the blinding factor that opens the sender's own previous balance commitment. It is secret, known only to the sender, unpredictable to everyone else, and already a witness in the circuit — so constraining it is free.
The consequence is visible in the receiving demo: no counterparty's channel secret reproduces t_j, so every recipient's trial ends in "no match" on the sender's slot. That is not a failure — it is the design.
nullifier = Hash(sk_j^spend, n_block) is deterministic in the spender's secret and the current block, so it is a per-block spend token: one transaction per account per block, enforced by a uniqueness set on chain. Without it, a sender could submit several transactions against the same shielded balance in one block, each individually well-formed, and overdraw.
Keying it to sk^spend rather than an account index is what makes double-spend prevention compatible with anonymity: the value is a hash of a secret, so it cannot be matched against any registered pk^spend. Keying it to n_block means nullifiers from different blocks cannot be correlated into a spending history for one institution. π constrains it to the same key used in the spend proof, so nobody can fabricate someone else's nullifier to censor them.
Six statements, all without revealing which slot the prover occupies:
k public keys in this anonymity set.k commitments.Σ v_i = 0.What it does not cover: the ciphertexts. Their correctness is deliberately outside the circuit.
Proving an AES-GCM encryption inside a SNARK is expensive enough to put real-time settlement out of reach, and the incentive analysis makes it unnecessary.
The commitment is well-formed regardless — π guarantees that — so a recipient is credited the money whether or not the payload is honest. A malformed ctxt_i only forces the slower recovery path: strip r_i·H from the commitment and solve for v in v·G. Meanwhile the failure is publicly attributable: the recipient can show that the block key derived from the pairwise secret does not authenticate the ciphertext, and AEAD authentication makes that claim checkable.
So the attack costs the sender real funds and produces a provable incriminating record against a regulated institution — while keeping the circuit cheap for everyone else. In this trust model that trade is the right one.
Either, and the protocol does not distinguish them. A commitment credits an account; what that credit represents is carried in the slot's ciphertext, which only the credited bank can open.
k−1 banks, and each of those payloads can itself cover many clients.Both ride identical envelopes. The commitment is the same object either way, the tag is derived the same way, and the padded ciphertext is the same length — so the chain cannot tell a settlement from a customer batch, and a single transaction can mix the two freely. Only the recipient learns which it received, and only for its own slot.
The count is metadata. If only credited slots carried a ciphertext, the envelope would announce how many banks were paid, and their positions would announce which ones. So every slot carries an equal-length ciphertext, and a decoy slot carries a real, openable payload for v = 0.
Equal length is not automatic — it has to be built. A settlement reference is far shorter than a list of end customers, and a decoy shorter still, so each plaintext is padded to a fixed size before encryption (256 bytes in this build; JSON ignores the trailing padding on the way out). Without that step the ciphertext length would leak both which slots were paid and what kind of payment each leg was.
Same reasoning for the tags: k tags, all the same width, so a five-way batch and an all-zero dummy transaction are byte-indistinguishable in shape. Fixing k rather than letting the sender pick it closes the same leak at the transaction level.
Learned: the k account indices in the set, k commitments, k equal-length tags, k ciphertexts, one nullifier, one proof — and that the commitments sum to the identity, so value was conserved.
Not learned: which slot is the sender, which slots were credited, any amount, whether this was a five-way batch or an all-zero dummy, and any balance. Every account starts at Com(0,0) = 𝒪, byte-identical across institutions, so an empty account and a settled one are the same kind of object.
The residual leak is participation: set membership is public, so an observer knows those k accounts were possibly involved in something.
For each transaction in the block, the node takes the tag at its own slot and compares it against Hash(s_ij, n_block) for each of the other k−1 members. That is k−1 hashes and no decryption — cheap and fully parallel.
A match names the sender. From the same pairwise secret the node derives K^n_ij and opens ctxt_i for the transfer detail, and derives r_i to verify the commitment: C_i − r_i·H must equal v_i·G. No match at any candidate means the node is itself the sender of that transaction — or has no channel to whoever sent it.
Discovery is trivial private information retrieval: each Privacy Node runs a full node, downloads the block and scans locally, so it never reveals to anyone which transactions interest it.
Normally it isn't. The amount arrives in the ciphertext, and C_i − r_i·H = v_i·G is used as a check on what the sender claimed, not as a recovery step.
When the payload is missing or maliciously formed, the node is left holding v·G and has to solve a discrete log — tractable only because v is a bounded monetary amount. Three options: a precomputed table of v·G over the plausible range, baby-step giant-step, or brute force up to the deployment's transfer cap. Deployments impose that cap partly to keep this fallback feasible.
You were a decoy: in the anonymity set, credited nothing. A transaction in which every slot is zero is a dummy — legitimate cover traffic that adds noise at the cost of one proof.
A decoy commitment is Com(0, r_i) with a freshly derived r_i, so on chain it is a uniformly distributed curve point exactly like a paying one. Only the holder of that slot can tell the difference, and only after trialing the tag.
There is no s_ij, so the sender cannot derive that slot's r_i or t_i and cannot place that bank in the anonymity set at all — not even as a decoy. On the receiving side, that bank has no candidate secret to trial against the sender's tag.
Coverage of the shared-secret matrix is therefore a hard operational constraint on both the achievable anonymity set and the discovery mechanism, not bookkeeping. Three pairs are deliberately left unagreed in this build, which is why some trial rows read no channel — cannot derive.
Within a single transaction it hides which of the k slots sent and which were credited; an observer's best guess at the sender is 1/k.
It does not hide that those k accounts appeared together. Over many blocks an observer accumulates a participation graph, and intersection analysis across transactions — not any single envelope — is the relevant threat. The defenses are set composition, dummy traffic, and the fact that per-block nullifiers and per-block tags cannot be linked across blocks, so co-occurrence is the only signal available.
At registration a bank encapsulates against the auditor's ML-KEM key, encrypts its own sk^view under the resulting secret, and publishes ("audit", B, ctxt_audit, ctxt_B) on chain. The auditor decapsulates with its view secret and recovers sk_B^view.
From then on the auditor can re-derive every pairwise secret that bank holds, and so can run the same tag trial, open the same payloads, re-derive the same blinding factors and independently recompute each channel commitment to check it against the matrix.
It never receives sk^spend. Since spend authority is proven only by knowledge of that preimage, the auditor can see everything and move nothing. Visibility and control are separate keys — that is the entire reason for the two-keypair split.
K^n_ij for a single block, encapsulated to the auditor and accompanied by a ZK proof that it was correctly derived from the pairwise secret — so the auditor can trust the key without ever holding the view key.sk^view. Complete visibility of that bank's activity, still zero spend authority.Per-block key rotation is what makes the narrowest tier meaningful: because K^n_ij = HKDF(s_ij, n_block), handing over one block's key says nothing about the same channel's earlier or later traffic.
The operator holds the contract owner key. It can halt transfer, withdraw and deposit — a circuit breaker that makes those calls revert — and it governs issuance parameters.
It cannot read: it holds no view key and no pairwise secret, and freezing the contract grants no visibility whatsoever. It cannot spend: balances move only against a valid π over a spend key it does not hold. Freezing is a liveness power, never a confidentiality or authorization one.
By adding points. Because commitments are additively homomorphic, summing every account's balance commitment yields a commitment to the aggregate: Σ C_i = Com(Σ v_i, Σ r_i). Issuance is on the public record, so anyone can verify that this summed point equals the commitment to the issued supply.
A transfer cannot change it: the envelope's own commitments sum to 𝒪, so adding them to the ledger leaves the aggregate fixed. The contract's check path performs exactly this comparison. At no point is any individual balance revealed — the invariant is verified entirely on aggregate curve points.
The two protocols optimize for different things. Payments needs additive commitments: the contract updates balances by adding points and verifies conservation by summing them, which only a homomorphic scheme permits.
DvP needs atomic exchange of specific assets. A note has to bind an owner, an asset id, an amount and a salt into a single leaf, be provable as a member of a Merkle tree, and be consumable exactly once. C = H(pk^spend, salt, amount, tokenId) does all of that in one Poseidon hash and hides all four fields behind a preimage. Homomorphism would buy nothing here, and an account-balance model cannot express "this exact bond against that exact cash."
It converts one commitment type into the other in a single call. Enygma.withdraw takes a vector of commitment deltas together with DvP deposit parameters, so the same transaction debits a Pedersen balance and mints notes.
The bank posts Com(−amount, −r) against its own account and Com(+amount, +r) against the DvP escrow account. The pair sums to 𝒪, so the supply invariant is untouched — value is not leaving the system, it is changing representation. The escrow commitment is the Pedersen mirror of the notes now in the tree, and the new leaf C = H(pk^spend, salt, amount, tokenId) is inserted at the next index.
Modelling note: on chain withdraw does not adjust totalSupply — only minting does — so this build carries the escrow account explicitly. That keeps Σ balances = issued supply visibly true across the bridge instead of appearing to leak value.
Alice's leg posts, in one transaction, everything both outcomes will need: C^out_B (Bob's side of the trade), C^out_A (what Alice receives), C^rev_A (Alice's refund note), nf_A, a deadline, π_A, and the payload addressed to Bob. swap_id = H(C^out_A, C^rev_A, nf_A, C^out_B, deadline) binds them into one object, and nf_A is marked locked rather than spent.
From there exactly two terminal states exist. Bob completes before the deadline: π_B is verified, nf_A and nf_B both become spent, and both output leaves are inserted. Or the deadline passes and anyone can revert: the locked nf_A is consumed and C^rev_A is inserted, returning Alice's asset. There is no reachable state in which one leg settles alone.
Alice encapsulates against Bob's ML-KEM view key and derives, from that one shared secret under distinct HKDF labels, salt_B^out, salt_A^out and the payload encryption key. Bob decapsulates, derives the identical values, and authenticates the payload with AEAD.
Then he does the check that matters: he recomputes C^out_B himself from pk_B^spend, the derived salt, and the amount and token he was told, and compares it to the commitment sitting on chain. If they match, the swap posted publicly is provably for the terms he was quoted — established without any circuit proving that Alice encrypted honestly.
If any check fails, Bob simply does nothing. The deadline reverts the swap and he has lost nothing but time.
No — and the tree is structurally incapable of recording it. A spend publishes nf = H(sk^spend, leafIndex) and proves in zero knowledge that some leaf in the tree opens under the key behind that nullifier, with the Merkle path and the leaf index kept private. The statement is, in effect: here is a nullifier for a leaf in the tree, I know its spend key and a path to it, and my output amounts sum to that leaf's amount.
So the contract cannot evaluate "is that note spent?". It can only evaluate "have I seen this nullifier before?", in that order: if the nullifier is already in the set the transaction reverts immediately, and only otherwise is the proof verified and the nullifier added. That is the entire double-spend defence, and it works without linking a nullifier to a leaf.
The tree is therefore append-only and carries no spent flag. Only a leaf's own holder can decide whether it is still spendable, by recomputing its nullifier and testing membership in the published set. This is why the notes table on the DvP tab has no status column: any such column would be information nobody actually has.
Leaf hashes, nullifiers, a Merkle root, a swap id and a deadline. Because pk^spend is inside the note hash, a leaf hides its owner as well as its asset and its amount — the tree is not a table of who holds what, it is a set of opaque digests. Nullifiers are H(sk^spend, leafIndex), unlinkable both to any registered key and to any leaf, so the tree never records which leaves are still live.
What is public is timing and lifecycle: that a swap of something was proposed against something, and that it later settled or reverted. Amounts, assets and both counterparties stay hidden.
Real, computed in your browser with WebCrypto and a hand-written secp256k1: every Pedersen commitment and point addition; the conservation checks Σ r ≡ 0 (mod N) and Σ C = 𝒪; all HKDF-SHA256 derivations under separate domain labels; the AES-256-GCM payloads, including the failed trial decryptions, which are genuine AEAD authentication failures rather than a rendered "no"; the tag trial; the note hashes, nullifiers and Merkle root; and the C − r·H = v·G opening check.
Substituted: SHA-256 stands in for Poseidon, since WebCrypto has no Poseidon. secp256k1 stands in for BN254. The ML-KEM exchange is modelled as a consistent pairwise secret rather than running a lattice KEM in JavaScript. And π is a placeholder digest — generating a real Groth16 proof is not a static-page operation.
None of the claims this page makes about conservation, discovery or attribution rest on the substituted parts. Each is re-derived independently from the exposed state by the accompanying headless test suites.