UltrafastSecp256k1 API Reference

July 7, 2026 · View on GitHub

Complete API documentation for CPU, CUDA, and WASM implementations.


Table of Contents

  1. CPU API
  2. Address Generation API
  3. Multi-Chain Coins API
  4. Unified Wallet API
  5. BIP-39 Mnemonic Seed Phrases
  6. Message Signing API
  7. Zero-Knowledge Proof API
  8. CUDA API
  9. WASM API
  10. C ABI (ufsecp)
  11. Performance Tips
  12. Examples

CPU API

Namespace: secp256k1::fast

Headers:

#include <secp256k1/field.hpp>
#include <secp256k1/scalar.hpp>
#include <secp256k1/point.hpp>

FieldElement

256-bit field element for secp256k1 curve (mod p where p = 22562^{256} - 2322^{32} - 977).

Construction

// Zero element
FieldElement a = FieldElement::zero();

// One element
FieldElement b = FieldElement::one();

// From 64-bit integer
FieldElement c = FieldElement::from_uint64(12345);

// From 4 x 64-bit limbs (little-endian, RECOMMENDED for binary I/O)
std::array<uint64_t, 4> limbs = {0x123, 0x456, 0x789, 0xABC};
FieldElement d = FieldElement::from_limbs(limbs);

// From 32 bytes (big-endian, for hex/test vectors only)
std::array<uint8_t, 32> bytes = {...};
FieldElement e = FieldElement::from_bytes(bytes);

// From hex string (developer-friendly)
FieldElement f = FieldElement::from_hex(
    "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"
);

Arithmetic Operations

FieldElement a, b;

// Basic arithmetic (immutable, returns new object)
FieldElement sum = a + b;
FieldElement diff = a - b;
FieldElement prod = a * b;
FieldElement sq = a.square();
FieldElement inv = a.inverse();

// In-place arithmetic (mutable, ~10-15% faster)
a += b;
a -= b;
a *= b;
a.square_inplace();    // a = a^2
a.inverse_inplace();   // a = a^-¹

Serialization

FieldElement a;

// To bytes (big-endian)
std::array<uint8_t, 32> bytes = a.to_bytes();

// To bytes into existing buffer (no allocation)
uint8_t buffer[32];
a.to_bytes_into(buffer);

// To hex string
std::string hex = a.to_hex();

// Access raw limbs (little-endian)
const auto& limbs = a.limbs();  // std::array<uint64_t, 4>

Comparison

FieldElement a, b;
if (a == b) { ... }
if (a != b) { ... }

Scalar

256-bit scalar for secp256k1 curve (mod n where n is the group order).

Construction

// Zero
Scalar a = Scalar::zero();

// One
Scalar b = Scalar::one();

// From 64-bit integer
Scalar c = Scalar::from_uint64(12345);

// From limbs (little-endian)
std::array<uint64_t, 4> limbs = {...};
Scalar d = Scalar::from_limbs(limbs);

// From bytes (big-endian)
std::array<uint8_t, 32> bytes = {...};
Scalar e = Scalar::from_bytes(bytes);

// From hex string
Scalar f = Scalar::from_hex(
    "E9873D79C6D87DC0FB6A5778633389F4453213303DA61F20BD67FC233AA33262"
);

Arithmetic Operations

Scalar a, b;

// Basic arithmetic
Scalar sum = a + b;
Scalar diff = a - b;
Scalar prod = a * b;

// In-place
a += b;
a -= b;
a *= b;

Utility Methods

Scalar s;

// Check if zero
bool isZero = s.is_zero();

// Get specific bit
uint8_t bit = s.bit(index);  // 0 or 1

// NAF encoding (Non-Adjacent Form)
std::vector<int8_t> naf = s.to_naf();

// wNAF encoding (width-w NAF)
std::vector<int8_t> wnaf = s.to_wnaf(4);  // width = 4

Point

Elliptic curve point on secp256k1 (internally Jacobian coordinates).

Construction

// Generator point G
Point G = Point::generator();

// Point at infinity (identity)
Point inf = Point::infinity();

// From affine coordinates
FieldElement x = FieldElement::from_hex("...");
FieldElement y = FieldElement::from_hex("...");
Point p = Point::from_affine(x, y);

// From hex strings (developer-friendly)
Point p2 = Point::from_hex(
    "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798",
    "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"
);

Point Operations

Point p, q;
Scalar k;

// Addition and doubling
Point sum = p.add(q);       // p + q
Point doubled = p.dbl();    // 2p

// Scalar multiplication
Point result = p.scalar_mul(k);  // k * p

// Negation
Point neg = p.negate();  // -p

Optimized Scalar Multiplication

// For fixed K x variable Q pattern (same K, different Q points):
Scalar K = Scalar::from_hex("...");
KPlan plan = KPlan::from_scalar(K);  // Precompute once

// Then for each Q:
Point Q1 = Point::from_hex("...", "...");
Point Q2 = Point::from_hex("...", "...");

Point R1 = Q1.scalar_mul_with_plan(plan);  // Fastest!
Point R2 = Q2.scalar_mul_with_plan(plan);

In-Place Operations (Fastest)

Point p;

// Increment/decrement by generator
p.next_inplace();    // p += G
p.prev_inplace();    // p -= G

// In-place arithmetic
p.add_inplace(q);    // p += q
p.sub_inplace(q);    // p -= q
p.dbl_inplace();     // p = 2p
p.negate_inplace();  // p = -p

// Mixed addition (when q is affine, z=1)
FieldElement qx, qy;
p.add_mixed_inplace(qx, qy);  // Branchless, ~12% faster

Serialization

Point p;

// Get affine coordinates
FieldElement x = p.x();
FieldElement y = p.y();

// Compressed format (33 bytes: 0x02/0x03 + x)
std::array<uint8_t, 33> compressed = p.to_compressed();

// Uncompressed format (65 bytes: 0x04 + x + y)
std::array<uint8_t, 65> uncompressed = p.to_uncompressed();

// Split x-coordinate for database lookups
std::array<uint8_t, 16> first_half = p.x_first_half();
std::array<uint8_t, 16> second_half = p.x_second_half();

Properties

Point p;

// Check if point at infinity
bool isInf = p.is_infinity();

// Direct Jacobian coordinate access
const FieldElement& X = p.X();  // Jacobian X
const FieldElement& Y = p.Y();  // Jacobian Y
const FieldElement& Z = p.z();  // Jacobian Z

Utility Functions

Self-Test

#include <secp256k1/point.hpp>

// Run correctness tests
bool passed = secp256k1::fast::Selftest(true);  // verbose=true
if (!passed) {
    std::cerr << "Self-test failed!" << std::endl;
}

ECDSA (RFC 6979)

Namespace: secp256k1

Header:

#include <secp256k1/ecdsa.hpp>

ECDSASignature

struct ECDSASignature {
    fast::Scalar r;
    fast::Scalar s;

    // DER encoding (variable length, max 72 bytes)
    std::pair<std::array<uint8_t, 72>, std::size_t> to_der() const;

    // Compact 64-byte encoding: r (32 bytes) || s (32 bytes)
    std::array<uint8_t, 64> to_compact() const;

    // Decode from compact
    static ECDSASignature from_compact(const std::array<uint8_t, 64>& data);

    // Normalize to low-S (BIP-62)
    ECDSASignature normalize() const;

    // Check low-S
    bool is_low_s() const;
};

Signing

// Sign a 32-byte message hash with RFC 6979 deterministic nonce.
// Returns normalized (low-S) signature. Returns {0,0} on failure.
ECDSASignature ecdsa_sign(
    const std::array<uint8_t, 32>& msg_hash,
    const fast::Scalar& private_key
);

Verification

// Verify ECDSA signature. Accepts both low-S and high-S.
bool ecdsa_verify(
    const std::array<uint8_t, 32>& msg_hash,
    const fast::Point& public_key,
    const ECDSASignature& sig
);

RFC 6979 Nonce

// Deterministic nonce generation per RFC 6979.
fast::Scalar rfc6979_nonce(
    const fast::Scalar& private_key,
    const std::array<uint8_t, 32>& msg_hash
);

Example

#include <secp256k1/ecdsa.hpp>
#include <secp256k1/sha256.hpp>
#include <secp256k1/point.hpp>

using namespace secp256k1;

auto msg_hash = SHA256::hash("Hello ECDSA", 11);
fast::Scalar sk = fast::Scalar::from_hex("...");
fast::Point pk = fast::Point::generator().scalar_mul(sk);

// Sign
auto sig = ecdsa_sign(msg_hash, sk);

// Verify
bool ok = ecdsa_verify(msg_hash, pk, sig);

// Compact encoding (64 bytes)
auto compact = sig.to_compact();
auto recovered = ECDSASignature::from_compact(compact);

Schnorr (BIP-340)

Namespace: secp256k1

Header:

#include <secp256k1/schnorr.hpp>

SchnorrSignature

struct SchnorrSignature {
    std::array<uint8_t, 32> r;  // R.x (nonce point x-coordinate)
    fast::Scalar s;              // scalar s

    // 64-byte encoding: r (32) || s (32)
    std::array<uint8_t, 64> to_bytes() const;
    static SchnorrSignature from_bytes(const std::array<uint8_t, 64>& data);
};

Signing

// BIP-340 Schnorr sign.
// aux_rand: 32 bytes of auxiliary randomness (use zeros for deterministic).
SchnorrSignature schnorr_sign(
    const fast::Scalar& private_key,
    const std::array<uint8_t, 32>& msg,
    const std::array<uint8_t, 32>& aux_rand
);

Verification

// BIP-340 Schnorr verify with x-only public key.
bool schnorr_verify(
    const std::array<uint8_t, 32>& pubkey_x,
    const std::array<uint8_t, 32>& msg,
    const SchnorrSignature& sig
);

Utilities

// X-only public key (BIP-340: negate if Y is odd)
std::array<uint8_t, 32> schnorr_pubkey(const fast::Scalar& private_key);

// Tagged hash: H_tag(msg) = SHA256(SHA256(tag) || SHA256(tag) || msg)
std::array<uint8_t, 32> tagged_hash(
    const char* tag, const void* data, std::size_t len
);

Example

#include <secp256k1/schnorr.hpp>

using namespace secp256k1;

fast::Scalar sk = fast::Scalar::from_hex("...");
auto pk_x = schnorr_pubkey(sk);

std::array<uint8_t, 32> msg = { /* message hash */ };
std::array<uint8_t, 32> aux = {}; // zeros for deterministic

auto sig = schnorr_sign(sk, msg, aux);
bool ok = schnorr_verify(pk_x, msg, sig);

Batch Verification

Namespace: secp256k1

Header:

#include <secp256k1/batch_verify.hpp>

Canonical libbitcoin C++ surface (ufsecp::lbtc::*)

The canonical bridge-free libbitcoin integration is the header-only target secp256k1::fastsecp256k1_libbitcoin. Build with -DSECP256K1_BUILD_LIBBITCOIN=ON (optionally -DSECP256K1_BUILD_LIBBITCOIN_TESTS=ON).

Package target: secp256k1::fastsecp256k1_libbitcoin (INTERFACE, carries C++20 + HAVE_ULTRAFAST + engine).

Header: <ufsecp/libbitcoin.hpp> — namespace ufsecp::lbtc::*, all stateless inline functions.

Byte layouts (match libbitcoin/libsecp256k1 exactly):

TypeFormat
pubkey33-byte compressed (0x02/0x03 || X big-endian)
hash32-byte message hash
ecdsa sig64-byte secp256k1_ecdsa_signature opaque (r limbs LE || s limbs LE)
schnorr sig64-byte BIP-340 (R.x big-endian || s big-endian)
xonly32-byte x-only pubkey

CT guarantees: Secret-bearing operations (sign, key create, seckey tweak) use secp256k1::ct::* primitives — constant-time, no data-dependent branches. Verify/recover/combine operations are variable-time (all inputs public). See docs/LIBBITCOIN_INTEGRATION.md for the full CT/VT matrix.

Fail-closed semantics: On any failure (invalid input, parsing error, signing error) every output buffer is zeroed and false is returned. Batch APIs write 1/0 per row to out_results; all-valid batches fill out_results with 1. count == 0 returns true. Invalid/nullptr row pointers return false.

Threading: max_threads == 0 = auto, max_threads == 1 = serial bounded chunks, N = cap.

Batch APIs:

  • ecdsa_verify_batch / ecdsa_verify_columns — rows [hash32|pub33|sig64] @ stride, or parallel spans
  • schnorr_verify_batch / schnorr_verify_columns — rows [msg32|xonly32|sig64] @ stride, or parallel spans

Public-data batch ops (validate / commitment / hashing): all [[nodiscard]] inline bool in ufsecp::lbtc, all variable-time / public-data (no secret is ever touched). Each is ONE surface — internal GPU acceleration via the EXISTING GpuBackend virtuals + deterministic CPU fallback, no CPU/GPU split, no GPU status code, no caller chunking, no C ABI.

// Validate N x-only pubkeys. out[i]=1 iff valid BIP-340 x-coordinate (x<p, even-y lift_x).
bool xonly_validate_batch(const uint8_t* keys32, size_t count,
                          uint8_t* out_results, size_t max_threads = 0) noexcept;

// Validate N compressed pubkeys. out[i]=1 iff prefix∈{0x02,0x03}, x<p, on curve.
bool pubkey_validate_batch(const uint8_t* pubkeys33, size_t count,
                           uint8_t* out_results, size_t max_threads = 0) noexcept;

// Verify N RAW taproot tweak commitments (tweak = precomputed scalar, NOT H_TapTweak).
// out[i]=1 iff x(lift_x_even(internal_i) + tweak_i*G)==tweaked_x_i AND y-parity==parity[i].
bool taproot_commitment_verify_batch(const uint8_t* internal_x32, const uint8_t* tweak32,
                                     const uint8_t* tweaked_x32, const uint8_t* parity,
                                     size_t count, uint8_t* out_results,
                                     size_t max_threads = 0) noexcept;

