Background on BLS Signature
There are plenty of good resources on BLS signature, for example the “BLS12-381 For The Rest Of Us” has a section on BLS digital signature. BLS Signature for Busy People is a also a good resource for a quick overview. For full reference check out the IETF draft. In short, we are verifying the following relation: Where is the public key, is hash of the message onto theG2 group, is the generator point in the G1 group and is the signature. denotes the bilinear pairing between a point in G1 and a point in G2.
The nice thing about pairing based signature is it enables signature aggregation. I.e. if you have multiple signers pk_0 .. pk_n on the a single message, you can compute the aggregate public key by adding up all the public keys (recall each public key is just a point on the G1 group), the aggregate signature by adding up individual signatures (which is just a point on the G2 group).
Then the aggregate signature verification is just
with a single pairing on chain, you can verify N signatures on the same message in constant time. In general, n unique messages takes n + 1 pairing operations to verify all signatures.
Hash of message H(m)
The message will need to be hashed on the curve H(m) for pairing operation to be applied. We follow the approach outlined in RFC 9380 - Hashing to Elliptic Curves.
The hashing method requires a unique domain separation tag (DST), it is highly advisable that your application choose a unique DST. For the requirements of DST, refer to section 3.1 of the RFC,
Run the Example
First go through the Setup process to get your development environment configured, then clone themain branch of soroban-examples repository:
bls_signature directory, and use cargo test.
Code
bls_signature/src/lib.rs
How it Works
The example contract stores a counter that can only be incremented if a set of owners have approved it. Open thebls_signature/src/lib.rs file or see the code above to follow along.
The Contract
__constructor(), which the host invokes at deployment, it initializes the aggregate public key of all the owners, the domain separation tag DST, and initializes the counter to 0.
It contains a single function increment which calls require_auth that will check the authorization condition defined later, and if success, increment and return the counter.
BLS Signature verification
CustomAccountInterface::__check_auth function implements the custom signature verification logic for this account.
env.crypto().bls12_381() initializes the bls12_381 module from which the BLS12-381 functions are available. The signature_payload contains the payload that was signed.
bls.hash_to_g2(&signature_payload.into(), &dst) hashes the message into the G2 group. agg_sig contains the aggregate signature which is another point in G2.
To perform the signature verification, we construct two vectors Vec<G1Affine> and Vec<G2Affine>, and call bls.pairing_check on them. The pairing check function performs e(pk, msg)*e(-g1, sig) == 1 which is equivalent to checking e(pk, msg) == e(g1, sig).
Tests
Open thebls_signature/src/test.rs file to follow along.
bls_signature/src/test.rs
CustomAccountInterface implementation for signature authorization. After the setup, calling env.try_invoke_contract_check_auth on the client will invoke the __check_auth logic we’ve defined in our contract.
The invocation will return nothing on success, and will panic on failure. env.cost_estimate().budget().print() at the end prints out the budget.
Signature aggregation
We first declare 10 random key pairs. These will be used as the signers of this test contract.G1Affine, then add them all up.
bls.hash_to_g2. Here we use our own defined DST.
To aggregate the signatures, we first produce individual signatures by having each signer sign the message. This means multiplying the secret key by the message’s G2 point. Then we add them all up. Here we use g2_msm, by an array of message (Vec<G2Affine>) points and an the array of secret keys (Vec<Fr>) and it computes their inner-product which is the aggregate signature we want.
pairing_check is a function with linear cost, so the more unique messages that needs to be signed, the higher cost. Here because all signers sign the same content (hash of the call stack of this contract), we can do this in constant time.
Build the Contract
To build the contract into a.wasm file, use the stellar contract build command.
.wasm file should be found in the target directory after building: