ExEthBls is an Elixir library for BLS signature verification that wraps the highly optimized BLST library used by Ethereum 2.0. This library provides Ethereum-compatible BLS signature operations including signing, verification, and aggregation.
- ✅ BLS signature generation and verification
- ✅ Public key aggregation
- ✅ Signature aggregation
- ✅ Fast aggregate verification (all signatures on same message)
- ✅ Aggregate verification (different messages per signature)
- ✅ Ethereum 2.0 compatible BLS12-381 curve
- ✅ Uses the same highly optimized BLST library as Ethereum clients
The package is distributed from GitHub. Add it to your list of dependencies in
mix.exs, pinned to a release tag:
def deps do
[
{:ex_eth_bls, github: "blockscout/ex_eth_bls", tag: "v0.1.1"}
]
endNo Rust toolchain is needed: on compile, rustler_precompiled downloads the
NIF for your platform from the GitHub release of that tag and verifies it
against the checksum-Elixir.ExEthBls.Native.exs file committed in the
repository. Always use a release tag (not a branch or an arbitrary commit), as
only tagged commits have a matching release and checksum file.
# Generate a private key from a seed
seed = :crypto.hash(:sha256, "my secret seed")
private_key = ExEthBls.key_gen!(seed)
# Derive the public key
public_key = ExEthBls.sk_to_pk!(private_key)
# Sign a message
message = "Hello, Ethereum!"
signature = ExEthBls.sign!(private_key, message)
# Verify the signature
ExEthBls.verify(public_key, message, signature)
# => true# Create multiple signers
signers = for i <- 1..3 do
seed = :crypto.hash(:sha256, "signer#{i}")
private_key = ExEthBls.key_gen!(seed)
public_key = ExEthBls.sk_to_pk!(private_key)
{private_key, public_key}
end
message = "Collective message"
# Each signer signs the same message
signers_with_signatures =
signers
|> Enum.map(fn {private_key, public_key} ->
signature = ExEthBls.sign!(private_key, message)
{signature, public_key}
end)
{signatures, public_keys} = Enum.unzip(signers_with_signatures)
# Aggregate the signatures and public keys
aggregated_signature = ExEthBls.aggregate_signatures!(signatures)
aggregated_public_key = ExEthBls.aggregate_public_keys!(public_keys)
# Verify the aggregated signature
ExEthBls.verify(aggregated_public_key, message, aggregated_signature)
# => true
# Or use fast aggregate verification (more efficient)
ExEthBls.fast_aggregate_verify(public_keys, message, aggregated_signature)
# => true# Each signer signs a different message
messages = ["message1", "message2", "message3"]
signers_with_messages =
signers
|> Enum.zip(messages)
|> Enum.map(fn {{private_key, public_key}, message} ->
signature = ExEthBls.sign!(private_key, message)
{signature, public_key}
end)
{signatures, public_keys} = Enum.unzip(signers_with_messages)
aggregated_signature = ExEthBls.aggregate_signatures!(signatures)
# Verify that each signer signed their respective message
ExEthBls.aggregate_verify(public_keys, messages, aggregated_signature)
# => trueExEthBls.key_gen(seed)- Generate a private key from a seed (≥32 bytes)ExEthBls.key_gen!(seed)- Same as above but raises on errorExEthBls.sk_to_pk(private_key)- Derive public key from private keyExEthBls.sk_to_pk!(private_key)- Same as above but raises on error
ExEthBls.sign(private_key, message)- Sign a messageExEthBls.sign!(private_key, message)- Same as above but raises on errorExEthBls.verify(public_key, message, signature)- Verify a signature
ExEthBls.aggregate_public_keys(public_keys)- Aggregate multiple public keysExEthBls.aggregate_public_keys!(public_keys)- Same as above but raises on errorExEthBls.aggregate_signatures(signatures)- Aggregate multiple signaturesExEthBls.aggregate_signatures!(signatures)- Same as above but raises on error
ExEthBls.fast_aggregate_verify(public_keys, message, signature)- Verify aggregated signature where all signers signed the same messageExEthBls.aggregate_verify(public_keys, messages, signature)- Verify aggregated signature where each signer signed a different message
private_key- 32-byte binarypublic_key- 48-byte binary (G1 point on BLS12-381)signature- 96-byte binary (G2 point on BLS12-381)
This library is designed to be compatible with Ethereum 2.0 BLS signatures. It uses:
- BLS12-381 elliptic curve
- G1 for public keys (48 bytes compressed)
- G2 for signatures (96 bytes compressed)
- Same domain separation tags as Ethereum 2.0
If you need to build the native library from source, set the environment variable:
export EX_ETH_BLS_BUILD=true
mix deps.compile ex_eth_blsThis requires:
- Rust toolchain (1.70+)
- C compiler (for BLST)
Releases are fully driven by CI. Do not push the tag or create the GitHub release by hand: the tag must point at a commit that contains the checksum file, and that file can only be computed from the built NIF archives.
- Bump
versioninmix.exs(and the tag in the Installation section above) and merge the change tomain. - Open the
Build precompiled NIFsworkflow in the Actions tab and run it onmain. The workflow:- reads the version from
mix.exsand fails if the tagvX.Y.Zalready exists; - builds the NIF for every supported target and NIF version;
- computes
checksum-Elixir.ExEthBls.Native.exsfrom the archives and commits it tomain; - creates the GitHub release
vX.Y.Z, tagged at that checksum commit, with all archives attached; - checks out the new tag and compiles and tests it without a Rust toolchain, i.e. exactly the way consumers do.
- reads the version from
- Point consumers at the new tag.
The committed checksum file always corresponds to the latest release; a
version bump on main without a release is expected to be out of sync with
it until the workflow runs, which is why only tagged commits should be used as
a dependency.
This project is licensed under the Blockscout Software Licence. See the LICENSE file for full terms.