// BIP-340 tagged hash over fixed-length msgs; tag_hash32 = SHA256(tag) shared across rows.
// out32[i] = SHA256(tag_hash32 || tag_hash32 || msgs[i]).
bool tagged_hash_batch(const uint8_t* tag_hash32, const uint8_t* msgs,
                       size_t msg_len, size_t count, uint8_t* out32,
                       size_t max_threads = 0) noexcept;
bool tagged_hash_batch(const char* tag, size_t tag_len, const uint8_t* msgs,
                       size_t msg_len, size_t count, uint8_t* out32,
                       size_t max_threads = 0) noexcept;  // computes tag_hash32 once

// BIP-340 tagged hash over per-item variable-length msgs (TapLeaf scripts).
// CPU covers ALL lengths (no device-side 256-byte cap).
bool tagged_hash_var_batch(const uint8_t* tag_hash32, const uint8_t* msgs,
                           const uint32_t* msg_lens, size_t stride, size_t count,
                           uint8_t* out32, size_t max_threads = 0) noexcept;

// Bitcoin HASH256 (double SHA-256) over fixed-length inputs.
// out32[i] = SHA256(SHA256(inputs[i])).
bool hash256_batch(const uint8_t* inputs, size_t input_len, size_t count,
                   uint8_t* out32, size_t max_threads = 0) noexcept;

// Bitcoin HASH256 (double SHA-256) over per-item variable-length inputs
// (e.g. CPU-pre-serialized transaction bytes for a future txid/wtxid batch
// wrapper). No tag prefix, no transaction parsing on GPU — the caller
// serializes on the CPU; row i = inputs[i*stride .. i*stride+input_lens[i]).
// out32[i] = SHA256(SHA256(row_i)). Every input_lens[i] is validated against
// stride per-row, host-side, before GPU dispatch.
bool hash256_var_batch(const uint8_t* inputs, const uint32_t* input_lens,
                       size_t stride, size_t count, uint8_t* out32,
                       size_t max_threads = 0) noexcept;

hash256_var_batch's max_threads parameter is accepted for signature parity with the other batch ops but is currently unused by the CPU fallback (single-pass per-row hashing; the GPU hook, when installed, does its own internal parallelism) — reserved for a future threaded CPU fallback.

Fail-closed contract — validate ops: count==0true (out untouched); null ptr or count×elem overflow → zero out_results (if non-null) and false; operational GPU failure declines → CPU overwrites every row (never all-zero). Hash ops never pre-zero out32 (zero = wrong/consensus-invalid hash): count==0true; null ptr, msg_len/input_len==0, stride < msg_lens[i], or overflow → false without touching out32; operational GPU failure declines → CPU writes the correct hash for every row. hash256_var_batch additionally rejects any individual input_lens[i]==0 or input_lens[i]>stride (per-row bounds check, not just a scalar stride check) before touching out32 or dispatching to GPU.

Consumer CMake example:

find_package(secp256k1-fast REQUIRED COMPONENTS CPU LIBBITCOIN)
target_link_libraries(myapp PRIVATE secp256k1::fastsecp256k1_libbitcoin)

Direct byte-span APIs

These APIs are for C++ consumers that already store libbitcoin/libsecp-compatible rows or column arrays. The engine parses and verifies in bounded chunks, including when max_threads == 1, so callers do not need to marshal full tables into ECDSABatchEntry or SchnorrBatchEntry vectors.

// ECDSA rows: [hash32 | compressed_pubkey33 | opaque_sig64]
bool ecdsa_batch_verify_opaque_rows(const uint8_t* rows,
                                    size_t stride,
                                    size_t count,
                                    uint8_t* out_results = nullptr,
                                    size_t max_threads = 0);

// ECDSA columns: digests[count][32], pubkeys[count][33], sigs[count][64]
bool ecdsa_batch_verify_opaque_columns(const uint8_t* digests32,
                                       const uint8_t* pubkeys33,
                                       const uint8_t* sigs64,
                                       size_t count,
                                       uint8_t* out_results = nullptr,
                                       size_t max_threads = 0);

// Schnorr rows: [msg32 | xonly_pubkey32 | bip340_sig64]
bool schnorr_batch_verify_bip340_rows(const uint8_t* rows,
                                      size_t stride,
                                      size_t count,
                                      uint8_t* out_results = nullptr,
                                      size_t max_threads = 0);

// Schnorr columns: digests[count][32], xonly[count][32], sigs[count][64]
bool schnorr_batch_verify_bip340_columns(const uint8_t* digests32,
                                         const uint8_t* xonly32,
                                         const uint8_t* sigs64,
                                         size_t count,
                                         uint8_t* out_results = nullptr,
                                         size_t max_threads = 0);

max_threads == 0 means auto, max_threads == 1 means serial bounded chunks. count == 0 returns true. If out_results is non-null, all-valid batches fill it with 1; mixed, invalid, or unparsable batches write per-row 1/0 after fallback. Invalid pointers or undersized strides return false and zero out_results when provided.


SHA-256

Namespace: secp256k1

Header:

#include <secp256k1/sha256.hpp>

One-shot Hashing

// SHA-256
SHA256::digest_type SHA256::hash(const void* data, std::size_t len);

// Double-SHA256: SHA256(SHA256(data))
SHA256::digest_type SHA256::hash256(const void* data, std::size_t len);

Streaming API

secp256k1::SHA256 ctx;
ctx.update("part1", 5);
ctx.update("part2", 5);
auto digest = ctx.finalize();

// Reuse
ctx.reset();
ctx.update("new data", 8);
auto digest2 = ctx.finalize();

Example

#include <secp256k1/sha256.hpp>

auto hash = secp256k1::SHA256::hash("Hello, world!", 13);
// hash is std::array<uint8_t, 32>

// Double-SHA256 (Bitcoin's hash)
auto hash256 = secp256k1::SHA256::hash256("tx_data", 7);

Constant-Time Layer

Namespace: secp256k1::fast::ct

Headers:

#include <secp256k1/ct/field.hpp>
#include <secp256k1/ct/scalar.hpp>
#include <secp256k1/ct/point.hpp>
#include <secp256k1/ct/ops.hpp>

The CT layer provides side-channel resistant variants of critical operations:

namespace secp256k1::fast::ct {

// Constant-time field operations
void field_mul(const FieldElement& a, const FieldElement& b, FieldElement& out);
void field_inv(const FieldElement& a, FieldElement& out);

// Constant-time scalar multiplication (branchless double-and-add)
void scalar_mul(const Point& base, const Scalar& k, Point& out);

// Complete addition formula (handles all edge cases without branching)
void point_add_complete(const Point& p, const Point& q, Point& out);

// Constant-time point doubling
void point_dbl(const Point& p, Point& out);

} // namespace secp256k1::fast::ct

[!] CT operations are ~5-7x slower than the fast variants. Use only for private key operations (signing, ECDH).


Address Generation API

Namespace: secp256k1

Header:

#include <secp256k1/address.hpp>

Address Types

All address functions take a Network enum (Mainnet or Testnet) and return a std::string.

P2PKH (Legacy, Base58Check)

// "1..." (mainnet) or "m/n..." (testnet)
std::string address_p2pkh(const fast::Point& pubkey,
                          Network net = Network::Mainnet);

P2WPKH (Native SegWit v0, Bech32)

// "bc1q..." (mainnet) or "tb1q..." (testnet)
std::string address_p2wpkh(const fast::Point& pubkey,
                           Network net = Network::Mainnet);

P2TR (Taproot, SegWit v1, Bech32m)

// "bc1p..." (mainnet) or "tb1p..." (testnet)
std::string address_p2tr(const fast::Point& internal_key,
                         Network net = Network::Mainnet);

// From raw 32-byte x-only output key
std::string address_p2tr_raw(const std::array<uint8_t, 32>& output_key_x,
                             Network net = Network::Mainnet);

P2SH-P2WPKH (Nested/Wrapped SegWit)

// "3..." (mainnet) -- wraps P2WPKH inside P2SH for backward compatibility (BIP-49)
std::string address_p2sh_p2wpkh(const fast::Point& pubkey,
                                Network net = Network::Mainnet);

P2SH (Pay-to-Script-Hash)

// "3..." (mainnet) -- from raw 20-byte script hash
std::string address_p2sh(const std::array<uint8_t, 20>& script_hash,
                         Network net = Network::Mainnet);

P2WSH (Witness Script Hash)

// "bc1q..." 32-byte program (SegWit v0)
std::string address_p2wsh(const std::array<uint8_t, 32>& witness_script_hash,
                          Network net = Network::Mainnet);

CashAddr (Bitcoin Cash BIP-0185)

// Encode hash160 as CashAddr (type: 0=P2PKH, 1=P2SH)
std::string cashaddr_encode(const std::array<uint8_t, 20>& hash,
                            const std::string& prefix,
                            uint8_t type = 0);

// CashAddr P2PKH from public key: "bitcoincash:q..."
std::string address_cashaddr(const fast::Point& pubkey,
                             const std::string& prefix = "bitcoincash");

Encoding Functions

Base58Check

std::string base58check_encode(const uint8_t* data, size_t len);
std::pair<std::vector<uint8_t>, bool> base58check_decode(const std::string& encoded);

Bech32 / Bech32m (BIP-173 / BIP-350)

std::string bech32_encode(const std::string& hrp,
                          uint8_t witness_version,
                          const uint8_t* witness_program,
                          size_t prog_len);

struct Bech32DecodeResult {
    std::string hrp;
    int witness_version;          // -1 if invalid
    std::vector<uint8_t> witness_program;
    bool valid;
};
Bech32DecodeResult bech32_decode(const std::string& addr);

HASH160

// RIPEMD160(SHA256(data))
std::array<uint8_t, 20> hash160(const uint8_t* data, size_t len);

WIF (Wallet Import Format)

std::string wif_encode(const fast::Scalar& private_key,
                       bool compressed = true,
                       Network net = Network::Mainnet);

struct WIFDecodeResult {
    fast::Scalar key;
    bool compressed;
    Network network;
    bool valid;
};
WIFDecodeResult wif_decode(const std::string& wif);

BIP-352 Silent Payments

struct SilentPaymentAddress {
    fast::Point scan_pubkey;     // B_scan
    fast::Point spend_pubkey;    // B_spend
    std::string encode(Network net = Network::Mainnet) const;  // sp1q... or tsp1q...
};

// Generate silent payment address from scan/spend private keys
SilentPaymentAddress silent_payment_address(const fast::Scalar& scan_privkey,
                                            const fast::Scalar& spend_privkey);

// Sender: compute unique output key for recipient
std::pair<fast::Point, fast::Scalar>
silent_payment_create_output(const std::vector<fast::Scalar>& input_privkeys,
                             const SilentPaymentAddress& recipient,
                             uint32_t k = 0);

// Receiver: scan transaction to detect addressed outputs (CT reference)
std::vector<std::pair<uint32_t, fast::Scalar>>
silent_payment_scan(const fast::Scalar& scan_privkey,
                    const fast::Scalar& spend_privkey,
                    const std::vector<fast::Point>& input_pubkeys,
                    const std::vector<std::array<uint8_t, 32>>& output_pubkeys);

// ── High-performance batch scanner ──────────────────────────────────────────

// Pre-aggregated transaction (A_eff already computed)
struct ScanTx {
    fast::Point a_eff;                                   // scan_sk × this = shared secret
    std::vector<std::array<uint8_t, 32>> outputs;        // x-only output pubkeys
};

// Raw transaction (input pubkeys not yet aggregated)
struct ScanTxRaw {
    std::vector<fast::Point> input_pubkeys;              // taproot-eligible inputs
    std::array<uint8_t, 36> smallest_outpoint;           // for input_hash (BIP-352 §A)
    std::vector<std::array<uint8_t, 32>> outputs;
};

// Compute A_eff = input_hash × Σ input_pubkeys
// Uses multi_scalar_mul (Pippenger) for m > 1 inputs.
ScanTx compute_a_eff(const ScanTxRaw& raw);

// Batch scan: one KPlan recode, one GLV decompose, lockstep wNAF loop for all txs.
// Stage 1: batch_scalar_mul_fixed_k (fixed scan_sk, N variable a_eff points).
// Stage 2: hash × t×G comparison for each candidate output.
// Expected throughput: ~206K tx/s (16-core) — Stage 1 bottleneck ~9.1 µs/tx/core.
std::vector<std::pair<uint32_t, fast::Scalar>>
fast_scan_batch(const fast::Scalar& scan_privkey,
                const fast::Scalar& spend_privkey,
                const std::vector<ScanTx>& txs);

// ── batch_scalar_mul_generator (fixed-base, N scalars) ──────────────────────
// Computes results[i] = scalars[i] × G for all i in [0, n).
// One mutex lock for all N multiplications; thread-local digit scratch.
// Non-GLV path when config.enable_glv=false (default): fill_window_digits_into + accumulate.
// GLV path when enable_glv=true: split_scalar_internal + shamir_windowed_glv.
// ESP32: falls back to per-point scalar_mul_generator. n=1 fast-path skips batch setup.
void batch_scalar_mul_generator(const Scalar* scalars, Point* results, std::size_t n);

// ── KPlan + batch_scalar_mul_fixed_k (low-level) ────────────────────────────

// Precomputed GLV wNAF schedule for a fixed scalar.
// Computed once; reused for all N points in a batch.
struct KPlan { /* opaque */ };

// static method on fast::Point:
//   static void batch_scalar_mul_fixed_k(const KPlan& plan,
//                                        const Point* pts, size_t n,
//                                        Point* results);
// One KPlan recode, lockstep wNAF loop, 1 field_inv per 2048-pt chunk.
// For N=1 falls back to scalar_mul_with_plan; non-FE52 builds use per-point fallback.

Multi-Chain Coins API

Namespace: secp256k1::coins

Headers:

#include <secp256k1/coins/coin_params.hpp>
#include <secp256k1/coins/coin_address.hpp>

Coin Parameters

