Exploring the arkworks
Libraries
As explained in the introduction, the ZK Hack puzzles are a great opportunity to explore the arkworks
libraries.
Although we have a reasonable understanding of what verify
does, let us pause a moment and explain how to find its way in all the crates arkworks
provides.
Say we want to understand what the function Bls12_381::product_of_pairings
does exactly.
First, we check the path used to bring Bls12_381
into scope in the src/bls.rs module:
use ark_bls12_381::{Bls12_381, G1Affine, G2Affine};
This tells us that we need to look into the ark-bls12-381
crate.1
The first thing to know is that there are two possible places where to look for information: Docs.rs, the documentation host for Rust crates hosted at crates.io, and the arkworks GitHub repositories.2
Second, one has to be careful about which version of the crate the puzzle requires. For this, we must inspect the Cargo.toml file which lists the package dependencies:
[dependencies]
ark-std = "0.3"
ark-ff = "0.3"
ark-ec = "0.3"
ark-serialize = "0.3"
ark-bls12-381 = "0.3"
ark-crypto-primitives = "0.3"
rand = "0.8"
rand_chacha = "0.3"
hex = "0.4"
prompt = { git = "https://github.com/kobigurk/zkhack-prompt" }
blake2s_simd = "0.5.11"
We can see that the puzzle requires version 0.3 of the ark-bls12-381
crate.
Hence, we select to correct version of the ark-bls12-381
crate on Docs.rs as starting point of our exploration.
If you prefer to browse libraries on GitHub or locally, be careful to check out the correct commit: the ark-ec
crate is part of the algebra repository, the releases of which are listed here.
When entering product_of_pairings
in the search bar on top of the documentation page of the ark-bls12-381
crate, we don't get any hit.
This probably means that this function is part of a trait that the type Bls12_381
implements using the default implementation.
Hence, we search for this type instead, which leads us to its definition:
type Bls12_381 = Bls12<Parameters>;
Following the link to the definition of the Bls12
type, we see that it is defined in the models::bls12
submodule of the ark-ec
crate, which contains all the generic code for curves of the BLS family with embedding degree 12:
pub struct Bls12<P: Bls12Parameters>(_);
This illustrates how arkworks
uses traits to abstract common behaviour of various curves.
Bls12
is an empty struct parameterized by a generic type P
that must satisfy the trait bound Bls12Parameters
.
A specific curve of the BLS-12 family, such as BLS12-381, is then instantiated by defining an empty struct Parameters
implementing the Bls12Parameters
trait in a specific way.
We can now search for product_of_pairings
in the ark-ec
crate.
We are more lucky this time as we find out that it is part of the PairingEngine
trait.
Let's take a look at the code:
pub trait PairingEngine: Sized + 'static + Copy + Debug + Sync + Send + Eq + PartialEq {
// ...
/// Computes a product of pairings.
#[must_use]
fn product_of_pairings<'a, I>(i: I) -> Self::Fqk
where
I: IntoIterator<Item = &'a (Self::G1Prepared, Self::G2Prepared)>,
{
Self::final_exponentiation(&Self::miller_loop(i)).unwrap()
}
// ...
}
We can see that a default implementation is indeed provided, computing a product of Miller loops followed by a single final exponentiation.
You can keep digging from here and inspect how miller_loop
and final_exponentiation
are implemented for the BLS-12 family.
Note that the product_of_pairings
function has been replaced in version 0.4.0 of the ark-ec
crate by the multi_pairing
function.
Next, we will see what the into
method applied to curve points does.
Affine versus Projective Coordinates
The ark-ec
library allows you to work both with affine and projective coordinates and to easily switch between them.
For short Weierstrass curves, the affine representation corresponds to the GroupAffine
struct implementing the AffineCurve
trait:
pub struct GroupAffine<P: Parameters> {
pub x: P::BaseField,
pub y: P::BaseField,
pub infinity: bool,
// some fields omitted
}
The projective representation uses Jacobian (not homogeneous) coordinates and corresponds to the GroupProjective
struct implementing the ProjectiveCurve
trait:
pub struct GroupProjective<P: Parameters> {
pub x: P::BaseField,
pub y: P::BaseField,
pub z: P::BaseField,
// some fields omitted
}
Note in particular how the GroupAffine
struct needs to hold a boolean field infinity
indicating whether an instance is the point at infinity or not, whereas GroupProjective
needs not.
The trait Parameters
, an alias for ark_ec::models::SWModelParameters
, contains all parameters specifying a prime-order subgroup of an elliptic curve in short Weierstrass form:
pub trait SWModelParameters: ModelParameters {
const COEFF_A: Self::BaseField;
const COEFF_B: Self::BaseField;
const COFACTOR: &'static [u64];
const COFACTOR_INV: Self::ScalarField;
const AFFINE_GENERATOR_COEFFS: (Self::BaseField, Self::BaseField);
fn mul_by_a(elem: &Self::BaseField) -> Self::BaseField { ... }
fn add_b(elem: &Self::BaseField) -> Self::BaseField { ... }
}
What about types G1Affine
and G2Affine
?
Recall that in order to define a pairing one needs two prime-order subgroups and of elliptic curves defined on respectively and some field extension
Types G1Affine
and G2Affine
correspond to the affine representation of these two subgroups and are defined for curves of the BLS-12 family respectively here and there as:
type G1Affine<P> = GroupAffine<<P as Bls12Parameters>::G1Parameters>;
type G2Affine<P> = GroupAffine<<P as Bls12Parameters>::G2Parameters>;
Type P
must implement the Bls12Parameters
trait:
pub trait Bls12Parameters: 'static {
type Fp: PrimeField + SquareRootField + Into<<Self::Fp as PrimeField>::BigInt>;
type Fp2Params: Fp2Parameters<Fp = Self::Fp>;
type Fp6Params: Fp6Parameters<Fp2Params = Self::Fp2Params>;
type Fp12Params: Fp12Parameters<Fp6Params = Self::Fp6Params>;
type G1Parameters: SWModelParameters<BaseField = Self::Fp>;
type G2Parameters: SWModelParameters<BaseField = Fp2<Self::Fp2Params>, ScalarField = <Self::G1Parameters as ModelParameters>::ScalarField>;
const X: &'static [u64];
const X_IS_NEGATIVE: bool;
const TWIST_TYPE: TwistType;
}
As expected, types G1Parameters
and G2Parameters
must both implement the SWModelParameters
trait with a prime base field for G1Parameters
and a quadratic extension field for G2Parameters
.
Conversion
Conversion between affine and projective coordinates is handled by the From
and Into
traits.
These are very general and useful traits that you can read about here and here.
They provide respectively an associated function from
and a method into
, the latter one being generally derived from the former.
The function from
for creating a point in projective coordinates from a point in affine coordinates is implemented here:
impl<P: Parameters> From<GroupAffine<P>> for GroupProjective<P> {
#[inline]
fn from(p: GroupAffine<P>) -> GroupProjective<P> {
if p.is_zero() {
Self::zero()
} else {
Self::new(p.x, p.y, P::BaseField::one())
}
}
}
The converse function, creating a point in affine coordinates from a point in projective coordinates, is implemented here:
impl<P: Parameters> From<GroupProjective<P>> for GroupAffine<P> {
#[inline]
fn from(p: GroupProjective<P>) -> GroupAffine<P> {
if p.is_zero() {
GroupAffine::zero()
} else if p.z.is_one() {
// If Z is one, the point is already normalized.
GroupAffine::new(p.x, p.y, false)
} else {
// Z is nonzero, so it must have an inverse in a field.
let zinv = p.z.inverse().unwrap();
let zinv_squared = zinv.square();
// X/Z^2
let x = p.x * &zinv_squared;
// Y/Z^3
let y = p.y * &(zinv_squared * &zinv);
GroupAffine::new(x, y, false)
}
}
}
Note that there are also more explicit into_affine
and into_projective
methods which simply call into
.
All what we just said was for version 0.3 of the crate. The structs and traits have been renamed in version 0.4 as follows:
- struct
GroupAffine
Affine
- trait
AffineCurve
AffineRepr
- struct
GroupProjective
Projective
- trait
ProjectiveCurve
CurveGroup: Group
.
Hopefully the code of the verify
function should now make completely sense.
Note in particular how elliptic curve points are converted from affine coordinates to projective coordinates using method into
before being passed to product_of_pairings
.
It's now time to see what the hash_to_curve
function does exactly.
1: Note that hyphens are not valid characters in Rust identifiers, however it is possible (and idiomatic) to use them in package and crate names. Cargo automatically converts them to underscores. See here.
2: If you're curious about what guarantees we have that the source codes on github.com and crates.io are really the same, I recommend this interesting blog post by Eric Seppanen.