scure-btc-signer

repository·main·Indexed 42 days ago

https://github.com/paulmillr/scure-btc-signer

An audited, minimal TypeScript library for creating, signing, and decoding Bitcoin transactions. Version 2.4.1 supports SegWit (P2WPKH, P2WSH), Taproot (P2TR), classic multisig (P2MS), MuSig2, and PSBT (v0 and v2). It includes utilities for UTXO selection and an EsploraProvider for fetching network data such as balances and transaction history.

Tokens
29K
Snippets
103
Records
122
Agent score
53%

What's inside scure-btc-signer

  1. Setup a BTC full node with HTTP API

    main

    To construct transactions and fetch UTXOs for arbitrary addresses, a standard Bitcoin Core node is insufficient. You must deploy an Electrum server to create the necessary indexes (approx. 1TB).

    There are two recommended approaches:

    1. Blockstream fork of Electrs (Recommended): Exposes a native HTTP API with CORS headers, making it compatible with web browsers and the @scure/btc-signer EsploraProvider.
    2. Original Electrs (romanz): Uses TCP-based JSON-RPC. To use this with web clients, you must run a TCP-to-HTTP proxy (provided in this repository as test/proxy.ts) to bridge the TCP connection to an HTTP endpoint.
  2. Perform UTXO selection for payments

    main

    UTXO selection chooses which unspent outputs to use as inputs for a transaction. Use btc.selectUTXO to automate this process.

    Key Options:

    • changeAddress: The address where remaining funds should be sent (required).
    • feePerByte: The fee rate in satoshis per vByte (required).
    • bip69: Enables lexicographical indexing of inputs and outputs.
    • createTx: If true, returns a Transaction object ready for signing.
    • filterTaproot: An array of x-only public keys. Use this to prevent the selector from choosing Taproot UTXOs that the current wallet cannot spend.

    Strategies: Selection behavior is determined by a strategy string. The recommended strategy for most cases is exactBiggest/accumSmallest (or default).

    Common strategies include:

    • default: Good for privacy.
    • all: Consolidates all coins to the change address.
    • accum: Accumulates inputs until the target value (+fees) is reached.
    • exact: Accumulates inputs until the target value is matched without going over (within a threshold).

    selectUTXO returns undefined if there are insufficient funds.

    import * as btc from '@scure/btc-signer';
    
    const selected = btc.selectUTXO(utxos, outputs, 'default', {
      changeAddress: 'bcrt1pea3850rzre54e53eh7suwmrwc66un6nmu9npd7eqrhd6g4lh8uqsxcxln8',
      feePerByte: 2n,
      bip69: true,
      createTx: true,
      network: regtest,
      filterTaproot: [myPublicKey],
    });
    
    if (selected) {
      const { tx } = selected;
      tx.sign(privKey);
      tx.finalize();
    }
  3. Install @scure/btc-signer

    main

    You can install the library using npm or Deno.

    For Node.js/npm:

    npm install @scure/btc-signer

    For Deno:

    deno add jsr:@scure/btc-signer

    Note for Deno users: Ensure you use the npm specifier. Note for React Native users: You may need to provide a polyfill for crypto.getRandomValues (e.g., react-native-get-random-values).

  4. Install and run Blockstream Electrs

    main

    This method installs the Blockstream fork of electrs, which provides an HTTP API.

    Installation Steps:

    1. Install dependencies (build-essential, cmake, protobuf-compiler, etc.).
    2. Create a system user electrs and group bitcoin.
    3. Clone https://github.com/Blockstream/electrs.git.
    4. Build using cargo build --locked --release --bin electrs.
    5. Install the binary to /usr/local/bin/electrs-blockstream.

    Key Configuration Flags:

    • --http-addr: The address the HTTP API listens on (e.g., 127.0.0.1:3000).
    • --cors '*': Enables CORS for browser-based tools.
    • --utxos-limit 10000: Increases the per-address UTXO lookup ceiling.
    • --electrum-txs-limit 10000: Increases the raw Electrum history ceiling.
    • --jsonrpc-import: Enables JSON-RPC import.
    # Build and install command snippet
    cd /srv/electrs/blockstream
    cargo build --locked --release --bin electrs
    sudo install -m 0755 target/release/electrs /usr/local/bin/electrs-blockstream
    
    # Execution example
    /usr/local/bin/electrs-blockstream \
      --network bitcoin \
      --daemon-dir /var/lib/bitcoin \
      --blocks-dir /var/lib/bitcoin/blocks \
      --cookie /var/lib/bitcoin/.cookie \
      --daemon-rpc-addr 127.0.0.1:8332 \
      --db-dir /var/lib/electrs-blockstream \
      --electrum-rpc-addr 127.0.0.1:51001 \
      --http-addr 127.0.0.1:3000 \
      --cors '*' \
      --utxos-limit 10000 \
      --electrum-txs-limit 10000 \
      --monitoring-addr 127.0.0.1:4324 \
      --jsonrpc-import
  5. Sign and broadcast a Bitcoin transaction

    main

    To spend funds, fetch UTXOs using net.unspent, select appropriate inputs using btc.selectUTXO, sign the transaction with the private key, and finalize it before broadcasting via net.sendTx.

    async function signSpend(privKey: Uint8Array, to: string, amount: bigint) {
      const spend = btc.p2wpkh(pubECDSA(privKey));
      const unspent = await net.unspent(spend.address!);
      const feePerByte = await net.fee(2);
      
      const selected = btc.selectUTXO(unspent.utxo, [{ address: to, amount }], 'default', {
        feePerByte,
        changeAddress: spend.address!,
      });
    
      if (!selected) throw new Error(`not enough funds for ${spend.address}`);
      
      const { tx } = selected;
      tx.sign(privKey);
      tx.finalize();
      return { txid: tx.id, raw: tx.hex };
    }
  6. Verify supply chain security and provenance

    main

    To ensure the integrity of the package, users should follow these security practices:

    1. Verify Commit Signatures: Ensure all commits are signed with PGP keys.
    2. Verify Provenance: Check the GitHub CI build provenance logs.
    3. Verify Single-file Builds: Use the GitHub CLI to verify the build:
      gh attestation verify --owner paulmillr scure-btc-signer.js
    4. Minimize Dependencies: The package uses a minimal set of audited dependencies (noble-hashes, noble-curves, scure-base, micro-packed).
  7. Configure Bitcoin Core for Electrum Server

    main

    For an Electrum server to function, your Bitcoin Core node must meet these requirements:

    • Fully synced.
    • Not pruned (prune=0).
    • txindex=1 enabled.
    • TCP-based RPC API enabled.

    Use the following bitcoin.conf settings:

    server=1
    txindex=1
    prune=0
    listen=1
    bind=127.0.0.1:8333
    rpcbind=127.0.0.1
    rpcallowip=127.0.0.1
    whitelist=download@127.0.0.1
    maxuploadtarget=512M
    maxconnections=14
    server=1
    txindex=1
    prune=0
    listen=1
    bind=127.0.0.1:8333
    rpcbind=127.0.0.1
    pcallowip=127.0.0.1
    whitelist=download@127.0.0.1
    maxuploadtarget=512M
    maxconnections=14
  8. Implement MuSig2 multi-signatures (BIP-327)

    main

    MuSig2 implementation is available in @scure/btc-signer/musig2.js. The process involves several rounds:

    1. Key Generation: Each signer generates a secret key and derives an individual public key using musig2.IndividualPubkey(secretKey).
    2. Key Aggregation: Signers share public keys. Use musig2.sortKeys(publicKeys) to sort them, then musig2.keyAggregate(sortedKeys) to create the aggregate key. The final X-only aggregate public key is extracted via musig2.keyAggExport(...).
    3. Nonce Generation (Round 1): Each signer generates nonces using musig2.nonceGen(individualPubKey, secretKey, aggregatePubKey, msg). This returns both a secret (to be kept private) and a public nonce.
    4. Nonce Aggregation: Signers share public nonces. Create a musig2.Session by passing the aggregated public nonces via musig2.nonceAggregate(...), the sorted public keys, and the message.
    5. Partial Signature (Round 2): Each signer generates a partial signature using session.sign(nonceSecret, secretKey).
    6. Aggregation & Verification: Aggregate partial signatures using session.partialSigAgg(partialSignatures) and verify the final signature using standard Schnorr verification.

    CRITICAL SECURITY WARNING: Every MuSig2 secret nonce must be used for exactly one partial signature. Reusing a nonce in distinct sessions can reveal the signer's secret key. Session.sign() only zeroes the specific Uint8Array passed to it; you must ensure clones or database records of the nonce are not reused.

    import * as btc from '@scure/btc-signer';
    import * as musig2 from '@scure/btc-signer/musig2.js';
    import { schnorr } from '@noble/curves/secp256k1.js';
    
    // 1. Key Generation
    const aliceSecretKey = btc.utils.randomPrivateKeyBytes();
    const alicePublicKey = musig2.IndividualPubkey(aliceSecretKey);
    const bobSecretKey = btc.utils.randomPrivateKeyBytes();
    const bobPublicKey = musig2.IndividualPubkey(bobSecretKey);
    
    // 2. Key Aggregation
    const individualPublicKeys = [alicePublicKey, bobPublicKey];
    const sortedPublicKeys = musig2.sortKeys(individualPublicKeys);
    const aggregatePublicKey = musig2.keyAggExport(musig2.keyAggregate(sortedPublicKeys));
    
    // 3. Nonce Generation
    const msg = new Uint8Array(32).fill(5);
    const aliceNonces = musig2.nonceGen(alicePublicKey, aliceSecretKey, aggregatePublicKey, msg);
    const bobNonces = musig2.nonceGen(bobPublicKey, bobSecretKey, aggregatePublicKey, msg);
    
    // 4. Session & Nonce Aggregation
    const session = new musig2.Session(
      musig2.nonceAggregate([aliceNonces.public, bobNonces.public]),
      sortedPublicKeys,
      msg
    );
    
    // 5. Partial Signature
    const alicePartialSignature = session.sign(aliceNonces.secret, aliceSecretKey);
    const bobPartialSignature = session.sign(bobNonces.secret, bobSecretKey);
    
    // 6. Aggregation
    const finalSignature = session.partialSigAgg([alicePartialSignature, bobPartialSignature]);
    
    // 7. Verification
    schnorr.verify(finalSignature, msg, aggregatePublicKey);
  9. Define a Signer as `Bytes` or `HDKey`

    main
    The Signer type defines the acceptable sources for signing transaction inputs. It can be either raw Bytes (a private key) or an object implementing the HDKey interface (containing publicKey, privateKey, fingerprint, derive, deriveChild, and sign methods).
  10. How MuSig2 signing works in scure-btc-signer

    main

    This module provides primitives for the MuSig2 multi-signature protocol as defined in BIP-327. It is not a full protocol implementation; it does not orchestrate the exchange of nonces or partial signatures. Developers must implement the communication layer (nonce/partial signature exchange) themselves.

    Security Warning: A secret nonce MUST be used for exactly one partial signature. Reusing the same nonce scalars in distinct sessions can reveal the signer's secret key. The Session.sign() method zeroes the Uint8Array passed to it, but it cannot erase copies, serialized values, or database records. Stateful integrations should maintain an authoritative nonce record and consume it atomically.

  11. Understand TxStatus and TxInfo data structures

    main

    The provider returns structured metadata for transactions and blocks.

    TxStatus indicates if a transaction is confirmed and provides the block height, blockHash, and timestamp (in milliseconds) if confirmed.

    TxInfo provides full metadata including txid, version, lockTime, size, weight, fee (in satoshis), inputs, outputs, and status.

    export type TxStatus = {
      confirmed: boolean;
      block?: number;
      blockHash?: string;
      timestamp?: number;
    };
    
    export type TxInfo = {
      txid: string;
      version: number;
      lockTime: number;
      size: number;
      weight: number;
      fee: bigint;
      inputs: TxInput[];
      outputs: TxOutput[];
      status: TxStatus;
      raw: string;
    };