28 coins predefined as constexpr CoinParams objects:

CoinTickerEncodingBIP-44Chain ID
BitcoinBTCBech320--
LitecoinLTCBech322--
DogecoinDOGEBase58Check3--
DashDASHBase58Check5--
EthereumETHEIP-55601
Bitcoin CashBCHCashAddr145--
Bitcoin SVBSVBase58Check236--
ZcashZECBase58Check133--
DigiByteDGBBech3220--
NamecoinNMCBase58Check7--
PeercoinPPCBase58Check6--
VertcoinVTCBech3228--
ViacoinVIABech3214--
GroestlcoinGRSBech3217--
SyscoinSYSBech3257--
BNB Smart ChainBNBEIP-556056
PolygonPOLEIP-5560137
AvalancheAVAXEIP-556043114
FantomFTMEIP-5560250
ArbitrumARBEIP-556042161
OptimismOPEIP-556010
RavencoinRVNBase58Check175--
FluxFLUXBase58Check19167--
QtumQTUMBase58Check2301--
HorizenZENBase58Check121--
Bitcoin GoldBTGBase58Check156--
KomodoKMDBase58Check141--
TronTRXTRON_BASE58195--

Lookup Functions

const CoinParams* find_by_coin_type(uint32_t coin_type);  // Returns nullptr if not found
const CoinParams* find_by_ticker(const char* ticker);      // Case-sensitive

Iteration

inline constexpr const CoinParams* ALL_COINS[];            // Array of all 28 coins
inline constexpr size_t ALL_COINS_COUNT = 28;

Coin Address Functions

// Default/preferred format for each coin (Bech32 for BTC, EIP-55 for ETH, etc.)
std::string coin_address(const fast::Point& pubkey, const CoinParams& coin,
                         bool testnet = false);

// Explicit format selection
std::string coin_address_p2pkh(const fast::Point& pubkey, const CoinParams& coin,
                               bool testnet = false);
std::string coin_address_p2wpkh(const fast::Point& pubkey, const CoinParams& coin,
                                bool testnet = false);
std::string coin_address_p2tr(const fast::Point& internal_key, const CoinParams& coin,
                              bool testnet = false);
std::string coin_address_p2sh_p2wpkh(const fast::Point& pubkey, const CoinParams& coin,
                                     bool testnet = false);
std::string coin_address_p2sh(const std::array<uint8_t, 20>& script_hash,
                              const CoinParams& coin, bool testnet = false);
std::string coin_address_cashaddr(const fast::Point& pubkey, const CoinParams& coin,
                                  bool testnet = false);

// WIF encoding with coin-specific prefix
std::string coin_wif_encode(const fast::Scalar& private_key, const CoinParams& coin,
                            bool compressed = true, bool testnet = false);

Functions return empty string if the coin does not support the requested format (e.g., coin_address_p2wpkh for Dogecoin).

Coin Key Generation

struct CoinKeyPair {
    fast::Scalar private_key;
    fast::Point  public_key;
    std::string  address;         // Default format for coin
    std::string  wif;             // WIF-encoded key (empty for EVM coins)
};

CoinKeyPair coin_derive(const fast::Scalar& private_key, const CoinParams& coin,
                        bool testnet = false, const CurveContext* ctx = nullptr);

Unified Wallet API

Namespace: secp256k1::coins::wallet

Header:

#include <secp256k1/coins/wallet.hpp>

Chain-agnostic facade over all address, signing, and recovery operations. Same API regardless of underlying chain.

Wallet Key Management

struct WalletKey {
    fast::Scalar priv;           // 32-byte private scalar
    fast::Point  pub;            // Public key
};

// Create from raw 32-byte key. Returns (key, success).
std::pair<WalletKey, bool> from_private_key(const uint8_t* priv32);

// Export in chain-appropriate format:
//   Bitcoin-family: WIF (Base58Check)
//   EVM-family:     0x-prefixed hex
//   Tron:           raw hex
std::string export_private_key(const CoinParams& coin, const WalletKey& key,
                               bool testnet = false);

std::string export_public_key_hex(const CoinParams& coin, const WalletKey& key);

Wallet Address Generation

// Default address format for coin
std::string get_address(const CoinParams& coin, const WalletKey& key,
                        bool testnet = false);

// Explicit format selection
std::string get_address_p2pkh(const CoinParams& coin, const WalletKey& key,
                              bool testnet = false);
std::string get_address_p2wpkh(const CoinParams& coin, const WalletKey& key,
                               bool testnet = false);
std::string get_address_p2sh_p2wpkh(const CoinParams& coin, const WalletKey& key,
                                    bool testnet = false);
std::string get_address_p2tr(const CoinParams& coin, const WalletKey& key,
                             bool testnet = false);
std::string get_address_cashaddr(const CoinParams& coin, const WalletKey& key,
                                 bool testnet = false);

Wallet Signing

struct MessageSignature {
    std::array<uint8_t, 32> r;
    std::array<uint8_t, 32> s;
    int recid;                   // Recovery ID (0-3)
    uint64_t v;                  // EIP-155 v (EVM) or 27+recid (Bitcoin)
    std::array<uint8_t, 65> to_rsv() const;  // [r:32][s:32][v:1]
};

// Chain-aware message signing:
//   Bitcoin: "\x18Bitcoin Signed Message:\n" prefix -> dSHA256
//   Ethereum: "\x19Ethereum Signed Message:\n" prefix -> Keccak-256
MessageSignature sign_message(const CoinParams& coin, const WalletKey& key,
                              const uint8_t* msg, size_t msg_len);

// Sign raw 32-byte hash (no prefix, no hashing)
MessageSignature sign_hash(const CoinParams& coin, const WalletKey& key,
                           const uint8_t* hash32);

// Verify signed message against public key
bool verify_message(const CoinParams& coin, const fast::Point& pubkey,
                    const uint8_t* msg, size_t msg_len,
                    const MessageSignature& sig);

Wallet Recovery

// Recover public key from message + signature
std::pair<fast::Point, bool>
recover_signer(const CoinParams& coin,
               const uint8_t* msg, size_t msg_len,
               const MessageSignature& sig);

// Recover address string from message + signature
std::pair<std::string, bool>
recover_address(const CoinParams& coin,
                const uint8_t* msg, size_t msg_len,
                const MessageSignature& sig);

BIP-39 Mnemonic Seed Phrases

Namespace: secp256k1

Header:

#include <secp256k1/bip39.hpp>

BIP-39 mnemonic code for generating deterministic keys. Converts entropy to human-readable word sequences and derives 512-bit seeds compatible with BIP-32.

Mnemonic Generation

// Generate mnemonic from OS CSPRNG entropy (12 words = 16 bytes)
auto [mnemonic, ok] = secp256k1::bip39_generate(16);

// Generate from explicit entropy (24 words = 32 bytes)
uint8_t entropy[32] = { /* ... */ };
auto [mnemonic24, ok2] = secp256k1::bip39_generate(32, entropy);
std::pair<std::string, bool>
bip39_generate(std::size_t entropy_bytes,
               const std::uint8_t* entropy_in = nullptr);

Parameters:

ParameterDescription
entropy_bytes16 (12 words), 20 (15), 24 (18), 28 (21), or 32 (24 words)
entropy_inOptional explicit entropy; nullptr = use OS CSPRNG

Returns: {mnemonic_string, success}

Mnemonic Validation

bool valid = secp256k1::bip39_validate("abandon abandon ... about");
bool bip39_validate(const std::string& mnemonic);

Validates word count (12/15/18/21/24), word membership in BIP-39 English wordlist, and SHA-256 checksum.

Seed Derivation

auto [seed, ok] = secp256k1::bip39_mnemonic_to_seed(mnemonic, "my passphrase");
// seed is std::array<uint8_t, 64> -- pass to bip32_master_key()
std::pair<std::array<std::uint8_t, 64>, bool>
bip39_mnemonic_to_seed(const std::string& mnemonic,
                       const std::string& passphrase = "");

Uses PBKDF2-HMAC-SHA512 with 2048 iterations. Salt = "mnemonic" + passphrase.

Mnemonic to Entropy

auto [ent, ok] = secp256k1::bip39_mnemonic_to_entropy("abandon abandon ... about");
// ent.data = raw entropy bytes, ent.length = byte count (16-32)
struct Bip39Entropy {
    std::array<std::uint8_t, 32> data{};
    std::size_t length = 0;
};

std::pair<Bip39Entropy, bool>
bip39_mnemonic_to_entropy(const std::string& mnemonic);

PBKDF2-HMAC-SHA512

void pbkdf2_hmac_sha512(const std::uint8_t* password, std::size_t password_len,
                         const std::uint8_t* salt, std::size_t salt_len,
                         std::uint32_t iterations,
                         std::uint8_t* output, std::size_t output_len);

Exposed for direct use and testing. Used internally by bip39_mnemonic_to_seed().

Wordlist Access

const char* const* words = secp256k1::bip39_wordlist_english();
// words[0] == "abandon", words[2047] == "zoo"

Returns pointer to the 2048-word sorted English BIP-39 wordlist.


Message Signing API

Namespace: secp256k1::coins

Header:

#include <secp256k1/coins/message_signing.hpp>

Low-level Bitcoin message signing (BIP-137 / Electrum format).

Bitcoin Message Hash

// SHA256(SHA256("\x18Bitcoin Signed Message:\n" + varint(msg_len) + msg))
std::array<uint8_t, 32> bitcoin_message_hash(const uint8_t* msg, size_t msg_len);

Sign / Verify / Recover

RecoverableSignature bitcoin_sign_message(const uint8_t* msg, size_t msg_len,
                                          const fast::Scalar& private_key);

bool bitcoin_verify_message(const uint8_t* msg, size_t msg_len,
                            const fast::Point& pubkey,
                            const ECDSASignature& sig);

std::pair<fast::Point, bool>
bitcoin_recover_message(const uint8_t* msg, size_t msg_len,
                        const ECDSASignature& sig, int recid);

Base64 Encoding

// 65-byte format: 1-byte header (recid + compression flag, range 27-34) + r + s
std::string bitcoin_sig_to_base64(const RecoverableSignature& rsig,
                                  bool compressed = true);

struct BitcoinSigDecodeResult {
    ECDSASignature sig;
    int recid;
    bool compressed;
    bool valid;
};
BitcoinSigDecodeResult bitcoin_sig_from_base64(const std::string& base64);

Zero-Knowledge Proof API

Namespace: secp256k1::zk

Header:

#include <secp256k1/zk.hpp>

Knowledge Proof (Schnorr Sigma)

Non-interactive proof of knowledge of discrete log via Fiat-Shamir transform.

KnowledgeProof

struct KnowledgeProof {
    std::array<uint8_t, 32> rx;  // R.x (nonce point x-coordinate)
    fast::Scalar s;               // response scalar

    std::array<uint8_t, 64> serialize() const;
    static bool deserialize(const uint8_t* data64, KnowledgeProof& out);
};

knowledge_prove

// Prove: "I know x such that pubkey = x*G"
// Uses CT layer (constant-time). Nonce hedged with aux_rand.
KnowledgeProof knowledge_prove(
    const fast::Scalar& secret,
    const fast::Point& pubkey,
    const std::array<uint8_t, 32>& msg,
    const std::array<uint8_t, 32>& aux_rand);

knowledge_verify

// Verify: s*G == R + e*P (FAST path)
bool knowledge_verify(
    const KnowledgeProof& proof,
    const fast::Point& pubkey,
    const std::array<uint8_t, 32>& msg);

knowledge_prove_base / knowledge_verify_base

Same as above but with an arbitrary base point (not just G):

KnowledgeProof knowledge_prove_base(
    const fast::Scalar& secret,
    const fast::Point& point,     // point = secret * base
    const fast::Point& base,
    const std::array<uint8_t, 32>& msg,
    const std::array<uint8_t, 32>& aux_rand);

bool knowledge_verify_base(
    const KnowledgeProof& proof,
    const fast::Point& point,
    const fast::Point& base,
    const std::array<uint8_t, 32>& msg);

DLEQ Proof (Discrete Log Equality)

Proves log_G(P) == log_H(Q) without revealing the secret.

DLEQProof

struct DLEQProof {
    fast::Scalar e;  // challenge
    fast::Scalar s;  // response

    std::array<uint8_t, 64> serialize() const;
    static bool deserialize(const uint8_t* data64, DLEQProof& out);
};

dleq_prove

// Prove: P = secret*G AND Q = secret*H (same secret)
// Uses CT layer. Nonce hedged with aux_rand.
DLEQProof dleq_prove(
    const fast::Scalar& secret,
    const fast::Point& G,
    const fast::Point& H,
    const fast::Point& P,
    const fast::Point& Q,
    const std::array<uint8_t, 32>& aux_rand);

dleq_verify

// Verify: s*G == R1 + e*P AND s*H == R2 + e*Q (FAST path)
bool dleq_verify(
    const DLEQProof& proof,
    const fast::Point& G,
    const fast::Point& H,
    const fast::Point& P,
    const fast::Point& Q);

Bulletproof Range Proof

Proves committed value is in [0, 2642^{64}) without revealing the value. Based on Bulletproofs (Bunz et al., 2018). Logarithmic proof size.

RangeProof

static constexpr size_t RANGE_PROOF_BITS = 64;
static constexpr size_t RANGE_PROOF_LOG2 = 6;

struct RangeProof {
    fast::Point A, S;              // vector commitments
    fast::Point T1, T2;            // polynomial commitments
    fast::Scalar tau_x, mu, t_hat; // scalar responses
    std::array<fast::Point, 6> L;  // inner product argument (log2(64) rounds)
    std::array<fast::Point, 6> R;
    fast::Scalar a, b;             // final inner product scalars
};

range_prove

// Generate Bulletproof range proof for Pedersen commitment C = value*H + blinding*G
// value must be in [0, $2^{64}$). Uses CT layer.
RangeProof range_prove(
    uint64_t value,
    const fast::Scalar& blinding,
    const PedersenCommitment& commitment,
    const std::array<uint8_t, 32>& aux_rand);

