Skip to main content
The BLS signature example illustrates how to implement BLS signature verification inside a contract account. This example is based off of the Complex Account example. Although the main goal is to illustrate the practical use of the BLS12-381 functionalities in a relevant setting. It is good to have an understanding of how a contract account works, so walk through the Simple Account example first, but it is not required. Open in Codespaces Open in Codeanywhere

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: e(pk,H(m))=e(g1,σ)e(pk, H(m)) = e(g1, \sigma) Where pkpk is the public key, H(m)H(m) is hash of the message onto the G2 group, g1g1 is the generator point in the G1 group and sigmasigma is the signature. e(,)e(,) 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 pkaggpk_{agg} by adding up all the public keys (recall each public key is just a point on the G1 group), the aggregate signature σagg\sigma_{agg} by adding up individual signatures (which is just a point on the G2 group). Then the aggregate signature verification is just e(pkagg,H(m))=e(g1,σagg)e(pk_{agg}, H(m)) = e(g1, \sigma_{agg}) 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,
For digital signatures, the G1 and G2 groups can be used interchangeably. Public keys can be chosen as elements of G1 with signatures in G2, or the other way around. The choice involves trade-offs between execution speed and storage size. G1 offers smaller points and faster operations, whereas G2 has larger points and slower performance.
The example presented below is intended for demonstration purpose only
  • It has not undergone security auditing.
  • It is not safe for use in production environments.
Implementing a production-safe signature scheme requires deep understanding of the underlying cryptography and security considerations. Use this at your own risk.

Run the Example

First go through the Setup process to get your development environment configured, then clone the main branch of soroban-examples repository:
Or, skip the development environment setup and open this example in GitHub Codespaces or Code Anywhere. To run the tests for the example, navigate to the bls_signature directory, and use cargo test.
You should see the output:

Code

bls_signature/src/lib.rs
Ref: https://github.com/stellar/soroban-examples/tree/main/bls_signature

How it Works

The example contract stores a counter that can only be incremented if a set of owners have approved it. Open the bls_signature/src/lib.rs file or see the code above to follow along.

The Contract

This contract is fairly simple and standard. On __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

The 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 the bls_signature/src/test.rs file to follow along.
bls_signature/src/test.rs
Most of what’s here is needed in order to create the contract client and ensure the invocation of its 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.
We aggregate the signer public keys, by first converting the bytes into G1Affine, then add them all up.
To produce the signature, the message will first be hashed into G2 via 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.
Running this test will produce the following budget output (some portion of the output omitted for brevity), the total CPU consumption for signature verification is around 31M. And you can add as many additional public keys as you like. In general 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.
The .wasm file should be found in the target directory after building: