arkworks
algebra APIsThe arkworks
ecosystem is a set of state-of-the-art Rust libraries that collectively provide tools to program zkSNARKs.
zkHack puzzles will be using arkworks
libraries for elliptic curve and finite field arithmetic. This document is a helpful cheat-sheet to get started with using these libraries.
There are three important traits when working with finite fields: [Field
],
[SquareRootField
], and [PrimeField
]. Let's explore these via examples.
Field
]The [Field
] trait provides a generic interface for any finite field.
Types implementing [Field
] support common field operations
such as addition, subtraction, multiplication, and inverses.
``rust
use ark_ff::Field;
// We'll use a field associated with the BLS12-381 pairing-friendly
// group for this example.
use ark_bls12_381::Fq2 as F;
//
ark-stdis a utility crate that enables
arkworkslibraries
// to easily support
stdand
nostdworkloads, and also re-exports
// useful crates that should be common across the entire ecosystem, such as
rand`.
use arkstd::{One, UniformRand};
let mut rng = arkstd::rand::threadrng(); // Let's sample uniformly random field elements: let a = F::rand(&mut rng); let b = F::rand(&mut rng);
// We can add... let c = a + b; // ... subtract ... let d = a - b; // ... double elements ... assert_eq!(c + d, a.double());
// ... multiply ... let e = c * d; // ... square elements ... assert_eq!(e, a.square() - b.square());
// ... and compute inverses ...
assert_eq!(a.inverse().unwrap() * a, F::one()); // have to to unwrap, as a
could be zero.
```
SquareRootField
]In some cases, it is important to take square roots of field elements
(e.g.: for point compression of elliptic curve elements.)
To support this, users can implement the [SquareRootField
] trait for their field type. This
provides access to the following methods:
```rust use arkff::{Field, SquareRootField}; // As before, we'll use a field associated with the BLS12-381 pairing-friendly // group for this example. use arkbls12381::Fq2 as F; use arkstd::{One, UniformRand};
let mut rng = arkstd::rand::threadrng(); // Let's try to sample a random square via rejection sampling: let mut a = F::rand(&mut rng); while a.legendre().is_qnr() { // A square is also called a quadratic residue a = F::rand(&mut rng); }
// Since a
is a square, we can compute its square root:
let b = a.sqrt().unwrap();
assert_eq!(b.square(), a);
// Let's sample a random non-square let mut a = F::rand(&mut rng); while a.legendre().isqr() { a = F::rand(&mut rng); } // The square root should not exist: asserteq!(a.sqrt(), None); ```
PrimeField
]If the field is of prime order, then users can choose
to implement the [PrimeField
] trait for it. This provides access to the following
additional APIs:
```rust use arkff::{Field, PrimeField, FpParameters, BigInteger}; // Now we'll use the prime field underlying the BLS12-381 G1 curve. use arkbls12381::Fq as F; use arkstd::{One, Zero, UniformRand};
let mut rng = arkstd::rand::threadrng();
let a = F::rand(&mut rng);
// We can access the prime modulus associated with F
:
let modulus =
// We can convert field elements to integers in the range [0, MODULUS - 1]: let one: numbigint::BigUint = F::one().into(); asserteq!(one, num_bigint::BigUint::one());
// We can construct field elements from an arbitrary sequence of bytes: let n = F::fromlebytesmodorder(&modulus.tobytesle()); assert_eq!(n, F::zero()); ```
There are two traits that are important when working with elliptic curves
over finite fields: [ProjectiveCurve
], and [AffineCurve
]. Both traits
represent the same curve, but provide different underlying representations.
In particular, a [ProjectiveCurve
] representation of a curve point is generally
more efficient for arithmetic, but does not provide a unique representative
for a curve point. An [AffineCurve
] representation, on the other hand, is unique,
but is slower for most arithmetic operations. Let's explore how and when to use
these:
```rust use arkec::{ProjectiveCurve, AffineCurve}; use arkff::{PrimeField, Field}; // We'll use the BLS12-381 G1 curve for this example. use arkbls12381::{G1Projective as G, G1Affine as GAffine, Fr as ScalarField}; use ark_std::{Zero, UniformRand};
let mut rng = arkstd::rand::threadrng(); // Let's sample uniformly random field elements: let a = G::rand(&mut rng); let b = G::rand(&mut rng);
// We can add... let c = a + b; // ... subtract ... let d = a - b; // ... and double elements. asserteq!(c + d, a.double()); // We can also negate elements... let e = -a; asserteq!(e + a, G::zero());
// ...and multiply group elements by elements of the corresponding scalar field
let scalar = ScalarField::rand(&mut rng);
let e = c.mul(&scalar.intorepr()); // intorepr() converts the scalar into a BigInteger
.
let f = e.mul(&scalar.inverse().unwrap().intorepr());
asserteq!(f, c);
// Finally, we can also convert curve points in projective coordinates to affine coordinates. let caff = c.intoaffine(); // Most group operations are slower in affine coordinates, but adding an affine point // to a projective one is slightly more efficient. let d = c.addmixed(&caff); assert_eq!(d, c.double());
// This efficiency also translates into more efficient scalar multiplication routines. let efromaff = caff.mul(scalar.intorepr()); asserteq!(e, efrom_aff);
// Finally, while not recommended, users can directly construct group elements // from the x and y coordinates. This is useful when implementing algorithms // like hash-to-curve. let eaffine = e.intoaffine(); let ex = eaffine.x; let ey = eaffine.y; let isatinfinity = eaffine.iszero(); let newe = GAffine::new(ex, ey, isatinfinity); asserteq!(eaffine, newe); // Users should check that the new point is on the curve and is in the prime-order group: assert!(newe.isoncurve()); assert!(newe.isincorrectsubgroupassumingoncurve()); ```
[PairingEngine
] is the primary trait for working with pairings. It contains
associated types and methods that are relevant to pairing operations:
```rust use arkec::{ProjectiveCurve, AffineCurve, PairingEngine}; use arkff::{PrimeField, Field}; // We'll use the BLS12-381 pairing-friendly group for this example. use arkbls12381::{Bls12381, G1Projective as G1, G2Projective as G2, G1Affine, G2Affine, Fr as ScalarField}; use arkstd::{Zero, UniformRand};
let mut rng = arkstd::rand::threadrng();
// Let's sample uniformly random field elements:
let a: G1Affine = G1::rand(&mut rng).into();
let b: G2Affine = G2::rand(&mut rng).into();
// We can compute the pairing of a
and b
:
let c = Bls12_381::pairing(a, b);
// We can also compute the pairing partwise: // First, we compute the Miller loop: let cml = Bls12381::millerloop(&[(a.into(), b.into())]); let cfe = Bls12381::finalexponentiation(&cml).unwrap(); asserteq!(c, c_fe); ```
Most types in the arkworks
ecosystem implement the [CanonicalSerialize
]
and [CanonicalDeserialize
] traits. These traits enable converting these types
to canonical byte representations that are suitable for disk storage and network
communication. They also enable support for point compression.
```rust use arkec::AffineCurve; // We'll use the BLS12-381 pairing-friendly group for this example. use arkbls12381::{G1Projective as G1, G2Projective as G2, G1Affine, G2Affine}; use arkserialize::{CanonicalSerialize, CanonicalDeserialize}; use ark_std::UniformRand;
let mut rng = arkstd::rand::threadrng(); // Let's sample uniformly random field elements: let a: G1Affine = G1::rand(&mut rng).into(); let b: G2Affine = G2::rand(&mut rng).into();
// We can serialize with compression... let mut compressedbytes = Vec::new(); a.serialize(&mut compressedbytes).unwrap(); // ...and without: let mut uncompressedbytes = Vec::new(); a.serializeuncompressed(&mut uncompressed_bytes).unwrap();
// We can reconstruct our points from the compressed serialization... let acompressed = G1Affine::deserialize(&*compressedbytes).unwrap();
// ... and from the uncompressed one: let auncompressed = G1Affine::deserializeuncompressed(&*uncompressed_bytes).unwrap();
asserteq!(acompressed, a); asserteq!(auncompressed, a);
// If we trust the origin of the serialization // (eg: if the serialization was stored on authenticated storage), // then we can skip some validation checks: let aunchecked = G1Affine::deserializeunchecked(&*uncompressedbytes).unwrap(); asserteq!(a_unchecked, a); ```