The Zellic cryptography team (Malte Leip, Mohit Sharma, and Avi Weinstock) and Sampriti Panda participated in the most recent ZK Hack competition, ZK Hack IV↗, consisting of three puzzles overall, with points determined by how fast one managed to solve it. We were happy to win the first puzzle and place second in the third one, securing a second place overall.
This ZK Hack’s puzzles consisted of small cryptographic applications written in Rust using the arkworks libraries↗. They combined the kind of cryptographic primitives that are typically used in projects surrounding ZK, but in each puzzle some kind of vulnerability was introduced as well. The task was then to understand the provided code, find the vulnerability theoretically, and then implement a solution leveraging the found vulnerability.
Puzzle 1: Gamma Ray
In the first puzzle, we were presented with parts of a small Zcash clone, the task being to double spend a note. Double spending is prevented in Zcash with the use of nullifiers; to spend a note, you must reveal a nullifier associated with that note, and this nullifier is then marked as used. Already used nullifiers are not allowed to be used anymore, so this prevents double spending as long as there is only one valid nullifier that can be computed for each note.
In the case of this puzzle, however, the relationship was roughly and , where is a point on an elliptic curve and refers to the x-coordinate in affine coordinates. But if is a point in affine coordinates on this elliptic curve, then it holds that and thus has the same x-coordinate. Thus the note does not change if we replace by (taken modulo the order of ), but the hash changes, so we obtain a second nullifier associated with the note. A more detailed write-up can be found on the ZK Hack page for this puzzle↗.
This was our solution code:
let secret_hack = MNT4BigFr::from(MNT6BigFr::MODULUS) - MNT4BigFr::from(leaked_secret);
let nullifier_hack = <LeafH as CRHScheme>::evaluate(&leaf_crh_params, vec![secret_hack]).unwrap();
Puzzle 2: Supervillain
The second puzzle concerned a combination of aggregated BLS signatures with a simple proof-of-knowledge scheme. Both of those used a pairing . We recently published an introductory article on pairings↗, so head over there if you would like an introduction.
In a sequence of signatures for a fixed hashed message , signature number is signed with a public key of the form , with a fixed element of . The signature is a normal BLS signature, so . Those can be aggregated with , and then verified by checking . That this equality follows from , and that in turn these equalities hold, is a consequence of the bilinearity of .
To prove that the signer actually knew , they can provide another elliptic curve element , and verification checks that , where is a fixed element.
In the puzzle, we were then given a sequence of public keys and proofs and for which verification of the proof of knowledge of succeeds (we do not get though). Our task was to come up with and , so that the proof-of-knowledge verification succeeds for this pair, and additionally a signature that would verify as a BLS signature for a specific hashed message using the aggregate key . This means our constraints were
and
and we have available for . The available equalities are usable for the first constraint but not the second, so to make the second one pass, we choose and , making both sides 0 by bilinearity1. The left hand side of the first constraint can then be rewritten as follows.
The first constraint thus means that we must choose in such a way that the following equality holds.
The puzzle is thus solved by using .
A more detailed write-up for this puzzle can again be found on its ZK Hack page↗.
This was our solution code:
let aggregate_rest = public_keys
.iter()
.fold(G1Projective::zero(), |acc, (pk, _)| acc + pk)
.into_affine();
let new_key = aggregate_rest.neg();
let proofs_raw = public_keys
.iter()
.enumerate()
.map(|(i, (_pk, proof))| *proof * Fr::from(i as u64 + 1).inverse().unwrap());
let proofs_add = proofs_raw.fold(G2Projective::zero(), |acc, el| acc + el).neg();
let new_proof = (proofs_add * Fr::from(new_key_index as u64 + 1)).into();
let aggregate_signature = G2Affine::zero();
Puzzle 3: Chaos Theory
In this puzzle, we again encountered BLS signatures, this time combined with ElGamal encryption. We were given an encrypted, authenticated message and had to find out which message it was, out of a small number of possibilities.
As in Puzzle 2, the sender has a secret key with public key , and so does the receiver. The ElGamal ciphertext of the message is then given by with (the receiver can decrypt by subtracting from ). The signature we are also provided is a BLS signature of a hash of the ElGamal ciphertext — so satisfying .
We have a small number of possible messages to check. To eliminate dependence on the message if we guessed correctly, we begin by considering , which will be in that case (and something different if ). Thus, if , we will have
While the middle steps above involve terms we do not know, we can check whether . If this equality holds, then .
As with the other puzzles, a more detailed write-up can be found on its ZK Hack page↗.
This was our solution code:
for (i, msg) in messages.iter().enumerate() {
let shared = blob.c.1 - msg.0;
let lhs = { Bls12_381::pairing(shared, blob.c.hash_to_curve()) };
let rhs = { Bls12_381::pairing(blob.rec_pk, blob.s) };
if lhs == rhs {
println!("Msg {i} is correct");
}
}
Conclusion
We would like to thank ZK Hack for organizing this competition and Geometry Research for creating these three interesting puzzles.
About Us
Zellic’s dedicated zero-knowledge team combines a distinguished skill set in advanced cryptography, vulnerability research, and competitive hacking. We review circuits in Circom, Halo2, and other frameworks for zkEVMs, zkVMs, privacy and identity protocols, and interoperability infrastructure.
Notable clients include rollups (Scroll), coprocessors (Axiom), privacy primitives (Nocturne), and zk-bridges (Polyhedra).
Contact us↗ for a ZK audit.
Endnotes
-
I am writing additively here to simplify exposition. ↩