range_verify

// Verify range proof (MSM-optimized, FAST path)
// Returns true if committed value is in [0, $2^{64}$)
bool range_verify(
    const PedersenCommitment& commitment,
    const RangeProof& proof);

ZK Batch Operations

batch_range_verify

// Batch-verify multiple range proofs (more efficient than individual verification)
// Returns true only if ALL proofs are valid.
bool batch_range_verify(
    const PedersenCommitment* commitments,
    const RangeProof* proofs,
    size_t count);

batch_commit

// Batch-create Pedersen commitments
void batch_commit(
    const fast::Scalar* values,
    const fast::Scalar* blindings,
    PedersenCommitment* commitments_out,
    size_t count);

GeneratorVectors

struct GeneratorVectors {
    std::array<fast::Point, 64> G;  // nothing-up-my-sleeve generators
    std::array<fast::Point, 64> H;
};

// Retrieve cached generator vectors (computed once on first call)
const GeneratorVectors& get_generator_vectors();

ZK Performance Summary

OperationTimeThroughputLayer
Pedersen Commit33.0 us30.3K op/sFAST
Knowledge Prove20.3 us49.3K op/sCT
Knowledge Verify21.8 us46.0K op/sFAST
DLEQ Prove40.0 us25.0K op/sCT
DLEQ Verify56.4 us17.7K op/sFAST
Range Prove (64b)13,467 us74 op/sCT
Range Verify (64b)2,634 us380 op/sFAST

Measured on i7-14400F, 11 passes, pinned core, median.


SNARK Witness Generation

Foreign-field witness generators that decompose secp256k1 signature verification into 5×52-bit limbs suitable for PLONK / Halo2 / Circom circuit wiring. All fields are split into ForeignFieldLimbs (five uint64_t values, each ≤ 2522^{52}).

EcdsaSnarkWitness

Decomposes an ECDSA signature (r, s) on message msg under pubkey into circuit-ready witness data: s_inv, u1 = msg·s⁻¹, u2 = r·s⁻¹, recovered R point, and the full public key coordinates.

struct EcdsaSnarkWitness {
    // --- public inputs (circuit "instance") ---
    ForeignFieldLimbs msg;       // H(m) mod n
    ForeignFieldLimbs sig_r;     // r
    ForeignFieldLimbs pub_x;     // pubkey.x   (field element)
    ForeignFieldLimbs pub_y;     // pubkey.y   (field element)

    // --- private witness ---
    ForeignFieldLimbs sig_s;     // s
    ForeignFieldLimbs s_inv;     // s⁻¹ mod n
    ForeignFieldLimbs u1;        // msg·s⁻¹ mod n
    ForeignFieldLimbs u2;        // r·s⁻¹ mod n
    ForeignFieldLimbs r_x;       // R.x  (recovered nonce point)
    ForeignFieldLimbs r_y;       // R.y

    // canonical 32-byte encodings
    std::array<uint8_t, 32> bytes_s_inv;
    std::array<uint8_t, 32> bytes_u1;
    std::array<uint8_t, 32> bytes_u2;
    std::array<uint8_t, 32> bytes_r_x;
    std::array<uint8_t, 32> bytes_r_y;
    std::array<uint8_t, 32> bytes_pub_y;

    bool valid;  // false if signature fails verification
};

EcdsaSnarkWitness ecdsa_snark_witness(
    const std::array<uint8_t, 32>& msg32,
    const fast::Point& pubkey,
    const fast::Scalar& sig_r,
    const fast::Scalar& sig_s);

SchnorrSnarkWitness

Decomposes a BIP-340 Schnorr signature (R.x, s) on message msg under pubkey_x (x-only) into circuit-ready witness data. Lifts both R and pubkey to full affine points (even Y via lift_x_even), computes the BIP-340 challenge e = H("BIP0340/challenge" || R.x || P.x || msg), and verifies s·G = R + e·P.

struct SchnorrSnarkWitness {
    // --- public inputs ---
    ForeignFieldLimbs msg;       // 32-byte message
    ForeignFieldLimbs sig_r;     // R.x (from sig64[0:32])
    ForeignFieldLimbs sig_s;     // s   (from sig64[32:64])
    ForeignFieldLimbs pub_x;     // x-only pubkey

    // --- private witness ---
    ForeignFieldLimbs r_y;       // lifted R.y (even)
    ForeignFieldLimbs pub_y;     // lifted P.y (even)
    ForeignFieldLimbs e;         // BIP-340 challenge scalar

    // canonical 32-byte encodings
    std::array<uint8_t, 32> bytes_r_y;
    std::array<uint8_t, 32> bytes_pub_y;
    std::array<uint8_t, 32> bytes_e;

    bool valid;  // false if signature fails BIP-340 verification
};

SchnorrSnarkWitness schnorr_snark_witness(
    const std::array<uint8_t, 32>& msg32,
    const std::array<uint8_t, 32>& pubkey_x,
    const fast::Scalar& sig_r,
    const fast::Scalar& sig_s);

CUDA API

Namespace: secp256k1::cuda

Header:

#include <secp256k1.cuh>

CUDA Data Structures

// Field element (4 x 64-bit limbs, little-endian)
struct FieldElement {
    uint64_t limbs[4];
};

// Scalar (4 x 64-bit limbs)
struct Scalar {
    uint64_t limbs[4];
};

// Jacobian point (X, Y, Z)
struct JacobianPoint {
    FieldElement x;
    FieldElement y;
    FieldElement z;
    bool infinity;
};

// Affine point (x, y)
struct AffinePoint {
    FieldElement x;
    FieldElement y;
};

// 32-bit view for optimized operations (zero-cost conversion)
struct MidFieldElement {
    uint32_t limbs[8];
};

CUDA Field Operations

All functions are __device__ and can only be called from GPU kernels.

// Initialization
__device__ void field_set_zero(FieldElement* r);
__device__ void field_set_one(FieldElement* r);

// Comparison
__device__ bool field_is_zero(const FieldElement* a);
__device__ bool field_eq(const FieldElement* a, const FieldElement* b);

// Arithmetic
__device__ void field_add(const FieldElement* a, const FieldElement* b, FieldElement* r);
__device__ void field_sub(const FieldElement* a, const FieldElement* b, FieldElement* r);
__device__ void field_mul(const FieldElement* a, const FieldElement* b, FieldElement* r);
__device__ void field_sqr(const FieldElement* a, FieldElement* r);
__device__ void field_inv(const FieldElement* a, FieldElement* r);
__device__ void field_neg(const FieldElement* a, FieldElement* r);

// Domain conversion (Montgomery mode only)
__device__ void field_to_mont(const FieldElement* a, FieldElement* r);
__device__ void field_from_mont(const FieldElement* a, FieldElement* r);

CUDA Point Operations

// Initialization
__device__ void jacobian_set_infinity(JacobianPoint* p);
__device__ void jacobian_set_generator(JacobianPoint* p);
__device__ bool jacobian_is_infinity(const JacobianPoint* p);

// Point arithmetic
__device__ void jacobian_double(const JacobianPoint* p, JacobianPoint* r);
__device__ void jacobian_add(const JacobianPoint* p, const JacobianPoint* q, JacobianPoint* r);
__device__ void jacobian_add_mixed(const JacobianPoint* p, const AffinePoint* q, JacobianPoint* r);

// Scalar multiplication
__device__ void scalar_mul(const JacobianPoint* p, const Scalar* k, JacobianPoint* r);
__device__ void scalar_mul_generator(const Scalar* k, JacobianPoint* r);

// Conversion
__device__ void jacobian_to_affine(const JacobianPoint* p, AffinePoint* r);

CUDA Batch Operations

#include <batch_inversion.cuh>

// Batch field inversion (Montgomery's trick)
// Inverts n field elements using only 1 modular inversion + 3(n-1) multiplications
__device__ void batch_invert(FieldElement* elements, int n, FieldElement* scratch);

CUDA Hash Operations

#include <hash160.cuh>

// Compute HASH160 = RIPEMD160(SHA256(pubkey))
__device__ void hash160_compressed(const uint8_t pubkey[33], uint8_t hash[20]);
__device__ void hash160_uncompressed(const uint8_t pubkey[65], uint8_t hash[20]);

CUDA Signature Operations

World-first: No other open-source GPU library provides secp256k1 ECDSA + Schnorr sign/verify.

Data Structures

#include <ecdsa.cuh>
#include <schnorr.cuh>
#include <recovery.cuh>

// ECDSA signature (r, s as Scalars)
struct ECDSASignatureGPU {
    Scalar r;
    Scalar s;
};

// Schnorr BIP-340 signature (32-byte R x-coordinate + Scalar s)
struct SchnorrSignatureGPU {
    uint8_t r[32];  // x-coordinate of R point
    Scalar s;
};

// Recoverable ECDSA signature
struct RecoverableSignatureGPU {
    ECDSASignatureGPU sig;
    int recid;  // Recovery ID (0-3)
};

Device Functions

// ECDSA Sign (RFC 6979 deterministic nonces, low-S normalization)
// Returns true on success
__device__ bool ecdsa_sign(
    const uint8_t msg_hash[32],   // 32-byte message hash
    const Scalar* privkey,         // Private key
    ECDSASignatureGPU* sig         // Output signature
);

// ECDSA Verify (Shamir's trick + GLV endomorphism)
// Returns true if signature is valid
__device__ bool ecdsa_verify(
    const uint8_t msg_hash[32],   // 32-byte message hash
    const JacobianPoint* pubkey,   // Public key (Jacobian)
    const ECDSASignatureGPU* sig   // Signature to verify
);

// ECDSA Sign with Recovery ID
__device__ bool ecdsa_sign_recoverable(
    const uint8_t msg_hash[32],
    const Scalar* privkey,
    RecoverableSignatureGPU* sig   // Output: signature + recid
);

// ECDSA Recover public key from signature
__device__ bool ecdsa_recover(
    const uint8_t msg_hash[32],
    const RecoverableSignatureGPU* sig,
    JacobianPoint* pubkey          // Output: recovered public key
);

// Schnorr Sign (BIP-340, tagged hash midstates for performance)
__device__ bool schnorr_sign(
    const Scalar* privkey,
    const uint8_t msg[32],
    const uint8_t aux_rand[32],    // Auxiliary randomness
    SchnorrSignatureGPU* sig       // Output signature
);

// Schnorr Verify (BIP-340, x-only pubkey)
__device__ bool schnorr_verify(
    const uint8_t pubkey_x[32],    // X-only public key (32 bytes)
    const uint8_t msg[32],
    const SchnorrSignatureGPU* sig
);

Batch Kernel Wrappers

Host-callable kernel wrappers for batch processing:

// Launch batch ECDSA sign (128 threads/block, 2 blocks/SM)
void ecdsa_sign_batch_kernel<<<blocks, 128>>>(
    const uint8_t* msg_hashes,     // N x 32 bytes
    const Scalar* privkeys,         // N scalars
    ECDSASignatureGPU* sigs,        // N output signatures
    int count
);

// Launch batch ECDSA verify
void ecdsa_verify_batch_kernel<<<blocks, 128>>>(
    const uint8_t* msg_hashes,
    const JacobianPoint* pubkeys,
    const ECDSASignatureGPU* sigs,
    bool* results,                  // N output booleans
    int count
);

// Launch batch Schnorr sign
void schnorr_sign_batch_kernel<<<blocks, 128>>>(
    const Scalar* privkeys,
    const uint8_t* msgs,
    const uint8_t* aux_rands,
    SchnorrSignatureGPU* sigs,
    int count
);

// Launch batch Schnorr verify
void schnorr_verify_batch_kernel<<<blocks, 128>>>(
    const uint8_t* pubkey_xs,
    const uint8_t* msgs,
    const SchnorrSignatureGPU* sigs,
    bool* results,
    int count
);

Performance

OperationTime/OpThroughput
ECDSA Sign204.8 ns4.88 M/s
ECDSA Verify410.1 ns2.44 M/s
ECDSA Sign + Recid311.5 ns3.21 M/s
Schnorr Sign273.4 ns3.66 M/s
Schnorr Verify354.6 ns2.82 M/s

RTX 5060 Ti, kernel-only timing, batch 16K


CUDA CT (Constant-Time) Operations

Namespace: secp256k1::cuda::ct

Header:

#include <ct/ct_sign.cuh>  // CT ECDSA/Schnorr sign
#include <ct/ct_zk.cuh>    // CT ZK knowledge/DLEQ prove

All CT functions use constant-time scalar multiplication and arithmetic to prevent side-channel leakage. Proving operations are CT; verification uses the fast path (public data only -- see zk.cuh).

CT Signing

// CT ECDSA sign (constant-time k*G, k^-1, scalar ops)
__device__ bool ct_ecdsa_sign(
    const uint8_t msg_hash[32],
    const Scalar* privkey,
    ECDSASignatureGPU* sig
);

// CT Schnorr sign (constant-time nonce generation + signing)
__device__ bool ct_schnorr_sign(
    const Scalar* privkey,
    const uint8_t msg[32],
    const uint8_t aux_rand[32],
    SchnorrSignatureGPU* sig
);

// CT Schnorr keypair (x-only pubkey extraction)
__device__ void ct_schnorr_keypair_create(
    const Scalar* privkey,
    SchnorrKeypairGPU* keypair
);

CT Zero-Knowledge Proofs

Data Structures
#include <zk.cuh>       // KnowledgeProofGPU, DLEQProofGPU
#include <ct/ct_zk.cuh>  // CT proving functions

// Knowledge proof (Schnorr sigma protocol)
struct KnowledgeProofGPU {
    uint8_t rx[32];  // R point x-coordinate
    Scalar s;        // Response scalar
};

