Usage Guide

View Source

This 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"}
  ]
end

Keypair 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
# => true

For 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
# => true

All 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)
# => true

DER 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 || s signatures 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)
# => true

ECDH 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)