Skip to content

BLS Signature Scheme

The BLS (Boneh–Lynn–Shacham) library offers a simple way to integrate BLS signature schemes into your DSS. BLS allows for efficient signature aggregation and multi-signature verification. By enabling the aggregation of multiple signatures into a single compact signature, the library reduces on-chain data size and computational overhead, making it ideal for use in threshold signatures and consensus algorithms.

This crate includes a complementary on-chain BLS library designed to interact seamlessly with it. The crate provides bindings and types that work directly with the on-chain BLS library, And also supports algebraic operations on BN254 curve points.

Note: 1. This crate operates on the curve BN254
2. The crate uses Ark_BN254 for low level elliptic curve algebra.
3. This crate operates upon the BN254 Keypair
4. Operations within the crate follow little endian format. Whereas in the ETH ecosystem operations are in big endian

Signing using BN254 keypair

For signing with the BN254 keypair, there are implementations for both the Signer and Verifier traits from the signature crate.

use eyre::Result;
use karak_kms::keypair::{
    bn254::{bls::signature::Signature, Keypair as Bn254Keypair},
    traits::Keypair,
};
use signature::Signer;
 
fn sign_using_bn254_keypair() -> Result<Signature> {
    // generate a random keypair for the BN254 curve
    let keypair = Bn254Keypair::generate();
 
    // random message
    let message = b"Hello, world!";
 
    // sign message using the keypair
    Ok(keypair.try_sign(message)?)
}

Verify message signed by BN254 keypair

To verify the validity of a signature you can use the verify method of the Verifier trait implementation on the G2Pubkey. The method takes in a message and signature.

use eyre::Result;
use karak_kms::keypair::{bn254::Keypair as Bn254Keypair, traits::Keypair};
use signature::{Keypair as SignatureKeypair, Signer, Verifier};
 
fn verify_message_signed_by_bn254_keypair() -> Result<()> {
    // generate a random keypair for the BN254 curve
    let keypair = Bn254Keypair::generate();
 
    // random message
    let message = b"Hello, world!";
 
    // sign message using the keypair
    let signature = keypair.try_sign(message)?;
 
    // extract the g2 pubkey from the keypair
    let g2_pubkey = keypair.public_key().g2;
    Ok(g2_pubkey.verify(&message, &signature)?)
 
    // alternatively, you could use the verifying_key method from the signature::Keypair trait to get the g2 pubkey and then verify
    // Ok(keypair.verifying_key().verify(&message, &signature)?)
}

Verify signature using both G1 and G2 pubkeys

We also provide an implementation of the Verifier trait on the combined pubkey, {g1: G1Pubkey, g2: G2Pubkey}.

use eyre::Result;
use karak_kms::keypair::{bn254::Keypair as Bn254Keypair, traits::Keypair as _};
use signature::{Signer, Verifier};
 
fn verify_message_signed_by_bn254_keypair() -> Result<()> {
    // generate a random keypair for the BN254 curve
    let keypair = Bn254Keypair::generate();
 
    // random message
    let message = b"Hello, world!";
 
    // sign message using the keypair
    let signature = keypair.try_sign(message)?;
 
    // extract the public key (containing both g1 & g2 pubkeys)
    let pubkey = keypair.public_key();
 
    // verify the signature
    Ok(pubkey.verify(&message, &signature)?)
}

Note: using the G2Pubkey is what you usually want to use for verifying BLS signatures, since signatures are nothing but G1Points themselves. The combined pubkey is useful only when you want to additionally verify that the 2 pubkeys are generated from the same private key.

Register to onchain BLS library

The onchain library requires an operator address and a bytes group containing the compressed data. This crate offers the register_operator_to_dss_with_data method, which transforms the provided data into the required format and submits it via a transaction to the contract utilizing the onchain BLS library.

use alloy::signers::local::PrivateKeySigner;
use alloy::{
    network::EthereumWallet,
    primitives::{Address, U256},
    providers::ProviderBuilder,
};
use eyre::Result;
use karak_rs::{
    contracts::Core,
    kms::keypair::bn254::bls::{
        registration::{BlsRegistration, OperatorRegistration},
        signature::Signature,
    },
    kms::keypair::bn254::{G1Pubkey, G2Pubkey},
};
use std::str::FromStr;
 
