Bob was deeply inspired by the Zcash design [1] 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...
[1] https://zips.z.cash/protocol/protocol.pdf
This challenge makes use of Arkworks crates ark_mnt4_753
and ark_mnt6_753
, respective implementations of the MNT4-753 (Miyaji–Nakabayashi–Takano, embedding degree 4, over a 753-bit field) and MNT6-753 (similar but embedding degree 6) elliptic curves. We will use
We'll use
main.rs
contains various type aliases and definitions, the most important of which is the following
struct SpendCircuit {
pub leaf_params: <LeafH as CRHScheme>::Parameters,
pub two_to_one_params: <LeafH as CRHScheme>::Parameters,
pub root: <CompressH as TwoToOneCRHScheme>::Output,
pub proof: Path<MntMerkleTreeParams>,
pub secret: ConstraintF,
pub nullifier: ConstraintF,
}
Assume there is a list of "leaves", and hashing successive pairs of leaves (via parameters specified by leaf_params
and two_to_one_params
) yields a Merkle tree with root root
. The idea of a SpendCircuit
is that it captures the statement that secret
is among the leaves of the given Merkle tree as established by a Merkle authentication path proof
, and the hash of secret
is nullifier
. Intuitively, the leaf secret
is nullified or "spent". One can imagine some separate collection of such nullifiers, recording (but without revealing) those leaves that have been spent. There is a somewhat similar notion of spend transaction in Zcash.
The precise statement captured by a SpendCircuit
is specified in detail in its implementation of the ConstraintSynthesizer
trait, which is used to build a R1CS (rank-1 constraint system). For example, the following allocates root
as a public input of the R1CS:
let root = <LeafHG as CRHSchemeGadget<LeafH, _>>::OutputVar::new_input(
ark_relations::ns!(cs, "new_digest"),
|| Ok(self.root),
)?;
Similarly, nullifier
is also allocated as a public input. leaf_params
and two_to_one_params
on the other hand are treated as constants (which don't allocate any variables or constraints). Finally, the following
let secret = FpVar::new_witness(ark_relations::ns!(cs, "secret"), || Ok(self.secret))?;
let cw: PathVar<MntMerkleTreeParams, ConstraintF, MntMerkleTreeParamsVar> =
PathVar::new_witness(ark_relations::ns!(cs, "new_witness"), || Ok(&self.proof))?;
allocates secret
and proof
as private witnesses.
Let's look at the constraints on these variables. First, the following
let secret_bits = secret.to_bits_le()?;
Boolean::enforce_smaller_or_equal_than_le(&secret_bits, MNT6BigFr::MODULUS)?;
is a constraint asserting that secret
is no larger than secret
's type is the same as that which the R1CS constraints are defined over
type ConstraintF = MNT4BigFr;
that is, it is a field element
let nullifier_in_circuit =
<LeafHG as CRHSchemeGadget<LeafH, _>>::evaluate(&leaf_crh_params_var, &[secret])?;
nullifier_in_circuit.enforce_equal(&nullifier)?;
computes the hash (according to leaf_params
) of secret
inside the circuit, and asserts that it matches the public input nullifier
. Finally for the following,
let base = G1Var::new_constant(ark_relations::ns!(cs, "base"), G1Affine::generator())?;
let pk = base.scalar_mul_le(secret_bits.iter())?.to_affine()?;
let leaf_g: Vec<_> = vec![pk.x];
cw.verify_membership(
&leaf_crh_params_var,
&two_to_one_crh_params_var,
&root,
&leaf_g,
)?
.enforce_equal(&Boolean::constant(true))?;
computes the scalar multiplication leaf_g
its (affine) cw
proves that leaf_g
is indeed a leaf in the Merkle tree. That is, hashing leaf_g
successively along the authentication path cw
indeed yields root
.
let rng = &mut ark_std::rand::rngs::StdRng::seed_from_u64(0u64);
let leaves: Vec<Vec<MNT4BigFr>> = from_file("./leaves.bin");
let leaked_secret: MNT4BigFr = from_file("./leaked_secret.bin");
let (pk, vk) = from_file("./proof_keys.bin");
let leaf_crh_params = poseidon_parameters::poseidon_parameters();
let two_to_one_crh_params = leaf_crh_params.clone();
let i = 2;
We are given the leaves
, which by inspection is a vector containing 4 singleton vectors, each holding a field element in leaked_secret
which is used to derive the nullifier
let nullifier = <LeafH as CRHScheme>::evaluate(&leaf_crh_params, vec![leaked_secret]).unwrap();
Now we build a Merkle tree from leaves
, and an authentication path tree_proof
from the leaf
to its root:
let tree = MntMerkleTree::new(
&leaf_crh_params,
&two_to_one_crh_params,
leaves.iter().map(|x| x.as_slice()),
)
.unwrap();
let root = tree.root();
let leaf = &leaves[i];
let tree_proof = tree.generate_proof(i).unwrap();
We build our first SpendCircuit
with the above data as follows, along with a convincing Groth16 proof of the circuit:
let c = SpendCircuit {
leaf_params: leaf_crh_params.clone(),
two_to_one_params: two_to_one_crh_params.clone(),
root: root.clone(),
proof: tree_proof.clone(),
nullifier: nullifier.clone(),
secret: leaked_secret.clone(),
};
let proof = Groth16::<MNT4_753>::prove(&pk, c.clone(), rng).unwrap();
assert!(Groth16::<MNT4_753>::verify(&vk, &vec![root, nullifier], &proof).unwrap());
Note that by definition of the circuit:
-
nullifier
is the hash ofleaked_secret
(which is no surprise since that is hownullifier
was computed above). -
leaked_secret
is an element$s\in\mathbb{F}_r^{(6)}$ . - Hashing
$x$ along the pathtree_proof
yieldsroot
, where$(x,y)=[s]G$ .
But recall tree_proof
is a path from the leaf
to root
, so 3. implies that leaf
.
The challenge now is to come up with an alternative secret and nullifier - secret_hack
and nullifier_hack
- such that a second SpendCircuit
with these values also induces a convincing proof, thereby implying a double spend!
let c2 = SpendCircuit {
// other fields same as c
nullifier: nullifier_hack.clone(),
secret: secret_hack.clone(),
};
let proof = Groth16::<MNT4_753>::prove(&pk, c2.clone(), rng).unwrap();
assert!(Groth16::<MNT4_753>::verify(&vk, &vec![root, nullifier_hack], &proof).unwrap());
Note that for a public key
Lemma 5.4.7. Let
From this, it is shown (in Theorem 5.4.8) that "taking the
This forms the basis of an attack. First let's do a scalar multiplication on leaked_secret
, following roughly what the circuit is doing, to obtain a public key. Recall from the previous section that its leaf
:
let g = G1Affine::generator();
let pk1 = g.mul_bigint(&leaked_secret.into_bigint()).into_affine();
let x1 = pk1.x().unwrap();
let y1 = *pk1.y().unwrap();
assert_eq!(&vec![*x1], leaf);
Inverting the public key
let pk2 = -pk1;
let x2 = pk2.x().unwrap();
let y2 = *pk2.y().unwrap();
assert_eq!(x2, x1);
assert_eq!(y2, -y1);
Now we would like to recover the secret giving rise to leaked_secret
. However we need to be careful - the following does not quite work:
let wrong_neg_secret = -leaked_secret;
let wrong_pk = g.mul_bigint(&wrong_neg_secret.into_bigint()).into_affine();
let wrong_x = wrong_pk.x().unwrap();
assert_ne!(wrong_x, x1);
that is, it does not give a valid public key for leaked_secret
is of type MNT4BigFr
, so we are inverting in
let neg_leaked_secret = -MNT6BigFr::from(leaked_secret.into_bigint());
let other_pk = g.mul_bigint(&neg_leaked_secret.into_bigint())
assert_eq!(other_pk, pk2);
confirms that inverting in the correct field
/* Enter your solution here */
let neg_leaked_secret = -MNT6BigFr::from(leaked_secret.into_bigint());
let secret_hack = MNT4BigFr::from(neg_leaked_secret.into_bigint());
let nullifier_hack = <LeafH as CRHScheme>::evaluate(&leaf_crh_params, vec![secret_hack]).unwrap();
/* End of solution */