5 weeks ago, Patract Hub applied a treasury proposal #24 for Megaclite v0.1. Megaclite project will be dedicated to introducing basic zero-knowledge proof underlying support for the Polkadot ecology, so that developers can easily develop applications at the upper level through WASM smart contracts or Runtime.
Now, we have finished all the works of v0.1 and you can review our source code at those repos. We will introduce more details at the following report.
We inherited the PairingEngine
trait through the CurveBasicOperations
trait and combined the three methods of ADD, MUL, and Pairings:
fn add(input: &[u8]) -> Result<Vec<u8>, SerializationError> { \\omit }
fn mul(input: &[u8]) -> Result<Vec<u8>, SerializationError> { \\omit }
fn pairings(input: &[u8]) -> Result<bool, SerializationError> { \\omit }
Three of the methods are exposed to the Runtime and ink! layers with byte slice interfaces. ADD and MUL return elliptic curve points in byte vector. Pairings are internally paired and accumulated in batches, and the result is then compared with Fqk::one()
. Return true if they are equal, otherwise just return.
The CurveBasicOperations
trait also encapsulates some different elliptic curve parameters needed to write groth16 verification code:
// curve basic parameters
const SCALAR_FIELD: &'static str;
const MODULUS: &'static [u8];
// G1 bytes length
const G1_LEN: usize;
// G2 bytes length
const G2_LEN: usize;
// Scalar bytes length
const SCALAR_LEN: usize;
// Curve ID is used for adaptation chain extension
const CURVE_ID: u32;
/// Pairing runtime interface
#[runtime_interface]
pub trait Pairing {
/// | curve | add | mul | pairing |
/// |-----------|------------|------------|------------|
/// | bls12_377 | 0x01000000 | 0x01000001 | 0x01000002 |
/// | bls12_381 | 0x01000010 | 0x01000011 | 0x01000012 |
/// | bn254 | 0x01000020 | 0x01000021 | 0x01000022 |
/// | bw6_761 | 0x01000030 | 0x01000031 | 0x01000032 |
fn call(func_id: u32, input: &[u8]) -> Option<Vec<u8>> {
curve::call(func_id, input).ok()
}
}
# Cargo.toml
[dependencies.curve]
package = "megaclite-arkworks"
git = "https://github.com/patractlabs/megaclite.git"
features = ["tests"]
default-features = false
megaclite-arkworks
supports no_std
,so we can import it directly in Runtime layer.
//! lib.rs
decl_module! {
#[weight = 10_000 + T::DbWeight::get().writes(1)]
pub fn wasm_bls12_377_add(origin) {
curve::tests::add(0x2a);
}
}
You can build the program like this:
# Clone the branch `curve-benchmark` of our fork
git clone https://github.com/patractlabs/jupiter.git \
--branch features/runtime-interfaces \
--depth=1
# Build the template
cd jupiter \
&& git submodule update --init \
&& cargo build -p jupiter-dev --all-features --release
# Check the command benchmark works fine
./target/release/jupiter-dev benchmark -p pallet_template -e wasm_bls_12_381_add
Then, run the benchmark scripts like this:
# 1. Under the jupiter repo
# 2. Has compiled the release version jupiter-dev
./target/benchmark.sh
Our benchmark result on ubuntu LTS 20.04. Time is measured in µs
| memory | processor |
|---------------------|-------------------------------------|
| 64GiB System memory | AMD Ryzen 9 5900X 12-Core Processor |
| Curve | Operation | Native Time(µs) | WASM Time(µs) | Speed(Native/WASM) |
|-------------------|--------------|-----------------|---------------|--------------------|
| bls12\_377 | add | 9.588 | 29.02 | ~3x |
| (~9.5x) | mul | 183.1 | 1893 | ~10x |
| | pairing\_two | 1732 | 15310 | ~7x |
| bls12\_381 | add | 13.9 | 28.31 | ~2x |
| (~10x) | mul | 177.1 | 1879 | ~10x |
| | pairing\_two | 1438 | 14770 | ~10x |
| bn254 | add | 5.631 | 16.05 | ~3x |
| (~5x) | mul | 107.7 | 534.3 | ~5x |
| | pairing\_two | 1150 | 5061 | ~5x |
| bw6\_761 | add | 26.9 | 92.94 | ~4x |
| (~13x) | mul | 956.8 | 14330 | ~15x |
| | pairing\_two | 5715 | 60960 | ~10x |
bilinearity: e(s * a, b) = e(s * b, a)
by using arkworks to generate test data.gronth16 is currently the zero-knowledge proof algorithm with the highest verification efficiency (only four pairings are required) and the smallest proof size, so we prefer to choose Groth16 proof system, its verification illustration is as follows:
In the paper, we can see that the core of verification is an equation:
For the convenience of use in the project implementation, the underlying pairing algorithm provide batch pairing calculation and accumulation, so we need to make a conversion:
It can be seen from the formula that four Pairings, l times ADD and MUL operation (related to the actual circuit) are required, and finally, the result of the four pairings will return true or false.
Our allocation for Megaclite function id in chain extension:
| curve | add | mul | pairing |
|------------|------------|------------|------------|
| bls12\_377 | 0x01000000 | 0x01000001 | 0x01000002 |
| bls12\_381 | 0x01000010 | 0x01000011 | 0x01000012 |
| bn254 | 0x01000020 | 0x01000021 | 0x01000022 |
| bw6\_761 | 0x01000030 | 0x01000031 | 0x01000032 |
The corresponding interface of Megaclite supports chain extension (called from ink! contract) through conditional compilation or directly executes related functions.
//! https://github.com/patractlabs/megaclite/blob/master/crates/curve/arkworks/src/lib.rs
/// Call curve functions using chain extensions
#[cfg(feature = "ink")]
pub fn call(func_id: u32, input: &[u8]) -> Result<Vec<u8>> {
Ok(ink_env::call_chain_extension(func_id, &Vec::from(input))?)
}
/// Call curve functions directly
#[cfg(not(feature = "ink"))]
pub fn call(func_id: u32, input: &[u8]) -> Result<Vec<u8>> {
Ok(match func_id {
0x01000000 => <ark_bls12_377::Bls12_377 as CurveBasicOperations>::add(input),
0x01000010 => <ark_bls12_381::Bls12_381 as CurveBasicOperations>::add(input),
0x01000020 => <ark_bn254::Bn254 as CurveBasicOperations>::add(input),
0x01000030 => <ark_bw6_761::BW6_761 as CurveBasicOperations>::add(input),
0x01000001 => <ark_bls12_377::Bls12_377 as CurveBasicOperations>::mul(input),
0x01000011 => <ark_bls12_381::Bls12_381 as CurveBasicOperations>::mul(input),
0x01000021 => <ark_bn254::Bn254 as CurveBasicOperations>::mul(input),
0x01000031 => <ark_bw6_761::BW6_761 as CurveBasicOperations>::mul(input),
0x01000002 => <ark_bls12_377::Bls12_377 as CurveBasicOperations>::pairings(input).map(b2b),
0x01000012 => <ark_bls12_381::Bls12_381 as CurveBasicOperations>::pairings(input).map(b2b),
0x01000022 => <ark_bn254::Bn254 as CurveBasicOperations>::pairings(input).map(b2b),
0x01000032 => <ark_bw6_761::BW6_761 as CurveBasicOperations>::pairings(input).map(b2b),
_ => Err(Error::new(ErrorKind::Other, "Invalid function id").into()),
}?)
}
[dependencies.curve]
package = "megaclite-arkworks"
git = "https://github.com/patractlabs/megaclite"
features = [ "ink" ]
Enable chain extension interfaces of Megaclite by using ink
feautre.
// ink! contract
#[ink(message)]
pub fn bls12_377_verify(
&self,
vk_gamma_abc: Vec<u8>,
vk: Vec<u8>,
proof: Vec<u8>,
public_inputs: Vec<Vec<u8>>,
) -> bool {
if let Ok(res) = groth16::verify::<Bls12_377>(&vk_gamma_abc, &vk, &proof, &public_inputs) {
res
} else {
false
}
}
You can build Jupiter by using the same way of above,but we need to pass groth16
to scripts/benchmark.sh
# 1. Under the jupiter repo
# 2. Has compiled the release version jupiter-dev
./target/benchmark.sh groth16
Our MiMC-Based Groth16 Verify Benchmark Results:
| Curve | Native Time(µs) | WASM Time(µs) | Speed(Native/WASM) |
|------------|-----------------|---------------|--------------------|
| bls12\_377 | 40860 | 60550 | ~1.5x |
| bls12\_381 | 39120 | 58400 | ~1.5x |
| bn254 | 30760 | 36800 | ~1.2x |
| bw6\_761 | 63798 | 172900 | ~2.7x |
NOTE: The implementation of MiMC-Based Groth16 Verifier here is that when Megaclite is imported into the contract, the verify function of ADD, MUL, Pairing can be run by calling the chain extension.
Test contract: https://github.com/patractlabs/metis/blob/master/groth16/lib.rs
According to the test results of the basic units , there is 5-7 times gap between the performance of the WASM version and the Native version. But, according to the test results of upper-level ink! contract for MiMC Groth16 Verifier, the speed only has little difference, and the time consumption (36-170ms) is acceptable for the block producer.
The WASM version does not need to modify the Host Calls in Native layer, so Megaclite will continue to develop the future roadmap in WASM layer, and suspend the development direction of the Native layer, leaving the native layer design to ParityTech and W3F Research. In addition, Jupiter will integrate Megaclite in runtime and ink! to provide a public online test environment.
MiMC implements a hash algorithm based on Field on the elliptic curve of alt_bn128, so its circuit implementation in the prove system of ZKP (based on the alt_bn128 curve) is very friendly.
The mimc implementation is shown in the figure below, using Sponge mode instantiated by MiMC permutation with a fixed key.
Source code:
let mut r = in_k.clone();
for i in 0..in_x.len() {
r = &r + in_x[i] + mimc_pe7(&mut in_x[i], &r, &in_seed, round_count) % &*SCALAR_FIELD;
}
In snark setting , MiMC-n/n block-cipher usually use Even-Mansour mode
Our MiMC-p/p with exponent of 7, so:
Source code:
let mut keccak = Keccak::v256();
let mut received = [0u8; 32];
keccak.update(&c.to_bytes_be()[..]);
keccak.finalize(&mut received);
c = U256::from_bytes_be(&received) % &*SCALAR_FIELD;
// x = (x + c_i + k)^7
t = &in_x + &c % &*SCALAR_FIELD + in_k % &*SCALAR_FIELD; // t = x + c_i + k
a = t.mulmod(&t, &*SCALAR_FIELD); // t^2
a = a.mulmod(&a, &*SCALAR_FIELD).mulmod(&a, &*SCALAR_FIELD);
in_x = a.mulmod(&t, &*SCALAR_FIELD); // t^7
It is built on JubJub curve. Jubjub is the twisted Edwards curve -u^2 + v^2 = 1 + d.u^2.v^2
of rational points over GF(q)
with a subgroup of prime order r
.
q = 21888242871839275222246405745257275088548364400416034343698204186575808495617
r = 21888242871839275222246405745257275088696311157297823662689037894645226208583
The choice of GF(q)
is made to be the scalar field of the Bn254 elliptic curve construction.
It also implements ETEC (Extened Twisted Edwards coordinate). In the Extended coordinate system, it can provide faster addition operations. In the Projective coordinate system, it can avoid inversion operations and provide faster double operations.
The core verification algorithm of EdDSA signature is as follows:
Where (s, R) is the signature, Pk is the public key, and h is the hash value of the message. Because R is generated by the private key and the message hash, EdDSA is also a deterministic signature algorithm.
Source code:
if let Some(lhs) = scalar_mult(GENERATE[0].clone(), GENERATE[1].clone(), s) {
let t = hash_to_u256(&input);
if let Some((pk_x, pk_y)) = scalar_mult(public_key[0].clone(), public_key[1].clone(), t) {
let [r_x, r_y] = r;
let etec_point = etec_add(
&point_to_etec(r_x, r_y, Q.clone()),
&point_to_etec(pk_x, pk_y, Q.clone()),
&*Q,
&JUBJUB_A.into(),
&JUBJUB_D.into(),
);
if let Some(rhs) = etec_to_point(etec_point, Q.clone()) {
return lhs == rhs;
}
}
}
false
We will propose v0.2 and v0.3 later after some time for research.