What follows is a solution to ZK Hack IV, Puzzle 1. ZK Hack is a series of zero-knowledge cryptography CTFs, workshops, hackathons, and study groups. Thanks to the ZK Hack organizers for creating this CTF, Geometry for this puzzle, and the ZK Hack community. Thanks to Paul Gafni and Daira Hopwood for discussion.
You might be interested in this post if you’re interested in:
- a smattering of zk-related trivia about nullifiers and elliptic curves
- The process of (cryptographic) puzzle solving
Recommended reading order:
- read this post top to bottom for a concise description and solution of the puzzle first, some mathematical nuggets to hold onto, and finally the messy stream of consciousness log of problem solving.
- read this post back to front for the order in which it was actually written (messy problem solving log, then cleaned up and bloggified knowledge nuggets).
puzzle.solve()
background
all the knowledge nuggets you need to solve the puzzle
- We have a merkle tree constructed with the poseidon hash function over elliptic curve MNT_753_4, with public keys stored at the leaves.
- Miyaji–Nakabayashi–Takano (MNT) curves are pairs of elliptic curves suited for elliptic curve pairings; that is, the (scalar, base) field for MNT_753_4 correspond to the (base, scalar) field for MNT_753_6.
- A nullifier in Zcash is a unique hash of the public keys stored at the leaves of the merkle tree. For curve generator point
and secret scalar , the public key is computed simply, . Thus the nullifier may be computed: .[1] - The prover in the nullifier argument provides a ZK proof of knowledge of
such that the nullifier hash is correct:[2] - In this puzzle, we use the Groth16 prover to construct a zk proof of the correctness of the hash.
- Zcash uses the Jubjub elliptic curve for their nullifier argument. Jubjub does not support a pairing function.
- As remarked on in Zcash spec lemma 5.4.7:
Lemma 5.4.7. Let
Then (subgroup of Jubjub of order r).
The lemma lends itself to a storage optimization for nullifiers over Jubjub: if
That is, instead of taking the hash
We now demonstrate the exploit.
==🔴Stop! This is the 🚨puzzle solving police🚨! You have been given everything you need to find your own solution! Take ten minutes, pull out your pen, and reap the joy and mathematical gainz of problem solving🔴==
.
.
.
.
.
.
.
.
.
the ‘sploit
The puzzle solution in short
Suppose
Let
If there exists
Which we may implement as follows:
|
|
One hiccup. We assumed (hoped) that MNT_753_4
? It’s time to put our twisted Edwards hats on.
puzzle.deets()
extended background on twisted Edwards elliptic curves, Zcash lemma 5.4.7
Twisted Edwards, and proof Zcash Lemma 5.4.7 with added notes
This lemma was initially confusing; thanks to several folks who helped clarify, including the author of the lemma herself. Historical record of my confusion notes here.
It may be helpful to first recall the twisted Edwards (tE) curve equation[^3]:
The point addition law:
And the point doubling law:
Finally, note that on a tE curve,
Lemma 5.4.7. Let
Proof
If
Further,
Which implies that
Now, anticipating contradiction, let
But also, $2=-[2]P$. Therefore either:
, a contradiction, since only for- Or, doubling is not injective on the subgroup, which contradicts the subgroup’s having odd order.
Lemma 5.4.7 applied to MNT_753_4
Returning to the problem, we might ask why we were able to assume that, given MNT_753_4
, why
In the Weierstrass affine coordinate system, we’re working over an entirely different group law, and if
now we steal all the coins, ahem, submit a whitehat bug report
puzzle.log()
My puzzle solving log, cut short for readability. You might be interested in this as a map of my state of mind while thinking about the puzzle. Keeping a log helps navigate overwhelming context dumps, and to avoid doing wrong and/or stupid things repeatedly. I use Obsidian to take these notes, for which I have written an extensive setup and usage guide. To see the full log, see this hackmd.
After a bit of information gathering, we know a few things:
- curves: we’re using the MNT4/6 cycle of curves for our nullifier, which support a pairing function. Zcash uses the BLS12-381 (pairing-supporting) curves for the nullifier argument. No actually, they use Jub-jub for this, which is pairing-unfriendly.
both are pairing friendly.- MNT embedding degree 4 and 6,
while the BLS curves have embedding degree 12. Larger embedding degree means less efficient pairing computation, and a harder DLP. BLS has a smaller field size.
- nullifier argument:
where N is the nullifier, H the poseidon hash, r a random nonce, and sk the secret key- It does not look like there is a random nonce included included in the hash eval; we see
LEAFH::evaluate(&leaf_crh_params, [leaked_secret])
, where leaf params are just the poseidon config, and leaked secret is the secret key.
- It does not look like there is a random nonce included included in the hash eval; we see
- the zcash-style proof should show:
- knowledge of (sk, r) such that the nullifier hash is correct
- knowledge of (v,a,r) such that H(v,a,r)=C - that the coin exists
- v the note’s value, a the address, C a commitment to the coin’s existence
- that nullifier N has not already been published
- Poseidon is an algebraic hash function used in zk circuits. Its parameter selection here looks a little different from the parameters that Zcash selected, though a vuln in Poseidon parameter selection would be surprising to me, given the hints. Two differences noted in param selection, from the zcash spec:
- partial_rounds = 56 - diff; partial_rounds = 29
- alpha = 5 - diff; alpha = 17
- didn’t check the vector of raw values, or the MDS matrix
- Groth16 - the only element of the Groth16 proving system that factors in here is the specification of the SpendCircuit, so it seems unlikely that we’ll find any Groth16-related bugs. The circuit checks:
- outputvar from root
- paramsvar’s for leaf_params and two_to_one_params (which are the same)
- fpvar witness for secret
- outputvar from nullifier
- g1var constant for g1 generator
- pathvar witness for proof
- to construct leaf_g:
- assign g1var constant for G1 generator (base)
- construct public key from secret key
- leaf_g is the public key
- assert that:
- secret < MNT6’s modulus
- nullifier hash is correctly calculated
- leaf_g lies in the merkle tree at index 2
- recall leaf_g = g1_generator * secret
- Shape of the problem, we have:
- a merkle tree, with the target leaked secret at index 2, configured to hash with poseidon over the MNT4BigFr Field
- a nullifier constructed from the hash of the leaked secret
- we want to construct a cheater-secret such that:
- secret < MNT6’s modulus
- nullifier hash is correctly calculated:
N == LeafHG::evaluate(¶ms, &[my_secret])?;
- leaf_g lies in the merkle tree at index 2
- recall
leaf_g = (g1_generator * my_secret).x
- recall
- in other words, we have: want construct a secret that obtains a hash collision:
h(params, cheat_secret) == h(params, secret)
root
/ \
h(h0,h1) h(h2,h3)
| | | |
h0 h1 h2 h3 (this row: leaves.bin)
| | | |
l0 l1 l2 l3
^(leaked_secret.bin)
Issue: how are we going to construct a hash collision?
hint 1 updated: see zcash spec lemma 5.4.7 for bob’s reasoning on why to only use the x coordinate when storing public keys.
Lemma 5.4.7. Let
- initial thought: the hint suggests that the x coordinate for the point on the chosen curve may not be unique, i.e. that for
, there may exist such that . Then we would have:- a new nullifier:
and - but with leaf:
, such that the merkle proof verifies correctly.
- a new nullifier:
- by the hint, it seems likely that the point we want is
.
Given leaked secret
It’s not immediately obvious how we will peel off
Still, we’ve made some progress. The state of the problem is now:
- we want to obtain
such that where . Solving for directly would involve solving the discrete log problem. - we’re pretty sure that there’s an exploit on the pairing somehow to help us solve for
.
Lemma and proof
Lemma 5.4.7. Let
- if P is the point at infinity (u,v)=(0,1), but -P=(0,-1) which does not lie on the curve.
- all other points P have odd order.
- further,
. if v=0, then then then $2=(0,1)=O$, which would have even order.- unsure what the
argument is saying. is part of the curve equation, . Check: –nope. Maybe there’s another curve representation they’re working over, something twisted-ed related. Oh, u,v denotes twisted edwards notation.
- unsure what the
- if
then . - Let
be a point on the curve. We will show is a contradiction.- Then $[2]Q=-[2]P=2
Q.v=-P.v -v \ne v$- what? Should expand on that
- Therefore either
or else doubling is not injective on the curve, which contradicts the curve’s odd order.
- Then $[2]Q=-[2]P=2
- further,
Hint 2 released:
Note that the MNT curves are represented in Weierstrass from in the circuit, and use the fact that both (x,y) and (x,-y) are valid points for spending
Which confirms my suspicion that we want
algebraic insight!
We observe that for the order
mnt4::Fr
field, but the order mnt4::Fp
, so we’ll have to do a type conversion.
changelog
- 2024-01-22 - final draft
- 2024-01-24 - zk hack solution published; fix typos in blog and publish
footnotes
^3]: Why do we use twisted Edwards curves at all? Speed. Every Montgomery form elliptic curve has a birational Twisted Edwards equivalent, though not necessarily a simple Edwards equivalent. Edwards curves have fast addition algorithms, and Twisted Edwards curves are nearly as fast, enabling faster point operations for many curves currently in use.
-
Unrelated to this puzzle, the Zcash nullifier argument also includes a nonce, so that a public key can be used more than once:
. -
Unrelated to this puzzle, the Zcash nullifier must prove two other conditions: (1) that nullifier
has not already been published–this is not included in the proof for the puzzle, but is checked elsewhere in the puzzle driver. Would need to be checked in the proof in real life. And (2) the existence of the note(s) (coins) that will be spent, via proof of knowledge of: ( ); the value of the note, address possessing the note, and random nonce, respectively.