DO NOT FORK THE REPOSITORY, AS IT WILL MAKE YOUR SOLUTION PUBLIC. INSTEAD, CLONE IT AND ADD A NEW REMOTE TO A PRIVATE REPOSITORY, OR SUBMIT A GIST
Use cargo run --release
to see it in action
|___ /| | / / | | | | | |
/ / | |/ / | |_| | __ _ ___| | __
/ / | \ | _ |/ _` |/ __| |/ /
./ /___| |\ \ | | | | (_| | (__| <
\_____/\_| \_/ \_| |_/\__,_|\___|_|\_\
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 MNT6753 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
It is assumed that you already know about elliptic curve, such as its point addition, scalar multiplication, ECDLP, Generator, order. These four articles describe them in detail. There is a fact that, in affine space the graph of the elliptic curve
Here we introduce Cycles of curves. As an example, this is the parameter of Secp256k1: q=0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFC2F
, the generator is point G, the amount of elements is order, and order=0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141
.
The Fields generated by mod q
is the base field, related to (x,y), and by mod order
is the scalar field, which is related to the coefficient of the point, such as
In SNARK, the witness prover input is the element in the scalar field, and the proof after instantiation is the point order
. At this time, we need to prepare two curves, and their q
and order
cross-equality. If you need a more detailed description, you can refer to here
When We look at the source code in main.rs
, you can find that two curves are used: ark_mnt4_753
and ark_mnt6_753
. According to the description of ark-mnt4-753:
The main feature of this curve is that its scalar field and base field respectively equal the base field and scalar field of MNT6_753.
Then I noticed that this is a MerkleTree, which is widely used in existence proof scenarios, such as Tornado Cash. arkworks provides a merkle-tree-example. If you have seen this example before, it will reduce your unfamiliarity.
In SpendCircuit
, the only things we customize are secret
and nullifier
. So we need to pay attention to how they are handled when they are inside the MerkleTree: We look at the implementation of generate_constraints()
in the ConstraintSynthesizer
trait.
-
Evaluate the secret with Tree's paramaters. The result need be equal to the input nullifier. We do not need to know what operations are performed in the
evaluate
. If we can get the correct secret, we can get the correct value by printingnullifier_in_circuit.value()
. If you want to figure out what evaluate does, you can check code, It just hashes the secret.let nullifier_in_circuit = <LeafHG as CRHSchemeGadget<LeafH, _>>::evaluate(&leaf_crh_params_var, &[secret])?; nullifier_in_circuit.enforce_equal(&nullifier)?;
-
Get the generator point of
G1Affine
and multiply the secret to get a new pointpk
. Note thatG1Affine
is defined as the mnt6 curve inark-mnt6-753
.let base = G1Var::new_constant(ark_relations::ns!(cs, "base"), G1Affine::generator())?; let pk = base.scalar_mul_le(secret_bits.iter())?.to_affine()?; # https://github.com/arkworks-rs/curves/blob/master/mnt6_753/src/curves/g1.rs#L9 pub type G1Affine = mnt6::G1Affine<crate::Config>;
-
Check that
pk.x
is indeed on the leaf of the Tree.// Allocate Leaf let leaf_g: Vec<_> = vec![pk.x]; // Allocate Merkle Tree Path let cw: PathVar<MntMerkleTreeParams, ConstraintF, MntMerkleTreeParamsVar> = PathVar::new_witness(ark_relations::ns!(cs, "new_witness"), || Ok(&self.proof))?; cw.verify_membership( &leaf_crh_params_var, &two_to_one_crh_params_var, &root, &leaf_g, )? .enforce_equal(&Boolean::constant(true))?;
-
It is required to use different
nullifier
, and the hidden condition is to use differentsecret
.assert_ne!(nullifier, nullifier_hack);
Therefore, we can find the inverse element of the point corresponding to mnt6
(because the Generator is mnt6
), then convert it to MNT4BigFr
, and then print out nullifier
, then used it as nullifier_hack
.
let zero_mnt6 = MNT6BigFr::from(0);
let leaked_secret_mnt6 = MNT6BigFr::from(leaked_secret.into_bigint());
let secret_hack_mnt6 = zero_mnt6 - leaked_secret_mnt6.clone();
let secret_hack = MNT4BigFr::from(secret_hack_mnt6.into_bigint());
let nullifier_hack = MNT4BigFr::from(BigInt([
3973337740569432013,
13414245347258487558,
2397428639358863969,
9003710485710992633,
7549792233030202438,
12021247171312009477,
10014790687727384398,
10719001394312000749,
12147986067925283906,
7048510174634313048,
13041399962513988848,
81761383246681,
]));