Bob was deeply inspired by the Zcash design for private transactions and had some pretty cool ideas on how to adapt it for his requirements. He was also inspired by the Mina design for the lightest blockchain and wanted to combine the two. In order to achieve that, Bob used the MNT6-753 cycle of curves to enable efficient infinite recursion, and used elliptic curve public keys to authorize spends. He released a first version of the system to the world and Alice soon announced she was able to double spend by creating two different nullifiers for the same key...
Bob was exploited by Alice when he attempted to compose a frankenstein protocol. Let's review his protocol and understand what went wrong.
Elliptic Curves are a valuable building block in cryptography. When cast over a finite field useful primitives appear. A set of points that live on the curve form a group
Bob's protocol uses this property in his protocol. A secret key in his protocol is
let base = G1Var::new_constant(ark_relations::ns!(cs, "base"), G1Affine::generator())?;
let pk = base.scalar_mul_le(secret_bits.iter())?.to_affine()?;
Short Weierstrass form is a way to represent elliptic curves. Here is an example of Weierstrass curve represented in 2 dimensions over all real numbers:
In affine coordinates
The curves that Bob is using are represented in this form.
Twisted Edwards form is a different way of representing elliptic curves that can lead to more efficient arithmetic. The point of infinity is usually represented by
Snarks are a way to prove the correctness of a computation while optionally allowing the inputs to the computation to be hidden. The proof can be significantly smaller than the size of the inputs to the computation.
Snarks are made up of a polynomial IOP which translates a program into a polynomial representation of random values, and a polynomial commitment, which allows a protocol to evaluate a committed polynomials at those random values. Many polynomial commitements use elliptic curve cryptography.
In Bob's protocol, he proves a correct computation on a secret key without revealing that secret key. A proof is output and can be verified by an honest party (like a blockchain).
Recursion of snarks is where we take a proof (
Elliptic curve cycles are defined as curves in which the scalar field (
This is applicable to snark recursion. Poplular polynomial commitment schemes such as Groth16, KZG, and Bulletproofs, require the commitment to group elements. This means that the polynomial IOP has to be done in
When recursing snarks, the verifier of
Bob's protocol attempts to use a cycle of curves called MNT4-753 and MNT6-753.
Bob's circuit forces one to prove knowledge of a secret key which corresponds to a public key. There is a database of public keys and the circuit makes sure that the public key corresponding the the secret key is in that database.
There is an issue though. Alice was seemingly able to prove membership of a single public key using 2 different private keys.
Based on clue #1, Bob's representation of the public key element was inspired by a passage in the Zcash protocol specification.
See lemma 5.4.7 in the Zcash spec for Bob's reasoning on why to only use the x coordinate when storing the public keys.
This is reflected in his circuit's code.
let leaf_g: Vec<_> = vec![pk.x];
The leema from the Zcash spec that he is referring to is as follows:
Let 𝑃 = (𝑢,v)
$\in$ $J$ ($𝑟$ ). Then (𝑢,−v)$\not\in$ $J$ ($𝑟$ ).
Bob does not realize that JubJub uses a different form than MNT6-753 and therefore lemma 5.4.7 is not applicable to his application. Let's explore JubJub by negating a point:
use ark_ed_on_bls12_381::{Fr as JubJubFr, EdwardsAffine};
let scalar = JubJubFr::from(1_u64);
let negated_scalar = scalar.neg();
let g = EdwardsAffine::generator();
println!("{}", g * scalar);
println!("{}", g * negated_scalar);
(8076246640662884909881801758704306714034609987455869804520522091855516602923, 13262374693698910701929044844600465831413122818447359594527400194675274060458)
(44359628534463305569565938749481659123655942513071768018083136608083064581590, 13262374693698910701929044844600465831413122818447359594527400194675274060458)
In both points,
This is different than the result we would get from negating the point in Weierstrass form.
use ark_mnt6_753::{Fr as MNT6BigFr, G1Affine};
let scalar = MNT6BigFr::from(1_u64);
let negated_scalar = scalar.neg();
let g = G1Affine::generator();
println!("{}", g * scalar);
println!("{}", g * negated_scalar);
(3458420969484235708806261200128850544017070333833944116801482064540723268149235477762870414664917360605949659630933184751526227993647030875167687492714052872195770088225183259051403087906158701786758441889742618916006546636728, 27460508402331965149626600224382137254502975979168371111640924721589127725376473514838234361114855175488242007431439074223827742813911899817930728112297763448010814764117701403540298764970469500339646563344680868495474127850569)
(3458420969484235708806261200128850544017070333833944116801482064540723268149235477762870414664917360605949659630933184751526227993647030875167687492714052872195770088225183259051403087906158701786758441889742618916006546636728, 14437982565586988252717614566858499873667733940785577960142578199436225087194633258220659402675483745929828964457019403099345314677681955251765513742498632717710601561232363037930119372875928969272289155715227295725310348309432)
In both points,
This means that Bob's technique of only checking the
In the codebase, the leaked secret key is represented as a scalar element in MNT4-753. When it is passed into the circuit, it is passed in as bytes in little endian encoding.
When we look in the circuit, Bob decode's those bytes into an element in the scalar field of MNT6-753. That scalar is then used to generate the public key in the circuit. This means that exploiting the protocol will not be as easy as negating the secret key.
Alice still found a way though:
let secret_hack = MNT4BigFr::from_le_bytes_mod_order(
&MNT6BigFr::from_le_bytes_mod_order(
&leaked_secret.into_bigint().to_bytes_le()
)
.neg()
.into_bigint()
.to_bytes_le()
);
let nullifier_hack = <LeafH as CRHScheme>::evaluate(
&leaf_crh_params, vec![secret_hack]
).unwrap();
She first encoded the secret key as bytes and create an element in the scalar field of MNT6-753, exactly like the circuit does. She then negated that element and transformed it back into a scalar in MNT4-753. She used that to create a nullifier that did not resemble her original one and exploited the protocol.