// DLEQ proof (discrete log equality)
struct DLEQProofGPU {
    Scalar e;  // Challenge
    Scalar s;  // Response
};
Device Functions
// CT Knowledge Proof: proves knowledge of secret s such that P = s * B
// Uses CT scalar_mul for k*B (k is secret), CT scalar arithmetic
__device__ bool ct_knowledge_prove_device(
    const Scalar* secret,
    const JacobianPoint* pubkey,   // P = secret * base
    const JacobianPoint* base,     // Arbitrary base point B
    const uint8_t msg[32],
    const uint8_t aux[32],         // Auxiliary randomness (XOR hedging)
    KnowledgeProofGPU* proof
);

// Convenience: prove for generator G
__device__ bool ct_knowledge_prove_generator_device(
    const Scalar* secret,
    const JacobianPoint* pubkey,   // P = secret * G
    const uint8_t msg[32],
    const uint8_t aux[32],
    KnowledgeProofGPU* proof
);

// CT DLEQ Proof: proves log_G(P) == log_H(Q) without revealing the log
// Two CT scalar_mul operations (k*G, k*H), 6-point challenge hash
__device__ bool ct_dleq_prove_device(
    const Scalar* secret,
    const JacobianPoint* G,        // First base
    const JacobianPoint* H,        // Second base
    const JacobianPoint* P,        // P = secret * G
    const JacobianPoint* Q,        // Q = secret * H
    const uint8_t aux[32],
    DLEQProofGPU* proof
);
Batch Kernels
// Batch CT knowledge prove (one thread per proof)
__global__ void ct_knowledge_prove_batch_kernel(
    const Scalar* secrets,
    const JacobianPoint* pubkeys,
    const JacobianPoint* bases,
    const uint8_t* messages,    // N * 32 bytes
    const uint8_t* aux_rands,   // N * 32 bytes
    KnowledgeProofGPU* proofs,
    bool* results,
    uint32_t count
);

// Batch CT knowledge prove with generator G
__global__ void ct_knowledge_prove_generator_batch_kernel(
    const Scalar* secrets,
    const JacobianPoint* pubkeys,
    const uint8_t* messages,
    const uint8_t* aux_rands,
    KnowledgeProofGPU* proofs,
    bool* results,
    uint32_t count
);

// Batch CT DLEQ prove
__global__ void ct_dleq_prove_batch_kernel(
    const Scalar* secrets,
    const JacobianPoint* G_pts,
    const JacobianPoint* H_pts,
    const JacobianPoint* P_pts,
    const JacobianPoint* Q_pts,
    const uint8_t* aux_rands,
    DLEQProofGPU* proofs,
    bool* results,
    uint32_t count
);

Design note: Verification functions (knowledge_verify_device, dleq_verify_device) live in zk.cuh and use the fast (variable-time) path -- verification inputs are public.


OpenCL ZK Operations

Kernel file: src/opencl/kernels/secp256k1_zk.cl

OpenCL ZK operations use the fast-path scalar multiplication (wNAF-5). OpenCL has no separate CT layer -- all operations are uniform across work-items by design.

// Device functions (called from kernels)
void zk_knowledge_prove_impl(...);
void zk_knowledge_verify_impl(...);
void zk_dleq_prove_impl(...);
void zk_dleq_verify_impl(...);

// Batch kernels
__kernel void zk_knowledge_prove_batch(...);
__kernel void zk_knowledge_verify_batch(...);
__kernel void zk_dleq_prove_batch(...);
__kernel void zk_dleq_verify_batch(...);

Metal ZK Operations

Shader file: src/metal/shaders/secp256k1_zk.h Kernel file: src/metal/shaders/secp256k1_kernels.metal (Kernels 19-22)

Metal ZK uses branchless affine_select in scalar multiplication (semi-CT). 8x32 limb representation.

// Device functions
bool zk_knowledge_prove(...);
bool zk_knowledge_verify(...);
bool zk_dleq_prove(...);
bool zk_dleq_verify(...);

// Kernels 19-22
kernel void zk_knowledge_prove_batch(...)  [[kernel]];
kernel void zk_knowledge_verify_batch(...) [[kernel]];
kernel void zk_dleq_prove_batch(...)       [[kernel]];
kernel void zk_dleq_verify_batch(...)      [[kernel]];

WASM API

Module: @ultrafastsecp256k1/wasm

Usage:

import { Secp256k1 } from './secp256k1.mjs';
const lib = await Secp256k1.create();

Functions

FunctionParametersReturnsDescription
selftest()--booleanRun built-in self-test
version()--stringLibrary version ("3.0.0")
pubkeyCreate(seckey)Uint8Array(32){x, y}Public key from private key
pointMul(px, py, scalar)Uint8Array(32) x 3{x, y}Scalar x Point
pointAdd(px, py, qx, qy)Uint8Array(32) x 4{x, y}Point addition
ecdsaSign(msgHash, seckey)Uint8Array(32) x 2Uint8Array(64)ECDSA sign (r‖s)
ecdsaVerify(msgHash, pubX, pubY, sig)Uint8Array(32) x 3 + Uint8Array(64)booleanECDSA verify
schnorrSign(seckey, msg, aux?)Uint8Array(32) x 2-3Uint8Array(64)Schnorr BIP-340 sign
schnorrVerify(pubkeyX, msg, sig)Uint8Array(32) x 2 + Uint8Array(64)booleanSchnorr verify
schnorrPubkey(seckey)Uint8Array(32)Uint8Array(32)X-only public key
sha256(data)Uint8ArrayUint8Array(32)SHA-256 hash

C API

For direct C/C++ or custom WASM bindings, see secp256k1_wasm.h.

Example

const lib = await Secp256k1.create();
console.log('v' + lib.version(), lib.selftest() ? 'OK' : 'X');

// ECDSA workflow
const privkey = new Uint8Array(32);
privkey[31] = 1;
const { x, y } = lib.pubkeyCreate(privkey);
const msgHash = lib.sha256(new TextEncoder().encode('Hello'));
const sig = lib.ecdsaSign(msgHash, privkey);
const valid = lib.ecdsaVerify(msgHash, x, y, sig);

See bindings/wasm/README.md for detailed build and usage instructions.


C ABI (ufsecp)

The stable C ABI is defined in include/ufsecp/ufsecp.h. This is the optional libufsecp package for C callers, bindings, and explicit bridge consumers. Native C++ integrations and the libsecp256k1-compatible shim should link the engine target (secp256k1::fast / fastsecp256k1) directly. Top-level install emits ufsecp.pc only with -DSECP256K1_INSTALL_CABI=ON; the default secp256k1-fast.pc links the native engine library.

All functions follow these rules:

  • Opaque context: ufsecp_ctx* -- one per thread, or externally synchronised
  • Every function returns ufsecp_error_t (0 = OK)
  • All I/O is uint8_t[] with fixed sizes -- no internal types leak
  • Dual-layer CT: signing/nonce/key-tweak always use the CT layer; verify/point-arith use the fast layer. No opt-in flag.
  • Caller owns all buffers -- library never allocates on behalf of caller (except ctx_create/ctx_clone)

Security Properties (updated 2026-06-06)

  • Strict private key parsing: All functions accepting a private key use Scalar::parse_bytes_strict_nonzero(), which rejects keys >= n and == 0. Scalar::from_bytes() (silent mod-n reduction) is never used on secret inputs.
  • CT pubkey derivation: ufsecp_pubkey_create and all functions that derive a public key from a private key use ct::generator_mul(), not the variable-time Point::generator().scalar_mul(). No secret value touches the fast scalar-mul path.
  • Degenerate output detection: All signing functions (ufsecp_ecdsa_sign, ufsecp_schnorr_sign, their _verified and _batch variants) check for r == 0, s == 0, and all-zero Schnorr R x-coordinate after signing. On degenerate output the output buffer is zeroed and UFSECP_ERR_INTERNAL is returned — the zero signature is never serialized as success.
  • Fail-closed output buffers: Message-signing, FROST, Taproot, BIP39 seed, and BIP144 hash wrappers clear output buffers before processing. On any non-OK return, callers must treat the output as zeroed/invalid and must not reuse a previous value from the same buffer.
  • BIP39 C ABI validation: ufsecp_bip39_to_seed validates the mnemonic word list and checksum before PBKDF2. Invalid mnemonics return UFSECP_ERR_BAD_INPUT and leave seed64_out all zero.
  • BIP144 strict parser: ufsecp_bip144_txid enforces minimal CompactSize encodings and consumes all witness stacks before the 4-byte locktime. Extra bytes between witness data and locktime return UFSECP_ERR_BAD_INPUT.
  • Taproot tweak parsing: Taproot tweak scalars are parsed strictly; t >= n is rejected, while t == 0 remains valid per BIP-341. A final infinity output key or zero tweaked private key returns a non-OK error.
  • Batch fail-closed: Batch sign functions clear all output slots before processing. A per-slot failure zeroes that slot's output bytes; partial success is not possible.
  • Batch count == 0 rejected: ufsecp_ecdsa_sign_batch and ufsecp_schnorr_sign_batch return UFSECP_ERR_BAD_INPUT when count == 0.

Error Codes

CodeNameValue
UFSECP_OKSuccess0
UFSECP_ERR_NULL_ARGNULL pointer argument1
UFSECP_ERR_BAD_KEYInvalid private key2
UFSECP_ERR_BAD_PUBKEYInvalid public key3
UFSECP_ERR_BAD_SIGInvalid signature4
UFSECP_ERR_BAD_INPUTInvalid input data5
UFSECP_ERR_VERIFY_FAILVerification failed6
UFSECP_ERR_ARITHArithmetic error7
UFSECP_ERR_SELFTESTSelf-test failure8
UFSECP_ERR_INTERNALInternal error9
UFSECP_ERR_BUF_TOO_SMALLOutput buffer too small10

Size Constants

ConstantValueDescription
UFSECP_PRIVKEY_LEN32Private key
UFSECP_PUBKEY_COMPRESSED_LEN33Compressed public key
UFSECP_PUBKEY_UNCOMPRESSED_LEN65Uncompressed public key
UFSECP_PUBKEY_XONLY_LEN32x-only public key (BIP-340)
UFSECP_SIG_COMPACT_LEN64Compact R||S / r||s
UFSECP_ECDSA_OPAQUE_SIG_LEN64Copied secp256k1-compatible ECDSA opaque scalar storage
UFSECP_SIG_DER_MAX_LEN72Maximum DER-encoded ECDSA sig
UFSECP_HASH_LEN32SHA-256 / Keccak-256 digest
UFSECP_HASH160_LEN20RIPEMD160(SHA256) digest
UFSECP_SHARED_SECRET_LEN32ECDH shared secret
UFSECP_BIP32_SERIALIZED_LEN78BIP-32 extended key

Context Lifecycle

// Create context (runs self-test on first call)
ufsecp_ctx* ctx = NULL;
ufsecp_error_t err = ufsecp_ctx_create(&ctx);

// Clone context (deep copy, for multi-thread use)
ufsecp_ctx* ctx2 = NULL;
ufsecp_ctx_clone(ctx, &ctx2);

// Error inspection
ufsecp_error_t last = ufsecp_last_error(ctx);
const char* msg = ufsecp_last_error_msg(ctx);

// FFI layout assertion
size_t sz = ufsecp_ctx_size();

// Point the engine at your own fixed-base cache directory (replaces config.ini).
// NULL or "" => current working directory. Call before the first ctx_create.
ufsecp_set_cache_dir("/var/lib/myapp/ufsecp_cache");

// Destroy (NULL-safe)
ufsecp_ctx_destroy(ctx);
FunctionSignatureDescription
ufsecp_ctx_create(ufsecp_ctx** ctx_out) -> error_tCreate new context
ufsecp_ctx_clone(const ctx*, ufsecp_ctx** ctx_out) -> error_tDeep copy context
ufsecp_ctx_destroy(ctx*) -> voidFree context (NULL-safe)
ufsecp_last_error(const ctx*) -> error_tLast error code
ufsecp_last_error_msg(const ctx*) -> const char*Last error message
ufsecp_ctx_size(void) -> size_tCompiled ctx struct size
ufsecp_set_cache_dir(const char* dir|NULL) -> error_tSet fixed-base cache directory (replaces config.ini); NULL/"" = CWD. Process-global
ufsecp_context_randomize(ctx, seed32[32]|NULL) -> error_tInstall scalar blinding (thread-local); NULL clears

Private Key Operations

FunctionSignatureDescription
ufsecp_seckey_verify(ctx, privkey[32]) -> error_tValidate private key (non-zero, < n)
ufsecp_seckey_negate(ctx, privkey[32]) -> error_tNegate in-place: key <- -key mod n
ufsecp_seckey_tweak_add(ctx, privkey[32], tweak[32]) -> error_tkey <- (key + tweak) mod n
ufsecp_seckey_tweak_mul(ctx, privkey[32], tweak[32]) -> error_tkey <- (key * tweak) mod n

Public Key Operations

FunctionSignatureDescription
ufsecp_pubkey_create(ctx, privkey[32], pubkey33_out[33]) -> error_tCompressed pubkey from privkey (CT path; rejects key >= n or == 0)
ufsecp_pubkey_create_uncompressed(ctx, privkey[32], pubkey65_out[65]) -> error_tUncompressed pubkey from privkey (CT path; rejects key >= n or == 0)
ufsecp_pubkey_parse(ctx, input, input_len, pubkey33_out[33]) -> error_tParse 33 or 65 bytes to compressed
ufsecp_pubkey_xonly(ctx, privkey[32], xonly32_out[32]) -> error_tx-only pubkey (BIP-340)
ufsecp_pubkey_add(ctx, a33[33], b33[33], out33[33]) -> error_tPoint addition: out = a + b
ufsecp_pubkey_negate(ctx, pubkey33[33], out33[33]) -> error_tPoint negation: out = -P
ufsecp_pubkey_tweak_add(ctx, pubkey33[33], tweak[32], out33[33]) -> error_tout = P + tweak*G
ufsecp_pubkey_tweak_mul(ctx, pubkey33[33], tweak[32], out33[33]) -> error_tout = tweak * P
ufsecp_pubkey_combine(ctx, pubkeys, n, out33[33]) -> error_tSum N compressed pubkeys