async fn register_operator() -> Result<()> {
    // address of the DSS contract
    let dss_address = Address::from_str("ADDRESS_OF_DSS_CONTRACT>")?;
 
    // importing points
    let g1_x = U256::from_str("0xb77a215b4f5cdd99f5ba438cc4996175bc729449b2cb250c3fb74eed2aaee62")?;
    let g1_y =
        U256::from_str("0x2158bc12a3fd6ab600d7a48a8b99a5defb35ac5a5bf652e368aeda7b97234cb6")?;
    let g2_x0 =
        U256::from_str("0x13de1b6acc2713a59bb7b57b71dcc47bb51e92bb5ce2ea27c22545b09ceb9e9")?;
    let g2_x1 =
        U256::from_str("0x80ce0e41fd5f198b867e8541cbc00b059d6e93e3611c2e2d3b55a7f2b519265")?;
    let g2_y0 =
        U256::from_str("0x2c2970386ad634cd092b2fb6308af892adee32057f139b3a7b3c3fe9559ded4e")?;
    let g2_y1 =
        U256::from_str("0x1873fc36edfb182034b6e1bbe05375c36f5bbbc39903fb431185787f28cb533d")?;
    let signature_x =
        U256::from_str("0x1294534ba6b2dc743ea1e8050e9e62526b7cb6d094ac5383bdd496568b1be0ba")?;
    let signature_y =
        U256::from_str("0x220338c1a0124231fa2ed8b56305a670b0c1f2d2ab43ceb6f69c3c5f56e109cf")?;
 
    // converting imported points to desired format
    let g1_pubkey = G1Pubkey::from((g1_x, g1_y));
    let g2_pubkey = G2Pubkey::from(([g2_x1, g2_x0], [g2_y1, g2_y0]));
    let signature = Signature::from((signature_x, signature_y));
 
    let registration = BlsRegistration {
        g1_pubkey,
        g2_pubkey,
        signature,
    };
 
    // setting up core instance
    let signer: PrivateKeySigner = "<PRIVATE_KEY>".parse()?;
    let wallet = EthereumWallet::from(signer.clone());
    let rpc_url = "<RPC_URL>".parse()?;
    let provider = ProviderBuilder::new()
        .with_recommended_fillers()
        .wallet(wallet)
        .on_http(rpc_url);
    let core = Core::new(Address::from_str("<Address_Of_Core>")?, provider);
 
    // calling registration
    core.register_operator_to_dss_with_bls(dss_address, &registration)
        .await?;
    Ok(())
}

Data Types

All the following data types are fully compatible with the onchain BLS library. They can be sent directly to the contracts, with any necessary type conversions handled automatically in the background.

G1 Point

G1 point represents an elliptic curve point on the G1 group. This data type is a wrapper on top of G1Affine defined by Ark_BN254

pub struct G1Point(pub G1Affine);

G2 Point

G2 point represents an elliptic curve point on the G2 group. This data type is a wrapper on top of G2Affine defined by Ark_BN254

pub struct G2Point(pub G2Affine);

Signature

A signature is another point on the G1 curve, created by converting a message into a G1 point and then performing scalar multiplication with the private_key which is a scalar field element.

BN254 point algebra

The Karak SDK supports algebraic operations on G1 and G2 curve points, such as aggregation, subtraction, and negation.

Aggregation

Aggregation can be performed on both G1 and G2 points, allowing multiple keys or signatures to be combined into a single aggregated value. This is particularly useful for use cases like aggregating public keys or signatures in distributed systems.

fn aggregate_points(point_one: G1Point, point_two: G1Point) -> G1Point {
    return point_one + point_two;
}

Negation

Negation is the operation that computes the inverse of a point and is applicable to both G1 and G2 points.

fn negate_point(point: G1Point) -> G1Point {
    return -point;
}

Subtraction

Subtraction is available for both G1 and G2 points. Under the hood, it negates the second point and then adds it to the first, effectively computing the difference between two points.

fn subtract_points(point_one: G2Point, point_two: G2Point) -> G2Point {
    return point_one - point_two;
}

Sum

Both G1 and G2 point implement the Sum trait that enables operations like this

let keys = (0..10)
    .map(|_| karak_rs::kms::keypair::bn254::Keypair::generate())
    .collect::<Vec<_>>();
 
let agg_key = keys.iter().map(|k| k.public_key().g2).sum::<G2Point>();
println!("Aggregated key: {agg_key}");

Aggregated Signature Verification

To aggregate signatures from multiple operators, each signs the same message. The aggregated public key on the G2 curve is then used to verify the aggregated signature on the G1 curve through elliptic curve pairings, ensuring that all the operators did in fact sign that particular message.


               | G2 Point    | G1 Point
    Operator 1 | PublicKey 1 | Signature 1
+   Operator 2 | PublicKey 2 | Signature 2
+   Operator 3 | PublicKey 3 | Signature 3
    --------------------------------------
    Aggregated | AggPubkey   | AggSign

Like each individual public key can verify its corresponding signature similarly, the aggregated public key AggPubkey can be used to verify the aggregated signature AggSign.

use karak_rs::kms::keypair::bn254::{bls::signature::Signature, G2Pubkey};
use signature::Verifier;
 
fn aggregated_sign_verification(
    pubkey1: G2Pubkey,
    sign1: Signature,
    pubkey2: G2Pubkey,
    sign2: Signature,
    message: &[u8],
) {
    let agg_pubkey = pubkey1 + pubkey2;
    let agg_sign = sign1 + sign2;
    assert!(agg_pubkey.verify(message, &agg_sign).is_ok());
}

Conversion to alloy types

As you can see in the registration example above, the data types used in the BLS library can be converted to and from tuples of the U256 alloy type. This is done to make the data types compatible with the onchain BLS library.

let keypair = karak_rs::kms::keypair::bn254::Keypair::generate();
 
let g1 = keypair.public_key().g1;
let g2 = keypair.public_key().g2;
 
let (g1_x, g1_y) = <(U256, U256)>::from(g1);
let ([g2_x1, g2_x0], [g2_y1, g2_y0]) = <([U256; 2], [U256; 2])>::from(g2);
 
assert_eq!(g1, G1Pubkey::from((g1_x, g1_y)));
assert_eq!(g2, G2Pubkey::from(([g2_x1, g2_x0], [g2_y1, g2_y0])));

Note that the conversion for a G2Point has the order reversed in the components for both x and y coordinates.

Additionally, these types implement the SolValue trait, so you can directly call .abi_encode() on them to get the ABI encoded bytes.

let keypair = karak_rs::kms::keypair::bn254::Keypair::generate();
let g1 = keypair.public_key().g1;
let g2 = keypair.public_key().g2;
(g1, g2).abi_encode();