Usage Guide
View SourceThis guide covers the basic usage of the lib_secp256k1 library for Elixir.
Scope
lib_secp256k1 wraps the upstream libsecp256k1 C library. Use Erlang's
:crypto module for generic hashing, random bytes, generic ECDSA, and raw
ECDH. Use this library when you need libsecp256k1-specific key formats,
signatures, Schnorr, MuSig2, or the libsecp256k1 ECDH output contract.
Installation
System Dependencies
The package includes the upstream libsecp256k1 source as a vendored tarball with a pre-generated configure script. Users need make, a C compiler, and standard Unix tools tar plus either sha256sum or shasum, which are present in standard Unix userland. No network, git, autoconf, automake, or libtool is required to compile the package.
macOS
Install Xcode Command Line Tools:
xcode-select --install
Linux (Ubuntu/Debian)
sudo apt-get install build-essential
Elixir Dependency
Add lib_secp256k1 to your dependencies in mix.exs:
def deps do
[
{:lib_secp256k1, "~> 0.8"}
]
endKeypair Generation
The library allows generating secure random secret keys and deriving public keys in various formats.
Generating a Random Keypair
You can generate a new random keypair with a specified public key format (:compressed, :uncompressed, or :xonly).
# Generate a keypair with a compressed public key (33 bytes)
{seckey, pubkey} = Secp256k1.keypair(:compressed)
# Generate a keypair with an uncompressed public key (65 bytes)
{seckey, pubkey_uncompressed} = Secp256k1.keypair(:uncompressed)
# Generate a keypair with an x-only public key (32 bytes, used for Schnorr/Taproot)
{seckey, xonly_pubkey} = Secp256k1.keypair(:xonly)Deriving a Public Key
If you already have a secret key (32 bytes), you can derive the public key from it.
seckey = <<0x1234...::256>> # Your 32-byte secret key
# Derive compressed public key
pubkey = Secp256k1.pubkey(seckey, :compressed)
# Derive x-only public key
xonly_pubkey = Secp256k1.pubkey(seckey, :xonly)If you receive a compressed public key but do not own its secret key, convert it directly to the 32-byte x-only format:
xonly_pubkey = Secp256k1.convert_pubkey(compressed_pubkey, :xonly)The conversion removes the compressed key's parity prefix and does not require secret-key material.
Key Tweaks
Full-key tweaks support BIP-32-style private and public derivation. Both operations add the same scalar, so deriving a public key from the tweaked secret key produces the same point as tweaking the original public key.
# Raw arithmetic example only. Derive this scalar according to BIP-32 or BIP-341.
tweak = <<1::256>>
compressed_pubkey = Secp256k1.pubkey(seckey, :compressed)
tweaked_seckey = Secp256k1.ec_seckey_tweak_add(seckey, tweak)
tweaked_pubkey = Secp256k1.ec_pubkey_tweak_add(compressed_pubkey, tweak)
Secp256k1.pubkey(tweaked_seckey, :compressed) == tweaked_pubkey
# => trueFor Taproot, tweak the even-Y x-only internal key. Public tweaking returns the output key and its full-point parity. Secret tweaking performs the same even-Y normalization, so the resulting secret key signs for that output key.
internal_pubkey = Secp256k1.pubkey(seckey, :xonly)
{:ok, output_pubkey, parity} = Secp256k1.xonly_pubkey_tweak_add(internal_pubkey, tweak)
true =
Secp256k1.xonly_pubkey_tweak_add_check(output_pubkey, parity, internal_pubkey, tweak)
output_seckey = Secp256k1.xonly_seckey_tweak_add(seckey, tweak)
Secp256k1.pubkey(output_seckey, :xonly) == output_pubkey
# => trueAll tweaks are 32-byte big-endian scalars. Zero is valid; values at or above the
curve order and tweaks that produce an invalid key return {:error, reason}. These
functions perform key arithmetic only. Applications must derive child-key tweaks
according to BIP-32 or commitment tweaks according to BIP-341.
ECDSA Signatures
ECDSA is the traditional signature scheme used in Bitcoin and other cryptocurrencies.
Signing a Message
To sign a message, you first need to hash it (typically using SHA-256).
Hashing belongs to :crypto
Hashing is intentionally done with Erlang's built-in :crypto module. This
library does not reimplement generic hashing APIs.
# 1. Prepare the message hash
message = "Hello, World!"
msg_hash = :crypto.hash(:sha256, message)
# 2. Sign the hash with your secret key
# Returns a 64-byte compact signature
signature = Secp256k1.ecdsa_sign(msg_hash, seckey)Verifying a Signature
To verify a signature, you need the signature, the message hash, and the public key.
# Verify the signature
is_valid = Secp256k1.ecdsa_valid?(signature, msg_hash, pubkey)
# => trueDER Wire Signatures and Low-S Normalization
The signing and verification APIs use compact 64-byte r || s signatures. Convert
strict DER signatures at protocol boundaries:
der_signature = Secp256k1.ecdsa_signature_serialize_der(signature)
compact_signature = Secp256k1.ecdsa_signature_parse_der(der_signature)Bitcoin transaction signatures append a sighash byte after the DER signature. Remove that byte before parsing and append the required value after serialization.
libsecp256k1 verification rejects high-S signatures. Protocols requiring canonical low-S must keep that rejection. Normalize only when the surrounding protocol deliberately accepts the mathematically equivalent high-S form:
compact_signature = Secp256k1.ecdsa_signature_normalize(compact_signature)
is_valid = Secp256k1.ecdsa_valid?(compact_signature, msg_hash, pubkey)Normalization accepts a malleable alternate signature. Use the normalized bytes for all subsequent identity, hashing, storage, and transmission operations.
ECDSA and :crypto
Erlang's :crypto module also provides generic ECDSA through
:crypto.sign/4 and :crypto.verify/5. Use that API when you want digest
selection through the standard Erlang/OpenSSL interface.
Use Secp256k1.ecdsa_sign/2 and Secp256k1.ecdsa_valid?/3 when you want the
libsecp256k1/Bitcoin-oriented contract:
- sign an already prepared 32-byte message hash
- verify with compressed or uncompressed secp256k1 public keys
- exchange compact
r || ssignatures internally and strict DER at wire boundaries
Schnorr Signatures
Schnorr signatures (BIP-340) are simpler and more efficient than ECDSA. They use x-only public keys.
Signing a Message
Schnorr signatures can sign a 32-byte hash or an arbitrary length message.
# Signing a hash (recommended for Bitcoin)
msg_hash = :crypto.hash(:sha256, "Hello Schnorr")
signature = Secp256k1.schnorr_sign(msg_hash, seckey)Verifying a Signature
Verification requires the signature, the original message (or hash), and the x-only public key.
# Derive x-only pubkey if you haven't already
xonly_pubkey = Secp256k1.pubkey(seckey, :xonly)
# Verify
is_valid = Secp256k1.schnorr_valid?(signature, msg_hash, xonly_pubkey)
# => trueECDH Shared Secrets
Secp256k1.ecdh/2 computes libsecp256k1's default hashed ECDH shared secret.
It accepts a 32-byte secret key and a compressed or uncompressed secp256k1
public key, and returns a 32-byte binary.
{alice_seckey, _alice_pubkey} = Secp256k1.keypair(:compressed)
{_bob_seckey, bob_pubkey} = Secp256k1.keypair(:compressed)
shared_secret = Secp256k1.ecdh(alice_seckey, bob_pubkey)
# => <<_::256>>ECDH and :crypto
This is not raw generic ECDH. It wraps the upstream C library behavior, currently SHA256 over the compressed shared point. This output intentionally differs from Erlang/OpenSSL raw ECDH. If you need raw ECDH instead, call:
:crypto.compute_key(:ecdh, other_pubkey, my_private_key, :secp256k1)