How Malformed Paillier Keys Leaked THORChain’s Vault Key
What our first GG20 reproduction got right—and the proof defect it missed
Update, 12 August 2026. This post originally documented a suspected full-signing oracle built from malformed Paillier material, Type 5 and Type 7 aborts, and weaknesses in THORChain’s TSS transport wrapper. THORSec’s later technical disclosure confirmed our malformed-key and residue-leak findings but revealed a simpler, complete exploit. Bob’s ordinary MtA proof leaked the mask that our test harness had instrumented, and Alice’s exact range-proof bypass was the degenerate commitment
Z = 1. The abort oracle and wrapper flaws were not needed. This revision separates the confirmed incident chain from the independent implementation findings.
On 15 May 2026, a newly churned THORChain validator compromised one Asgard vault and drained approximately USD 10.7M. The attacker reconstructed the vault’s full ECDSA private key, then signed and broadcast transactions directly. The resulting signatures were indistinguishable from valid vault signatures, so this was not a router bypass, smart-contract bug, or oracle manipulation.
We examined the incident-era stack: THORChain tss-lib tag v0.1.6 at commit 287e1e2, as used by thornode v3.18.0, plus the surrounding bifrost/tss/go-tss wrapper. Our first reproduction reached the correct residue equation but stopped one step short of the production extractor.
TL;DR
The exploit combined three implementation flaws:
- Malformed Paillier key admission. During keygen,
tss-lib v0.1.6accepted and persisted a 2048-bit modulus containing 17 attacker-known small primes and one large factor. Its legacy checks did not prove that the modulus was the product of exactly two appropriately sized primes. - A degenerate Alice range proof. During signing, the attacker used the out-of-range MtA plaintext \(k = N / p\). The verifier accepted
Z = 1, disconnecting the range proof from that plaintext. Grinding the remaining challenge for a 17-bit factor cost roughly \(2^{17}\) hash attempts and could be done offline. - A leaky Bob proof. Bob’s response included \(T_1 = e\beta' + \gamma\). In the old implementation, both the Paillier mask \(\beta'\) and blinding value \(\gamma\) were sampled below the roughly 2048-bit modulus \(N\), while \(e\) could be roughly 256 bits. The product \(e\beta'\) dominated \(\gamma\), leaking enough information to recover the mask and therefore the honest participant’s secret modulo \(p\).
Repeating the exchange across the known factors yielded enough congruences to reconstruct shares with the Chinese Remainder Theorem. The attacker caused repeated signing failures to obtain fresh MtA responses: THORChain reports 864 failures over roughly two and a half days. After recovering enough shares, the attacker reconstructed the vault key and signed the theft transactions outside TSS.
Our local work correctly demonstrated that the old keygen accepted a malformed modulus, that BobMid and BobMidWC accepted an encryption of \(N / p\) with a forged proof, and that revealing \(\beta'\) exposed the honest input modulo \(p\). It was wrong to treat \(\beta'\) as unavailable in production and to look to Type 5, Type 7, route mismatch, duplicate overwrite, or repair injection for the missing bridge.
Why the malformed modulus leaks residues
GG18/GG20-style threshold ECDSA uses Paillier encryption inside multiplication-to-addition, or MtA, subprotocols. Alice encrypts a private value, Bob homomorphically combines that ciphertext with his own private value, and the parties receive additive shares of the product without directly revealing their inputs.
This relies on Alice’s Paillier modulus being well formed and on the accompanying zero-knowledge proofs enforcing their stated bounds. Paillier public keys and proof parameters are published during distributed key generation, persisted by peers, and reused in later signing sessions.
For one attacker-known factor \(p\) of a malformed modulus \(N\), the malicious Alice chooses \(k = N / p\). Bob’s decrypted MtA response has the simplified form
\[ m_B = (N / p)b + \beta' \pmod N, \]
where \(b\) is Bob’s private input and \(\beta'\) is his Paillier mask. Reducing \(m_B\) modulo \(N / p\) reveals \(\beta' \bmod (N / p)\), but normally leaves many possible lifts of the mask. If the correct \(\beta'\) can be identified, Alice can subtract it and recover
\[ b \bmod p = \frac{m_B - \beta'}{N / p} \bmod p. \]
Repeating this for enough factors produces a system of congruences whose unique solution is Bob’s secret in the expected range.
Flaw 1: keygen admits a multi-prime modulus
The old key validation checked that \(N\) was 2048 bits, rejected divisibility by the 168 primes below 1000, and ran a 13-challenge proof intended to reject repeated factors. It did not establish that \(N\) contained exactly two large prime factors.
The disclosed incident modulus contained these 17 small primes:
65537, 65539, 65543, 65551, 65557, 65563, 65579, 65581, 65587,
65599, 65609, 65617, 65629, 65633, 65647, 65651, 65657
Their product is 273 bits. Multiplying it by a disclosed 1776-bit large factor produced the attacker’s 2048-bit \(N\). Every small factor sat above the legacy trial-division floor, the factors were distinct, and the resulting modulus passed the old checks.
Our admission harness, gg20_paillier_admission_and_forge.py, independently demonstrated the same verifier weakness with a different synthetic modulus containing factors beginning at 1009. That was evidence of the admission failure, not a reconstruction of the incident modulus.
Modern MOD and FAC proofs close this gap. MOD establishes the required two-prime structure, while FAC proves knowledge of a suitably bounded factorization. Both are necessary because a factor-knowledge proof alone can sometimes prove knowledge of two composite groups whose product is still \(N\).
Flaw 2: Z = 1 defeats Alice’s range proof
The attacker needed to encrypt \(N / p\), a roughly 2031-bit plaintext, instead of an honest curve scalar of roughly 256 bits. Alice’s range proof should have rejected it.
The old verifier did not reject the commitment term Z = 1. Because Z was supposed to bind the proof to the encrypted plaintext, setting it to the multiplicative identity made that part of the verification equation vacuous. The attacker could then grind the remaining Fiat-Shamir challenge until it was compatible with the selected 17-bit factor, at an expected cost of roughly \(2^{17}\) attempts.
Our pairwise harness used the same malicious plaintext and reached the same accepted security invariant, but its forged proof was non-degenerate. It followed the proof-shaping idea in SafeHeron’s public CheatProve technique and happened to complete after 8,253 challenge attempts. That test remains meaningful, but it was not the exact transcript construction used in the incident.
The incident fix is direct: reject Z = 1. The upstream hardening also rejects other degenerate proof elements such as S = 1 and S1 = S2.
Flaw 3: Bob’s proof leaks the Paillier mask
Our first test exposed Bob’s internal \(\beta'\) through instrumentation and confirmed that the decrypted response yielded the victim’s input modulo \(p\). We treated removing that instrumentation as an unsolved oracle problem.
The production protocol already leaked the missing information. Bob’s proof contained
\[ T_1 = e\beta' + \gamma, \]
with the old ranges
betaPrime < N
gamma < N
e < q
Here \(N\) is roughly 2048 bits and the secp256k1 order \(q\) is roughly 256 bits. The product \(e\beta'\) can therefore approach 2304 bits while \(\gamma\) remains roughly 2048 bits. Dividing \(T_1\) by the public challenge \(e\) gives a close approximation of \(\beta'\), resolving the ambiguity left after reducing the MtA response modulo \(N / p\).
The upstream fix rebalances the ranges:
betaPrime < q^5 # roughly 1280 bits
gamma < q^7 # roughly 1792 bits
e * betaPrime < q^6
The blinding term is then sampled from a range roughly 256 bits wider than the product it hides, so \(T_1\) no longer reveals a useful approximation of the mask.
From residues to the vault key
One MtA response reveals one honest input modulo one selected factor. The attacker repeated the exchange across the known factors, combined the residues with CRT, and reconstructed honest signing shares. The process was repeated against enough committee members to recover the threshold needed to reconstruct the full vault private key.
THORChain’s reconstruction attributes 864 signing failures over roughly two and a half days to the malicious validator. After collecting the useful MtA responses, the attacker deliberately caused signing to fail so peers retried. The failures looked like ordinary node flakiness and incurred penalties that the attacker was willing to absorb.
Once the private key was reconstructed, no TSS ceremony was needed. The attacker signed and broadcast ordinary chain transactions directly. This explains why proactive signing checks could not stop the theft: those checks never saw the attacker’s locally produced signatures.
What our first investigation got right
The initial package was unusually close to the final exploit:
- It identified the newly churned malicious-node model and the attacked vault.
- It tied the incident to THORChain’s stale
tss-lib v0.1.6fork. - It demonstrated malformed-modulus admission against the real old code.
- It constructed and passed a proof for a ciphertext encoding \(N / p\).
- It recovered the victim input modulo \(p\) once \(\beta'\) was visible.
- It exercised both
BobMidand the long-term-shareBobMidWCpath. - It found proof grinding at approximately the correct 17-bit scale.
- It correctly concluded that affected vault keys had to be retired rather than merely reopening signing after a software patch.
The pairwise harness therefore reached the exact residue primitive used in the incident. It instrumented the value that the production proof leaked.
What the initial model got wrong
The full-signing harness produced a real and clean experimental matrix:
| Candidate state | Instrumented outcome |
|---|---|
| Correct nonce-side and long-term residues | Valid ECDSA signature |
| Wrong nonce-side residue | Type 5 abort |
| Correct nonce-side but wrong long-term residue | Type 7 abort |
| Both wrong | Type 5 appears first |
Those observations remain true for the synthetic harness, but they were not the incident’s extraction oracle. The attacker did not need to guess residues and classify later aborts. An ordinary Round 2 MtA response already carried both the residue relation and a leaky proof of Bob’s mask.
That also removes the need for target-specific transport isolation. The actual attack required repeated pairwise MtA responses, not one broadcast branch for a target and a compatible branch for everyone else. The failed split-view strategies in our original work were falsifying a mechanism the attacker never needed.
Separate finding: go-tss route binding
The wrapper findings should be kept on their own terms. They are not part of the disclosed key-extraction chain.
In the examined go-tss wrapper, processTSSMsg decides whether to run reliable-broadcast hash confirmation from the outer WireMessage.Routing.IsBroadcast. Later, updateLocal passes the inner BulkWireMsg.Routing.IsBroadcast into tss-lib:
outer WireMessage.Routing.IsBroadcast = false # skips wrapper hash confirmation
inner BulkWireMsg.Routing.IsBroadcast = true # processed as a TSS broadcast
Focused tests showed that this route mismatch can bypass the wrapper’s broadcast hash-confirmation gate. Related tests demonstrated bulk-shape ambiguity, accepted-then-overwritten Round 5 state, incompletely bound TSSControl repairs, and blame records that can lose the signed offending body.
These remain valid implementation-hardening concerns in the public wrapper reviewed on 12 August 2026. They were not needed for mask recovery, retries, target selection, or incident attribution. We also have not established that every path remains reachable end to end with THORChain’s private patched TSS library.
Detection and forensics
The strongest keygen fingerprint is the disclosed factor set itself. Operators with saved peer Paillier material can compute the GCD of each \(N\) with the product of the 17 known factors. Any nontrivial result is decisive evidence that the saved modulus contains incident-shaped small factors.
Additional indicators include:
- a single validator responsible for a disproportionate number of signing failures over a short window;
- repeated useful MtA exchanges followed by a failure that causes the same transaction to be retried;
RangeProofAlicetranscripts containingZ = 1or other degenerate proof elements;- saved Paillier moduli that pass the legacy gate but fail modern MOD or FAC verification;
- the same suspicious Paillier structure recurring in keygens involving one participant.
Route mismatches, duplicate signing branches, and mismatched TSSControl responses remain useful indicators for the separate wrapper findings, but they should not be presented as fingerprints of this exploit.
Keygen, keysign, and blame transcripts should be preserved before local state is rotated or garbage-collected. Raw TSS artifacts can contain sensitive proof and abort data and should be reviewed and redacted before publication.
Fixes and adoption
All three exploit-breaking changes existed upstream before the incident:
| Exploit leg | Upstream change | Effect |
|---|---|---|
| Malformed modulus | BNB tss-lib 3d95e54 and 1a14f3a, released in the v2 line |
Adds FAC/MOD proof machinery and proof-session context. |
| Leaky Bob mask | BNB tss-lib 3d95e54, released in the v2 line |
Samples \(\beta' < q^5\) and \(\gamma < q^7\). |
| Degenerate Alice commitment | BNB tss-lib 5d01446, included in the v3 line |
Rejects Z = 1 and related degenerate values. |
THORChain’s public v3.18.1 release was an operational containment patch ensuring that Bifrost-internal signing flows honored HALTSIGNING; v3.18.2 still exposed the old public cryptographic dependency. The public dependency migration began with v3.19.0, which referenced a private v3 TSS library and instructed operators to use a private binary containing additional security patches. THORChain reports that validators upgraded to patched v3.19.1 before normal operation resumed.
The private library cannot be independently audited from the public checkout. Confidence that it contains the three relevant fixes therefore rests on THORSec’s technical disclosure, the corresponding public upstream changes, and THORChain’s integration and release evidence.
Remediation
Fixing this family requires action at several layers:
Vaults. Generate fresh vault keys under hardened keygen, sweep funds, and retire any vault whose shares may have leaked. Patching software does not make a reconstructable old private key safe again.
Keygen. Require MOD and FAC proofs with session-bound context. Reject malformed and unbalanced factorization, and preserve enough keygen evidence to replay historical material against the hardened verifier.
MtA proofs. Use mask and blinding ranges that statistically hide \(e\beta'\), and reject Z = 1 plus other degenerate range-proof elements.
Operations. Alert on concentrated signing failures by one validator and on repeated MtA-complete retries. Security monitoring should aggregate behavior across ceremonies instead of treating each failure as isolated flakiness.
Wrapper hardening. Independently bind the delivering peer, outer and inner routes, message type, session ID, body hash, bulk shape, and repair tuple before passing any message into the TSS state machine. Signing storage should be store-once for each sender and message type.
What this means for threshold systems
The incident was not one catastrophic break of Paillier or GG20. It was a composition of three implementation weaknesses: a malformed key passed keygen, a degenerate commitment passed Alice’s range proof, and an undersized blinding term leaked Bob’s mask. Any one of the corresponding fixes would have broken this particular chain.
The broader lesson is still cross-layer, but the layers must be named accurately. Cryptographic key validation, zero-knowledge proof ranges, state handling, transport binding, retry behavior, and operational monitoring all contribute to the security boundary. In this incident, the first three cryptographic failures completed the extractor; the transport findings were separate hardening work.
References
- THORChain Exploit Report #1, 20 May 2026.
- THORChain Exploit Report #2, 3 July 2026.
- THORChain Exploit Report #3 / THORSec technical deep dive, 6 August 2026.
- THORNode releases.
- THORChain
tss-libv0.1.6, commit287e1e2. - THORChain
thornodev3.18.0, commite5edbfc. - BNB
tss-libv2.0.0 release. - BNB
tss-libsecurity changes3d95e54,1a14f3a, and5d01446. - SafeHeron / Fireblocks Labs GG20 malicious-Paillier proof of concept.
- Fireblocks, GG18 and GG20 Paillier Key Vulnerability.
- Gennaro and Goldfeder, One Round Threshold ECDSA with Identifiable Abort, 2020.
- Gennaro and Goldfeder, Fast Multiparty Threshold ECDSA with Fast Trustless Setup, 2018.
Disclosure note: The reproduction package contains synthetic harnesses and fixtures and is available upon request.