ECDSA

FunctionSignatureDescription
ufsecp_ecdsa_sign(ctx, msg32[32], privkey[32], sig64_out[64]) -> error_tSign (RFC 6979, low-S, CT path; rejects key >= n or == 0; returns UFSECP_ERR_INTERNAL on r==0 or s==0, zeroes output)
ufsecp_ecdsa_sign_verified(ctx, msg32[32], privkey[32], sig64_out[64]) -> error_tSign + verify (fault resistance; same CT and degenerate-output guarantees as ufsecp_ecdsa_sign)
ufsecp_ecdsa_sign_batch(ctx, n, msgs32[], privkeys32[], sigs64_out[]) -> error_tCPU CT batch sign; rejects n == 0; clears all output slots before processing; per-slot failure zeroes that slot; repeats stable 32/32/64-byte item layout per entry
ufsecp_ecdsa_verify(ctx, msg32[32], sig64[64], pubkey33[33]) -> error_tVerify compact signature
ufsecp_ecdsa_sig_compact_to_opaque(ctx, sig64[64], opaque64_out[64]) -> error_tConvert compact ECDSA r||s to secp256k1-compatible opaque scalar storage; does not normalize
ufsecp_ecdsa_sig_opaque_to_compact(ctx, opaque64[64], sig64_out[64]) -> error_tConvert opaque scalar storage to compact r||s; does not normalize
ufsecp_ecdsa_sig_normalize_opaque(ctx, opaque64[64], opaque64_out[64], changed_out*) -> error_tLow-S normalize opaque storage; input/output may alias; changed_out is optional
ufsecp_ecdsa_verify_opaque(ctx, msg32[32], opaque64[64], pubkey33[33]) -> error_tVerify copied secp256k1-compatible opaque ECDSA storage; normalizes high-S internally before verify
ufsecp_ecdsa_verify_opaque_batch(ctx, msgs32[], pubs33[], opaque_sigs64[], n, results_out[]) -> error_tPer-row opaque ECDSA verify from columns; malformed rows return result 0 without aborting the whole batch
ufsecp_ecdsa_verify_opaque_rows(ctx, rows, stride, n, results_out[]) -> error_tPer-row opaque ECDSA verify from strided rows: `msg32
ufsecp_ecdsa_sig_to_der(ctx, sig64[64], der_out, der_len*) -> error_tCompact to DER encoding
ufsecp_ecdsa_sig_from_der(ctx, der, der_len, sig64_out[64]) -> error_tDER to compact encoding
ufsecp_ecdsa_sign_recoverable(ctx, msg32, privkey, sig64_out, recid_out*) -> error_tSign with recovery id (0-3)
ufsecp_ecdsa_recover(ctx, msg32, sig64, recid, pubkey33_out[33]) -> error_tRecover pubkey from recoverable sig

Opaque ECDSA support is for integrations whose public signature value stores copied secp256k1_ecdsa_signature scalar bytes. ufsecp_ecdsa_verify remains a strict compact r||s verifier; the opaque verify APIs parse the scalar storage, apply low-S normalization, and then verify, matching libsecp256k1's secp256k1_ecdsa_signature_normalize(...) plus verify path.

Schnorr / BIP-340

FunctionSignatureDescription
ufsecp_schnorr_sign(ctx, msg32, privkey, aux_rand[32], sig64_out) -> error_tBIP-340 sign (CT path; rejects key >= n or == 0; returns UFSECP_ERR_INTERNAL on s==0 or R x-coord all-zeros, zeroes output; aux_rand=zeros for deterministic)
ufsecp_schnorr_sign_verified(ctx, msg32, privkey, aux_rand, sig64_out) -> error_tSign + verify (fault resistance; same CT and degenerate-output guarantees as ufsecp_schnorr_sign)
ufsecp_schnorr_sign_batch(ctx, n, msgs32[], privkeys32[], aux_rands32[], sigs64_out[]) -> error_tCPU CT batch sign; rejects n == 0; clears all output slots before processing; per-slot failure zeroes that slot; aux_rands32 is requiredNULL is rejected with UFSECP_ERR_NULL_ARG (pass a zero-filled buffer to opt out of hedging)
ufsecp_schnorr_verify(ctx, msg32, sig64, pubkey_x[32]) -> error_tVerify BIP-340 signature

ECDH

FunctionSignatureDescription
ufsecp_ecdh(ctx, privkey, pubkey33, secret32_out) -> error_tSHA256(compressed shared point)
ufsecp_ecdh_xonly(ctx, privkey, pubkey33, secret32_out) -> error_tSHA256(x-coordinate)
ufsecp_ecdh_raw(ctx, privkey, pubkey33, secret32_out) -> error_tRaw x-coordinate (no hash)

Hashing

FunctionSignatureDescription
ufsecp_sha256(data, len, digest32_out) -> error_tSHA-256 (HW-accel when available)
ufsecp_sha512(data, len, digest64_out) -> error_tSHA-512
ufsecp_hash160(data, len, digest20_out) -> error_tRIPEMD160(SHA256)
ufsecp_tagged_hash(tag, data, len, digest32_out) -> error_tBIP-340 tagged hash

Addresses and WIF

FunctionSignatureDescription
ufsecp_addr_p2pkh(ctx, pubkey33, network, addr_out, addr_len*) -> error_tP2PKH (Base58)
ufsecp_addr_p2wpkh(ctx, pubkey33, network, addr_out, addr_len*) -> error_tP2WPKH (Bech32, SegWit v0)
ufsecp_addr_p2tr(ctx, internal_key_x[32], network, addr_out, addr_len*) -> error_tP2TR (Bech32m, Taproot)
ufsecp_wif_encode(ctx, privkey, compressed, network, wif_out, wif_len*) -> error_tPrivate key to WIF
ufsecp_wif_decode(ctx, wif, privkey32_out, compressed_out*, network_out*) -> error_tWIF to private key

Network constants: UFSECP_NET_MAINNET (0), UFSECP_NET_TESTNET (1).

BIP-32 HD Keys

Opaque key type: ufsecp_bip32_key (82 bytes, contains 78-byte serialised key + metadata).

FunctionSignatureDescription
ufsecp_bip32_master(ctx, seed, seed_len, key_out*) -> error_tMaster key from seed (16-64 bytes)
ufsecp_bip32_derive(ctx, parent*, index, child_out*) -> error_tSingle child derivation (>=0x80000000 = hardened)
ufsecp_bip32_derive_path(ctx, master*, path_str, key_out*) -> error_tFull path, e.g. "m/44'/0'/0'/0/0"
ufsecp_bip32_privkey(ctx, key*, privkey32_out) -> error_tExtract 32-byte private key
ufsecp_bip32_pubkey(ctx, key*, pubkey33_out) -> error_tExtract 33-byte compressed public key

Taproot / BIP-341

FunctionSignatureDescription
ufsecp_taproot_output_key(ctx, internal_x[32], merkle_root, output_x_out[32], parity_out*) -> error_tDerive Taproot output key
ufsecp_taproot_tweak_seckey(ctx, privkey[32], merkle_root, tweaked32_out[32]) -> error_tTweak privkey for key-path spend
ufsecp_taproot_verify(ctx, output_x, parity, internal_x, merkle_root, len) -> error_tVerify Taproot commitment

Taproot tweak helpers reject merkle_root == NULL when a positive root length is specified, reject roots longer than 32 bytes, strict-parse tweak scalars, and clear output buffers before returning input or internal errors.

BIP-39 Mnemonics

FunctionSignatureDescription
ufsecp_bip39_generate(ctx, entropy_bytes, entropy_in, mnemonic_out, mnemonic_len*) -> error_tGenerate mnemonic (12/15/18/21/24 words)
ufsecp_bip39_validate(ctx, mnemonic) -> error_tValidate mnemonic (checksum + wordlist)
ufsecp_bip39_to_seed(ctx, mnemonic, passphrase, seed64_out) -> error_tMnemonic to 64-byte seed (PBKDF2)
ufsecp_bip39_to_entropy(ctx, mnemonic, entropy_out, entropy_len*) -> error_tMnemonic back to raw entropy

Entropy sizes: 16 (12 words), 20 (15), 24 (18), 28 (21), 32 (24 words). Pass entropy_in=NULL for random.

Batch Verification

FunctionSignatureDescription
ufsecp_schnorr_batch_verify(ctx, entries, n) -> error_tVerify N Schnorr sigs. Entry: 32 xonly + 32 msg + 64 sig = 128 bytes
ufsecp_ecdsa_batch_verify(ctx, entries, n) -> error_tVerify N ECDSA sigs (single-threaded). Entry: 32 msg + 33 pubkey + 64 sig = 129 bytes
ufsecp_ecdsa_batch_verify_mt(ctx, entries, n, max_threads) -> error_tSame as ufsecp_ecdsa_batch_verify but verifies across CPU threads (engine-owned parallelism). max_threads: 0 = auto (hardware_concurrency, no arbitrary upper cap), 1 = serial. Result identical to serial for any thread count (verify is variable-time over public data)
ufsecp_schnorr_batch_identify_invalid(ctx, entries, n, invalid_out, invalid_count*) -> error_tFind indices of invalid Schnorr sigs
ufsecp_ecdsa_batch_identify_invalid(ctx, entries, n, invalid_out, invalid_count*) -> error_tFind indices of invalid ECDSA sigs

The C++ engine also provides secp256k1::schnorr_batch_verify_mt(entries, n, max_threads), the multi-threaded twin of ecdsa_batch_verify_mt (same chunked design, 0=auto/1=serial, result identical to serial for any thread count).

Standard integration surface (CPU): for drop-in libsecp256k1 compatibility, prefer the shim batch extension in compat/libsecp256k1_shim/include/secp256k1_batch.h (secp256k1_ecdsa_verify_batch[_mt|_results] / secp256k1_schnorrsig_verify_batch[_mt|_results]), which wraps the engine MT path with caller max_threads control and optional per-row results. Every function also takes a trailing const ufsecp_cancel_token* cancel (shared type in ufsecp/ufsecp_cancel.h, same as the libbitcoin bridge; C++ default NULL = no cancellation, zero overhead) so a long batch can be aborted from outside; on cancel it returns 0 (fail-closed). See INTEGRATION_MODELS.md. The ufsecp_* batch entries above are the native (non-shim) ABI; the libbitcoin_bridge below is the advanced GPU/zero-copy tier.

Libbitcoin Bridge

The optional compat/libbitcoin_bridge C ABI exposes one controller and two script-signature batch layouts. Both layouts return identical per-row verdicts and keep ECDSA and Schnorr batches homogeneous.

FunctionSignatureDescription
ufsecp_lbtc_ctrl_create(ctrl**, backend) -> error_tCreate bridge controller; AUTO binds GPU when available, otherwise CPU
ufsecp_lbtc_verify_ecdsa_mt / _ecdsa_opaque_mt / _ecdsa_compact_mt / _schnorr_mt(ctrl, rows, n, key_size, results, invalid_idx, invalid_cap, invalid_count, max_threads, cancel=NULL)Multi-threaded twins of the packed-row verify entries. max_threads: 0=auto (all cores), 1=serial (identical to the non-mt fn), N=cap. Identical per-row verdict; GPU path unaffected (governs the CPU fallback only); cancellation preserved. _compact_mt's CPU fallback is per-row/serial — prefer _opaque_mt for MT.
ufsecp_lbtc_verify_ecdsa(ctrl, rows, n, key_size, results, invalid_idx, invalid_cap, invalid_count, cancel=NULL)Packed-row ECDSA verify; row = `hash32
ufsecp_lbtc_verify_schnorr(ctrl, rows, n, key_size, results, invalid_idx, invalid_cap, invalid_count, cancel=NULL)Packed-row Schnorr verify; row = `hash32
ufsecp_lbtc_verify_ecdsa_columns(ctrl, hashes32, pubkeys33, sigs64, n, results, cancel=NULL)Columnar ECDSA verify; avoids bridge-side row-to-column de-interleave
ufsecp_lbtc_verify_schnorr_columns(ctrl, hashes32, pubkeys_x32, sigs64, n, results, cancel=NULL)Columnar Schnorr verify; avoids bridge-side row-to-column de-interleave
ufsecp_lbtc_verify_ecdsa_collect(ctrl, rows, n, key_size, cancel=NULL)Packed-row collect; valid rows zero the row tail, invalid rows keep it
ufsecp_lbtc_verify_schnorr_collect(ctrl, rows, n, key_size, cancel=NULL)Packed-row Schnorr collect
ufsecp_lbtc_verify_ecdsa_collect_mt / _schnorr_collect_mt(ctrl, rows, n, key_size, max_threads, cancel=NULL)Multi-threaded twins of the collect entries (same verdict/key-cell semantics; max_threads 0=auto, 1=serial, N=cap).
ufsecp_lbtc_verify_ecdsa_columns_collect(ctrl, hashes32, pubkeys33, sigs64, n, key_cells, key_size, cancel=NULL)Columnar collect; valid rows zero key_cells[i], invalid rows keep it
ufsecp_lbtc_verify_schnorr_columns_collect(ctrl, hashes32, pubkeys_x32, sigs64, n, key_cells, key_size, cancel=NULL)Columnar Schnorr collect

Use the packed-row API when the producer naturally stores [record | key] tuples. Use the columnar API when the producer already stores hashes[], pubkeys[], sigs[], and optional key_cells[]; on GPU builds the bridge forwards these columns directly to the existing GPU C ABI instead of building temporary columns from rows. The opaque key bytes are correlation metadata only and are never interpreted by the bridge.

All libbitcoin verify/collect entry points accept a trailing const ufsecp_cancel_token* cancel argument. NULL preserves the old behavior. When the token callback returns non-zero, the call returns UFSECP_ERR_CANCELLED; callers must discard partial results or collect-key state. C++ callers may omit the argument because the header supplies a default NULL; plain C callers pass the final argument explicitly.

Multi-Scalar Multiplication

FunctionSignatureDescription
ufsecp_shamir_trick(ctx, a[32], P33[33], b[32], Q33[33], out33[33]) -> error_tCompute aP + bQ
ufsecp_multi_scalar_mul(ctx, scalars, points, n, out33[33]) -> error_tCompute sum(scalars[i] * points[i])

Scalars: 32-byte big-endian, contiguous. Points: 33-byte compressed, contiguous.

MuSig2 / BIP-327

Size constants: UFSECP_MUSIG2_PUBNONCE_LEN (66), UFSECP_MUSIG2_AGGNONCE_LEN (66), UFSECP_MUSIG2_KEYAGG_LEN (165), UFSECP_MUSIG2_SESSION_LEN (165), UFSECP_MUSIG2_SECNONCE_LEN (64).

FunctionSignatureDescription
ufsecp_musig2_key_agg(ctx, pubkeys, n, keyagg_out, agg_pubkey32_out) -> error_tAggregate x-only pubkeys
ufsecp_musig2_nonce_gen(ctx, privkey, pubkey32, agg_pubkey32, msg32, extra_in, secnonce_out, pubnonce_out) -> error_tGenerate nonce pair
ufsecp_musig2_nonce_agg(ctx, pubnonces, n, aggnonce_out) -> error_tAggregate public nonces
ufsecp_musig2_start_sign_session(ctx, aggnonce, keyagg, msg32, session_out) -> error_tStart signing session
ufsecp_musig2_partial_sign(ctx, secnonce, privkey, keyagg, session, signer_idx, partial_sig32_out) -> error_tProduce partial signature
ufsecp_musig2_partial_verify(ctx, partial_sig32, pubnonce, pubkey32, keyagg, session, signer_idx) -> error_tVerify partial signature
ufsecp_musig2_partial_sig_agg(ctx, partial_sigs, n, session, sig64_out) -> error_tAggregate partials to final BIP-340 sig

FROST Threshold Signatures

Size constants: UFSECP_FROST_SHARE_LEN (36), UFSECP_FROST_KEYPKG_LEN (141), UFSECP_FROST_NONCE_LEN (64), UFSECP_FROST_NONCE_COMMIT_LEN (70).

Input invariants enforced by the current implementation:

  • participant IDs are 1-based; id == 0 is rejected during keygen/finalize and yields a zero Lagrange coefficient
  • signer/commitment/share sender sets must be unique; duplicate signer IDs are treated as invalid and fail closed
  • FROST keygen polynomial coefficients are derived from the pair (participant_id, coeff_index) rather than the old arithmetic id*1000+j scheme, preventing cross-participant coefficient collisions
FunctionSignatureDescription
ufsecp_frost_keygen_begin(ctx, id, threshold, n, seed, commits_out, commits_len*, shares_out, shares_len*) -> error_tKey generation phase 1
ufsecp_frost_keygen_finalize(ctx, id, all_commits, commits_len, shares, shares_len, t, n, keypkg_out) -> error_tKey generation phase 2
ufsecp_frost_sign_nonce_gen(ctx, id, nonce_seed, nonce_out, nonce_commit_out) -> error_tGenerate signing nonce
ufsecp_frost_sign(ctx, keypkg, nonce, msg32, nonce_commits, n_signers, partial_sig_out[36]) -> error_tProduce partial signature
ufsecp_frost_verify_partial(ctx, partial_sig[36], verification_share33[33], nonce_commits, n_signers, msg32, group_pubkey32) -> error_tVerify partial signature
ufsecp_frost_aggregate(ctx, partial_sigs, n, nonce_commits, n_signers, group_pubkey32, msg32, sig64_out) -> error_tAggregate to final Schnorr sig

Adaptor Signatures

Size constants: UFSECP_SCHNORR_ADAPTOR_SIG_LEN (97), UFSECP_ECDSA_ADAPTOR_SIG_LEN (130).

Current adaptor invariants:

  • Schnorr adaptor signatures remain bound to the supplied adaptor point through the challenge computed over R^ + T
  • Schnorr adaptor verification rejects pre-signatures whose reconstructed R = R^ + T_adj has odd Y parity, matching the BIP-340 even-Y requirement for the final adapted signature
  • ECDSA adaptor signatures use a multiplicative adapt/extract flow: adapt divides out the adaptor secret and extract recovers the same secret from (pre_sig, sig)
  • ECDSA adaptor verification is adaptor-point bound; verifying the same pre-signature against the wrong adaptor point fails

Schnorr Adaptor:

FunctionSignatureDescription
ufsecp_schnorr_adaptor_sign(ctx, privkey, msg32, adaptor33, aux_rand, pre_sig_out[97]) -> error_tPre-sign
ufsecp_schnorr_adaptor_verify(ctx, pre_sig[97], pubkey_x, msg32, adaptor33) -> error_tVerify pre-signature
ufsecp_schnorr_adaptor_adapt(ctx, pre_sig[97], secret[32], sig64_out) -> error_tAdapt to valid signature
ufsecp_schnorr_adaptor_extract(ctx, pre_sig[97], sig64, secret32_out) -> error_tExtract adaptor secret

ECDSA Adaptor:

FunctionSignatureDescription
ufsecp_ecdsa_adaptor_sign(ctx, privkey, msg32, adaptor33, pre_sig_out[130]) -> error_tPre-sign
ufsecp_ecdsa_adaptor_verify(ctx, pre_sig[130], pubkey33, msg32, adaptor33) -> error_tVerify pre-signature
ufsecp_ecdsa_adaptor_adapt(ctx, pre_sig[130], secret[32], sig64_out) -> error_tAdapt to valid signature
ufsecp_ecdsa_adaptor_extract(ctx, pre_sig[130], sig64, secret32_out) -> error_tExtract adaptor secret

Pedersen Commitments

FunctionSignatureDescription
ufsecp_pedersen_commit(ctx, value[32], blinding[32], commitment33_out) -> error_tC = valueH + blindingG
ufsecp_pedersen_verify(ctx, commitment33, value[32], blinding[32]) -> error_tVerify commitment
ufsecp_pedersen_verify_sum(ctx, pos, n_pos, neg, n_neg) -> error_tVerify balance: sum(pos) == sum(neg)
ufsecp_pedersen_blind_sum(ctx, blinds_in, n_in, blinds_out, n_out, sum32_out) -> error_tCompute blinding factor sum
ufsecp_pedersen_switch_commit(ctx, value, blinding, switch_blind, commitment33_out) -> error_tSwitch commitment (3-gen)

Zero-Knowledge Proofs

Size constants: UFSECP_ZK_KNOWLEDGE_PROOF_LEN (64), UFSECP_ZK_DLEQ_PROOF_LEN (64), UFSECP_ZK_RANGE_PROOF_MAX_LEN (675).

FunctionSignatureDescription
ufsecp_zk_knowledge_prove(ctx, secret, pubkey33, msg32, aux_rand, proof_out[64]) -> error_tProve knowledge of discrete log
ufsecp_zk_knowledge_verify(ctx, proof[64], pubkey33, msg32) -> error_tVerify knowledge proof
ufsecp_zk_dleq_prove(ctx, secret, G33, H33, P33, Q33, aux_rand, proof_out[64]) -> error_tProve DLEQ: logG(P) == logH(Q)
ufsecp_zk_dleq_verify(ctx, proof[64], G33, H33, P33, Q33) -> error_tVerify DLEQ proof
ufsecp_zk_range_prove(ctx, value, blinding, commitment33, aux_rand, proof_out, proof_len*) -> error_tBulletproof range proof
ufsecp_zk_range_verify(ctx, commitment33, proof, proof_len) -> error_tVerify Bulletproof range proof
ufsecp_zk_ecdsa_snark_witness(ctx, msg32, pubkey33, sig64, witness_out*) -> error_tECDSA SNARK witness (760 bytes) — ForeignFieldLimbs decomposition for PLONK/Halo2
ufsecp_zk_schnorr_snark_witness(ctx, msg32, pubkey_x32, sig64, witness_out*) -> error_tBIP-340 Schnorr SNARK witness (472 bytes) — ForeignFieldLimbs decomposition for PLONK/Halo2

Multi-Coin Wallet

Coin type constants (BIP-44): UFSECP_COIN_BITCOIN (0), UFSECP_COIN_LITECOIN (2), UFSECP_COIN_DOGECOIN (3), UFSECP_COIN_DASH (5), UFSECP_COIN_ETHEREUM (60), UFSECP_COIN_BITCOIN_CASH (145), UFSECP_COIN_TRON (195).

FunctionSignatureDescription
ufsecp_coin_address(ctx, pubkey33, coin_type, testnet, addr_out, addr_len*) -> error_tDefault address for any coin
ufsecp_coin_derive_from_seed(ctx, seed, seed_len, coin_type, account, change, index, testnet, privkey_out, pubkey_out, addr_out, addr_len*) -> error_tFull derivation from seed
ufsecp_coin_wif_encode(ctx, privkey, coin_type, testnet, wif_out, wif_len*) -> error_tWIF for any coin
ufsecp_btc_message_sign(ctx, msg, msg_len, privkey, base64_out, base64_len*) -> error_tBitcoin message sign (BIP-137)
ufsecp_btc_message_verify(ctx, msg, msg_len, pubkey33, base64_sig) -> error_tBitcoin message verify
ufsecp_btc_message_hash(msg, msg_len, digest32_out) -> error_tBitcoin message hash

Ethereum

Available when built with -DSECP256K1_BUILD_ETHEREUM=ON (default ON).

FunctionSignatureDescription
ufsecp_keccak256(data, len, digest32_out) -> error_tKeccak-256 hash
ufsecp_eth_address(ctx, pubkey33, addr20_out) -> error_t20-byte Ethereum address
ufsecp_eth_address_checksummed(ctx, pubkey33, addr_out, addr_len*) -> error_tEIP-55 checksummed address string
ufsecp_eth_personal_hash(msg, msg_len, digest32_out) -> error_tEIP-191 personal_sign hash
ufsecp_eth_sign(ctx, msg32, privkey, r_out, s_out, v_out*, chain_id) -> error_tECDSA with recovery (EIP-155 v)
ufsecp_eth_ecrecover(ctx, msg32, r, s, v, addr20_out) -> error_tRecover address from v,r,s

GPU Operations

Backend-neutral GPU acceleration surface. All functions use opaque ufsecp_gpu_ctx* (separate from CPU ufsecp_ctx*). Include <ufsecp/ufsecp_gpu.h>.

GPU Error Codes (100--106): UFSECP_ERR_GPU_UNAVAILABLE (100), UFSECP_ERR_GPU_DEVICE (101), UFSECP_ERR_GPU_LAUNCH (102), UFSECP_ERR_GPU_MEMORY (103), UFSECP_ERR_GPU_UNSUPPORTED (104), UFSECP_ERR_GPU_BACKEND (105), UFSECP_ERR_GPU_QUEUE (106).

Current GPU C ABI failure semantics:

  • output buffers are cleared to their invalid/zero default before processing for result-bearing batch operations, and cleared again if backend dispatch returns non-OK
  • ufsecp_bip352_prepare_scan_plan and ufsecp_gpu_bip352_scan_batch reject scan_privkey32 == 0 and scan_privkey32 >= n; rejected scan keys leave plan264_out / prefix64_out zeroed
  • secret-bearing GPU backends erase uploaded ECDH, BIP-352, and BIP-324 key buffers before releasing host/shared/device storage
  • ufsecp_gpu_*_verify_collect is excluded from output pre-clearing because its key_buffer is intentionally an in/out marker buffer used by fallback callers

Discovery

FunctionSignatureDescription
ufsecp_gpu_backend_count(ids_out*, max) -> uint32_tNumber of compiled backends; optionally fills array
ufsecp_gpu_backend_name(bid) -> const char*Human-readable backend name ("CUDA", "OpenCL", "Metal")
ufsecp_gpu_is_available(bid) -> int1 if backend has >= 1 usable device, 0 otherwise
ufsecp_gpu_device_count(bid) -> uint32_tDevices visible to backend bid
ufsecp_gpu_device_info(bid, dev, info_out*) -> error_tFill ufsecp_gpu_device_info_t (name, memory, CUs, clock)

Context Lifecycle

FunctionSignatureDescription
ufsecp_gpu_ctx_create(ctx_out**, bid, dev) -> error_tCreate GPU context on backend bid, device dev
ufsecp_gpu_ctx_destroy(ctx*) -> voidDestroy context (NULL-safe)
ufsecp_gpu_last_error(ctx*) -> error_tLast error code from ctx
ufsecp_gpu_last_error_msg(ctx*) -> const char*Last error message string
ufsecp_gpu_error_str(code) -> const char*Error code to string (CPU + GPU codes)

Batch Operations and Extensions

FunctionSignatureDescription
ufsecp_gpu_generator_mul_batch(ctx, scalars32[], n, pubkeys33_out[]) -> error_tk*G for n scalars
ufsecp_gpu_ecdsa_verify_batch(ctx, msgs32[], pubs33[], sigs64[], n, results_out[]) -> error_tECDSA batch verify
ufsecp_gpu_ecdsa_verify_opaque_rows / ufsecp_gpu_ecdsa_verify_lbtc_rows(ctx, rows, stride, n, results_out[]) -> error_tDirect strided opaque ECDSA rows; CUDA/OpenCL/Metal parse and low-S normalize on device
ufsecp_gpu_schnorr_verify_batch(ctx, msgs32[], pubs_x32[], sigs64[], n, results_out[]) -> error_tSchnorr/BIP-340 batch verify
ufsecp_gpu_ecdsa_verify_lbtc_columns(ctx, digests32[], pubs33[], sigs64[], n, results_out[]) -> error_tDirect libbitcoin ECDSA column (SoA) verify; opaque-LE sig parsed on device. C ABI completeness only — the libbitcoin-direct surface is the C++ engine entrypoint, which reaches GPU via install_gpu_columns_verify_hook (see below), not this C ABI.
ufsecp_gpu_schnorr_verify_lbtc_columns(ctx, digests32[], xonly32[], sigs64[], n, results_out[]) -> error_tDirect libbitcoin Schnorr column (SoA) verify; BIP-340 parsed on device. C ABI completeness only (see above).
ufsecp_gpu_ecdsa_verify_collect(ctx, msgs32[], pubs33[], sigs64[], n, key_buffer[]) -> error_tECDSA batch verify, in-place 1-byte/row verdict (valid→0, invalid→left); libbitcoin collect. Native on CUDA, OpenCL, and Metal
ufsecp_gpu_schnorr_verify_collect(ctx, msgs32[], pubs_x32[], sigs64[], n, key_buffer[]) -> error_tSchnorr batch verify, in-place verdict; native on CUDA, OpenCL, and Metal (see ecdsa_verify_collect)
ufsecp_gpu_ecdh_batch(ctx, privkeys32[], pubs33[], n, secrets32_out[]) -> error_tECDH shared secrets (SECRET-BEARING)
ufsecp_gpu_hash160_pubkey_batch(ctx, pubs33[], n, hashes20_out[]) -> error_tSHA-256 + RIPEMD-160 of pubkeys
ufsecp_gpu_tagged_hash_var(ctx, tag_hash32, msgs, msg_lens[], stride, n, out32) -> error_tBIP-340 tagged hash over per-item variable-length messages (TapLeaf scripts); each msg_lens[i] must be in [1,256]
ufsecp_gpu_hash256(ctx, inputs, input_len, n, out32) -> error_tBatch Bitcoin HASH256 (double SHA-256) of fixed-length inputs; input_len must be in [1,320]
ufsecp_gpu_hash256_var(ctx, inputs, input_lens[], stride, n, out32) -> error_tBatch Bitcoin HASH256 (double SHA-256) over per-item variable-length inputs; row i = inputs[i*stride .. i*stride+input_lens[i]), bytes beyond input_lens[i] up to stride are ignored padding. No tag prefix, no transaction parsing — GPU treats each row as an opaque byte string; rows up to stride <= 4 MiB (kMaxHash256VarStride). PUBLIC-DATA / variable-time
ufsecp_gpu_msm(ctx, scalars32[], points33[], n, result33_out) -> error_tMulti-scalar multiplication
ufsecp_gpu_frost_verify_partial_batch(ctx, z_i32[], D_i33[], E_i33[], Y_i33[], rho_i32[], lambda_ie32[], negate_R[], negate_key[], n, results_out[]) -> error_tBatch FROST partial verification
ufsecp_gpu_ecrecover_batch(ctx, msgs32[], sigs64[], recids[], n, pubkeys33_out[], valid_out[]) -> error_tBatch public-key recovery from recoverable ECDSA signatures
ufsecp_gpu_zk_knowledge_verify_batch(ctx, proofs64[], pubkeys65[], msgs32[], n, results[]) -> error_tBatch ZK knowledge proof verification (PUBLIC)
ufsecp_gpu_zk_dleq_verify_batch(ctx, proofs64[], G65[], H65[], P65[], Q65[], n, results[]) -> error_tBatch DLEQ proof verification (PUBLIC)
ufsecp_gpu_bulletproof_verify_batch(ctx, proofs324[], commits65[], H65, n, results[]) -> error_tBatch Bulletproof range proof verification (PUBLIC)
ufsecp_gpu_bip324_aead_encrypt_batch(ctx, keys32[], nonces12[], plain[], sizes[], max_payload, n, wire_out[]) -> error_tBatch BIP-324 ChaCha20-Poly1305 AEAD encrypt (SECRET: keys sent to GPU)
ufsecp_gpu_bip324_aead_decrypt_batch(ctx, keys32[], nonces12[], wire[], sizes[], max_payload, n, plain_out[], valid[]) -> error_tBatch BIP-324 ChaCha20-Poly1305 AEAD decrypt (SECRET)
ufsecp_gpu_zk_ecdsa_snark_witness_batch(ctx, msgs32[], pubs33[], sigs64[], n, witnesses760_out[]) -> error_tBatch ECDSA SNARK witness generation — eprint 2025/695 (PUBLIC inputs)
ufsecp_gpu_zk_schnorr_snark_witness_batch(ctx, msgs32[], pubkeys_x32[], sigs64[], n, witnesses472_out[]) -> error_tBatch BIP-340 Schnorr SNARK witness generation (PUBLIC inputs). GPU kernels pending — CPU fallback returns Unsupported via virtual dispatch.
ufsecp_gpu_bip352_scan_batch(ctx, scan_privkey32, spend_pubkey33, tweaks33[], n, prefix64_out[]) -> error_tBIP-352 Silent Payment batch scan — returns upper-64-bit x-coordinate prefix per tweak (SECRET-BEARING: scan key sent to GPU)

BIP-352 CPU utility

FunctionSignatureDescription
ufsecp_bip352_prepare_scan_plan(scan_privkey32, plan264_out) -> error_tPrecompute 264-byte BIP-352 GLV wNAF scan plan for repeated GPU batch scans; rejects zero/order-or-larger scan keys and zeroes the plan on error

libbitcoin-direct column verify (C++ engine surface)

Header: <secp256k1/batch_verify.hpp>. These are the single public surface the libbitcoin-direct engine calls (ufsecp::lbtc::* invokes exactly these). They verify column (Structure-of-Arrays) batches and, when a GPU accelerator is installed, use it transparently — the caller sees one API, one boolean-plus-per-row result, no CPU/GPU split, and no recoverable GPU status. Verification is variable-time over PUBLIC data (pubkey, signature, message) — correct by design.

namespace secp256k1 {

// Column entrypoints. Return true iff all rows verify. When out_results is
// non-null it is filled 1=valid / 0=invalid per row. count == 0 returns true.
// Null column pointers or a size overflow return false and zero out_results
// (fail-closed input validation). When a GPU columns hook is installed the
// entrypoint attempts GPU first and falls back to the deterministic CPU column
// path — with identical results — whenever the hook declines. An operational
// GPU/backend error is a decline -> CPU fallback, NEVER a consensus-invalid row.
bool ecdsa_batch_verify_opaque_columns(const std::uint8_t* digests32,
                                       const std::uint8_t* pubkeys33,
                                       const std::uint8_t* sigs64,
                                       std::size_t count,
                                       std::uint8_t* out_results = nullptr,
                                       std::size_t max_threads = 0);

bool schnorr_batch_verify_bip340_columns(const std::uint8_t* digests32,
                                         const std::uint8_t* xonly32,
                                         const std::uint8_t* sigs64,
                                         std::size_t count,
                                         std::uint8_t* out_results = nullptr,
                                         std::size_t max_threads = 0);

// Installable GPU accelerator hook (PUBLIC-DATA verify only). Null by default =>
// pure CPU. Self-installed by the GPU-host layer (gpu_engine_hook.cpp) via a
// static initializer whenever the GPU host is linked; no compile-time macro.
//   kind == 0 : ECDSA opaque-LE columns (keys = pubkeys33)
//   kind == 1 : Schnorr BIP-340 columns (keys = xonly32)
// Return contract:
//    1 -> handled; out_results fully written; ALL rows valid.
//    0 -> handled; out_results fully written; at least one row invalid.
//   -1 -> NOT handled (GPU unavailable/unsupported/operational error); the engine
//         MUST fall back to the CPU column path. A declining hook MUST NOT write a
//         consensus-invalid all-zero buffer to signal "not handled".
using GpuColumnsVerifyHook = int (*)(int kind,
        const std::uint8_t* digests32, const std::uint8_t* keys,
        const std::uint8_t* sigs64, std::size_t count,
        std::uint8_t* out_results) noexcept;

// Install (nullptr clears). Thread-safe. Returns the previous hook (save/restore).
GpuColumnsVerifyHook install_gpu_columns_verify_hook(GpuColumnsVerifyHook hook) noexcept;

} // namespace secp256k1

Semantics:

  • One caller surface. There is no caller-visible _gpu variant and no recoverable GPU status on this surface. The caller never chunks, never selects CPU vs GPU, and never manages GPU buffers; chunking and reusable staging are engine/backend-owned.
  • GPU is an internal accelerator. With a hook installed, each entrypoint attempts the GPU column verify (kind 0=ECDSA / 1=Schnorr). With no hook, or on a -1 decline, the engine completes the batch on the deterministic CPU column path with byte-identical boolean + per-row results.
  • ECDSA high-S is consensus-valid here. The libbitcoin-direct opaque ECDSA entrypoints accept mathematically valid high-S signatures and do not rewrite caller buffers. Low-S strictness remains a shim/standardness policy, not this consensus verify contract.
  • Fatal, not invalid. Operational GPU/backend errors are declines converted to a CPU fallback — never consensus-invalid rows or an all-zero OK result. Only an unrecoverable inability to complete the math (with no fallback) fails hard (a fatal engine failure, not consensus data). Malformed layout (null column pointers / size overflow) is fail-closed false with zeroed results (input validation, not a GPU error).
  • Default is pure CPU. The hook is null unless the libbitcoin-GPU build installs one, so the default direct build is CPU-only until that enablement lands.

Performance Tips

CPU

  1. Use in-place operations when possible:

    // Slower: creates temporary
    point = point.add(other);
    
    // Faster: no allocation
    point.add_inplace(other);
    
  2. Use KPlan for fixed-K multiplication:

    // If K is constant and Q varies, precompute K once
    KPlan plan = KPlan::from_scalar(K);
    for (auto& Q : points) {
        result = Q.scalar_mul_with_plan(plan);
    }
    
  3. Use from_limbs for binary I/O (not from_bytes):

    // Database/binary files: use from_limbs (native little-endian)
    FieldElement::from_limbs(limbs);
    
    // Hex strings/test vectors: use from_bytes (big-endian)
    FieldElement::from_bytes(bytes);
    

CUDA

  1. Batch operations: Process thousands of points in parallel
  2. Avoid divergence: Use branchless algorithms where possible
  3. Memory coalescing: Align data structures to 32/64 bytes
  4. Use hybrid 32-bit multiplication: Enabled by default (SECP256K1_CUDA_USE_HYBRID_MUL=1)

Examples

Generate Bitcoin Address (CPU)

#include <secp256k1/point.hpp>
#include <secp256k1/scalar.hpp>

using namespace secp256k1::fast;

int main() {
    // Private key (256-bit)
    Scalar private_key = Scalar::from_hex(
        "E9873D79C6D87DC0FB6A5778633389F4453213303DA61F20BD67FC233AA33262"
    );
    
    // Public key = private_key x G
    Point G = Point::generator();
    Point public_key = G.scalar_mul(private_key);
    
    // Get compressed public key (33 bytes)
    auto compressed = public_key.to_compressed();
    
    // Print as hex
    for (auto byte : compressed) {
        printf("%02x", byte);
    }
    printf("\n");
    
    return 0;
}

Batch Point Generation (CUDA)

#include <secp256k1.cuh>

using namespace secp256k1::cuda;

__global__ void generate_points_kernel(
    const Scalar* private_keys,
    AffinePoint* public_keys,
    int count
) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx >= count) return;
    
    JacobianPoint result;
    scalar_mul_generator(&private_keys[idx], &result);
    jacobian_to_affine(&result, &public_keys[idx]);
}

void generate_points(
    const Scalar* d_private_keys,
    AffinePoint* d_public_keys,
    int count
) {
    int threads = 256;
    int blocks = (count + threads - 1) / threads;
    generate_points_kernel<<<blocks, threads>>>(
        d_private_keys, d_public_keys, count
    );
}

Verify Self-Test (CPU)

#include <secp256k1/point.hpp>
#include <iostream>

int main() {
    bool ok = secp256k1::fast::Selftest(true);
    if (ok) {
        std::cout << "All tests passed!" << std::endl;
        return 0;
    } else {
        std::cerr << "TESTS FAILED!" << std::endl;
        return 1;
    }
}

Build Configuration Macros

CPU

MacroDefaultDescription
SECP256K1_USE_ASMONEnable x64/RISC-V assembly
SECP256K1_RISCV_FAST_REDUCTIONONFast modular reduction (RISC-V)
SECP256K1_RISCV_USE_VECTORONRVV vector extension

CUDA

MacroDefaultDescription
SECP256K1_CUDA_USE_HYBRID_MUL132-bit hybrid multiplication (~10% faster)
SECP256K1_CUDA_USE_MONTGOMERY0Montgomery domain arithmetic
SECP256K1_CUDA_LIMBS_320Use 8x32-bit limbs (experimental)

Platform Support

PlatformAssemblySIMDStatus
x86-64 Linux/Windows/macOSBMI2/ADXAVX2[OK] Production
RISC-V 64RV64GCRVV 1.0[OK] Production
ARM64 (Android/iOS/macOS)MUL/UMULHNEON[OK] Production
CUDA (sm_75+)PTX--[OK] Production
ROCm/HIP (AMD)Portable--[OK] CI
OpenCL 3.0PTX--[OK] Production
WebAssemblyPortable--[OK] Production
ESP32-S3 / ESP32Portable--[OK] Tested
STM32F103 (Cortex-M3)UMULL--[OK] Tested

libsecp256k1 Shim Extensions

Functions in compat/libsecp256k1_shim/ that extend the libsecp256k1 API with shim-specific lifecycle helpers. These have no counterpart in upstream libsecp256k1.

MuSig2 Session Cleanup

/* Explicitly release the shim's internal state for a MuSig2 session that was
 * abandoned before secp256k1_musig_partial_sig_agg (e.g. on protocol abort).
 * Calling on a never-initialised or already-cleared cache is a no-op.
 * Not needed for completed sessions — partial_sig_agg auto-cleans. */
void secp256k1_musig_keyagg_cache_clear(secp256k1_musig_keyagg_cache *keyagg_cache);

When to call: Only when a MuSig2 signing session is aborted before the final secp256k1_musig_partial_sig_agg call. Completed sessions clean up automatically.


Version

UltrafastSecp256k1 v4.5.0

For more information, see the README or GitHub repository.