Programming Bitcoin

Learn how to use Lit to trigger transactions on Bitcoin.

Programming Bitcoin

Overview

Introduction to Bitcoin's Limitations

Bitcoin, the world's first and largest blockchain, is renowned for its security and decentralization but lacks native programmability. This limitation hinders the development of complex decentralized applications directly on the Bitcoin network.

The absence of programmability restricts Bitcoin's ecosystem to simple value transfers. Enabling programmability would allow for the creation of smart contracts, decentralized applications, and innovative real-world solutions. Lit Protocol's Programmable Key Pairs (PKPs) offer a solution by bringing programmability to Bitcoin.

Triggering a Bitcoin (P2PKH) Transaction with Lit Protocol

With Lit PKPs, you can create programmable, policy-controlled key pairs that interact with the Bitcoin network, enabling functionalities that were previously unattainable. This guide demonstrates how a P2PKH transaction can be prepared and signed using a Lit Protocol PKP on Bitcoin. Note that you can use Lit to sign different Bitcoin transaction types as well (i.e. P2SH, P2WPKH), but the required setup will differ from the following example.

Background

Programmable Key Pairs (PKPs)

PKPs are threshold public/private key pairs created by the Lit network using Distributed Key Generation (DKG). Each Lit node generates a share of the private key, and more than two-thirds of these shares must be collected to execute a given action (i.e. signing a transaction).

PKPs can be programmed with custom logic to automate on-chain actions and communicate across different blockchains to enable chain abstraction. They can be used to create non-custodial wallets, automate transactions, and build complex decentralized applications.

Your Multi-Chain Account

Since generating signatures with Lit is handled off-chain, one of the significant uses of PKPs is serving as a wallet across multiple blockchains, including Bitcoin, Ethereum, Cosmos, and others. This means you aren't restricted to deploying on a single chain and instead can use Lit to build apps and experiences that span across the entire web3 ecosystem.

Lit Actions: Making Keys Programmable

Lit Actions are custom JavaScript programs that can be used to dictate the signing logic for PKPs to follow. For example, a simple Lit Action may take in an input via API (like a blockchain event) and produce a signature (using a PKP) when a given condition is met.

Use Cases

Integrating PKPs with Bitcoin transactions via Lit Protocol unlocks a wealth of possibilities for developers and users alike. One of the most compelling use cases is the automation of transactions based on specific conditions or events. For example, you can program a PKP to authorize and sign a Bitcoin transaction only when certain state is true on a smart contract platform—such as a 'liquidate' function being called. Block height is the example demonstrated in this guide. This conditional execution can be extended to more real-world scenarios like releasing payments upon contract fulfillment, automating payroll when work milestones are achieved, or triggering transactions based on market conditions, like dollar cost averaging.

Another significant use case is the creation of multi-chain decentralized applications (dApps) and wallets. Since PKPs can interact with multiple blockchains—including Bitcoin, Ethereum, and Cosmos—you can build applications and chain abstraction protocols that operate seamlessly across different networks. This enables users to manage assets on multiple chains using a single PKP as well as use programmable keys for orchestration. The result is   simplifying the user experience and fostering interoperability in the blockchain  ecosystem.

Additionally, the programmable nature of PKPs through Lit Actions allows for enhanced security features, such as multi-signature approvals and compliance checks, making them ideal for corporate treasury management, decentralized finance (DeFi) applications, and other scenarios requiring robust control over transaction authorization

Example: Conditional Bitcoin Transaction Signing

To illustrate the capabilities of PKPs and Lit Actions, we will demonstrate the following example:

  • Objective: Use a PKP to sign a Bitcoin transaction.
  • Condition: The transaction will only be signed when the current Bitcoin block height number is odd.

Bitcoin Transaction Types

In Bitcoin's long lifetime, different transaction algorithms have emerged with improvements being made at every step. As mentioned previously, this example will use the legacy P2PKH (Pay-to-PubKey-Hash) transaction format.

P2PKH - Legacy Transactions

  • P2PKH (Pay-to-PubKey-Hash) is the most common type of Bitcoin transaction. The Bitcoin is sent to the recipient's Bitcoin address, which is derived from their public key. The recipient must provide a signature that matches the public key hash in order to spend the Bitcoin.
  • In this transaction type, the sender provides a script with the recipient's hashed public key, and the recipient must sign the transaction to unlock the funds.
  • Addresses of this type start with a 1.

P2WPKH - SegWit Transactions

  • P2WPKH (Pay-to-Witness-Pubkey-Hash) is an upgrade from the legacy P2PKH transactions. This type introduces a Segregated Witness (SegWit), separating the signature data (witness data) from the main transaction data, functionally reducing the transaction size and therefore lowering the transaction fee.
  • This transaction type benefits from reduced transaction fees, enhanced privacy with Bench32, and enables advanced protocol developments (i.e. Lightning Network, Taproot). This is now the recommended transaction type for Bitcoin.
  • Addresses of this type start with a bc1.

P2SH

  • P2SH (Pay-to-Script-Hash) transactions send the Bitcoin to a script hash instead of a public key hash. The recipient must provide a script that matches the hash along with the necessary data to satisfy the conditions specified in the script.
  • This transaction type is commonly used for multi-signature wallets, requiring multiple keys to sign before the script hashes match.
  • Addresses of this type start with a 3.

Bitcoin has many transaction types, so you may also be interested in others, like Nested SegWit or P2TR (Pay-to-Taproot). More information on these can be found here.

Guide: Using a PKP to Sign a Transaction on Bitcoin

Prerequisites

Before compiling and trying this example out, there are a few things you'll need to prepare.

  1. You will need to mint a PKP. The fastest way to do this is through the Lit Explorer. Please make sure you mint the PKP on the same network you run the example on.
  2. An Ethereum wallet. Please make sure that this wallet was used to mint the PKP, and therefore has ownership of it.
  3. The PKP must have a UTXO. Without a UTXO, the PKP will be unable to send any Bitcoin and this example will fail. To find the Base58 address of the PKP public key, you can visit the code in the Deriving a BTC Base58 Address from the Public Key part of this example. This code will compute the P2PKH address of your public key. You can then send Bitcoin to the output address.

Complete Code Example

After making sure you have met the prerequisites, you can choose to visit the complete code example here. That being said, we strongly recommend you read through the following explanation.

Explaining the Implementation

ENVs

PKP_PUBLIC_KEY=
ETHEREUM_PRIVATE_KEY=
BTC_DESTINATION_ADDRESS=
BROADCAST_URL=https://mempool.space/api/tx
LIT_NETWORK=
LIT_CAPACITY_CREDIT_TOKEN_ID=
  1. REQUIRED PKP_PUBLIC_KEY: Public key of the PKP. It must be owned by the account associated with the given Ethereum private key. Do not include the 0x.
  2. REQUIRED ETHEREUM_PRIVATE_KEY: The Ethereum account that will be used to pay for usage of the Lit network and owns the PKP.
  3. REQUIRED BTC_DESTINATION_ADDRESS: The Bitcoin address to receive the Bitcoin sent from the PKP.
  4. GIVEN BROADCAST_URL: The endpoint URL that the transaction hex will be submitted to. This is used to broadcast the transaction.
  5. OPTIONAL LIT_NETWORK: The Lit network to execute this example on. If one is not provided, the example will default to the Datil network. If you wish to use a different network, say datil-test, then set the ENV to datil-test.
  6. OPTIONAL LIT_CAPACITY_CREDIT_TOKEN_ID: The capacity credit that will be used to create a capacityDelegationAuthSig and pay for usage of the Lit network. If not provided, a new one will need to be minted. This requires the Ethereum account to have Lit tstLPX tokens.

Packages

Lit Packages

import { LitNodeClient } from "@lit-protocol/lit-node-client";
import { LitNetwork, LIT_RPC } from "@lit-protocol/constants";
import {
  createSiweMessageWithRecaps,
  generateAuthSig,
  LitAbility,
  LitActionResource,
  LitPKPResource,
} from "@lit-protocol/auth-helpers";
import { LitContracts } from "@lit-protocol/contracts-sdk";
import { LIT_NETWORKS_KEYS } from "@lit-protocol/types";

Other Packages

import BN from "bn.js";
import mempoolJS from "@mempool/mempool.js";
import fetch from 'node-fetch';
import elliptic from 'elliptic';
import * as bitcoin from "bitcoinjs-lib";
import * as ethers from "ethers";
import * as ecc from "tiny-secp256k1";
import * as bip66 from "bip66";
import * as crypto from 'crypto';

Constants

Here we inject the ENVs into our file, create a new ethers.Wallet, initialize instances of the elliptic curve cryptography (ECC), and set the bitcoinjs-lib to use the cryptography of secp256k1.

const PKP_PUBLIC_KEY = process.env["PKP_PUBLIC_KEY"]!;
const ETHEREUM_PRIVATE_KEY = process.env["ETHEREUM_PRIVATE_KEY"]!;
const BTC_DESTINATION_ADDRESS = process.env["BTC_DESTINATION_ADDRESS"]!;
const BROADCAST_URL = process.env["BROADCAST_URL"];
const LIT_NETWORK = process.env["LIT_NETWORK"] as LIT_NETWORKS_KEYS || LitNetwork.Datil;
const LIT_CAPACITY_CREDIT_TOKEN_ID = process.env["LIT_CAPACITY_CREDIT_TOKEN_ID"];

const ethersWallet = new ethers.Wallet(ETHEREUM_PRIVATE_KEY,
    new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE));

const address = ethersWallet.address;

const EC = elliptic.ec;
bitcoin.initEccLib(ecc);

Lit Implementation

Lit Connections

Using Lit requires a connection to the Lit network, which can be established using LitNodeClient.

In case a LIT_CAPACITY_CREDIT_TOKEN_ID is not provided, we also connect to LitContracts. This can be used to mint a new capacity credit.

let litNodeClient: LitNodeClient | undefined;
litNodeClient = new LitNodeClient({
  litNetwork: LIT_NETWORK,
  debug: false,
});
await litNodeClient.connect();

const litContracts = new LitContracts({
  signer: ethersWallet,
  network: LIT_NETWORK,
  debug: false,
});
await litContracts.connect();

Interacting with the Lit Network

The Lit network requires session signatures to authenticate your current session and define the session capabilities.

let capacityTokenId = LIT_CAPACITY_CREDIT_TOKEN_ID;
if (!capacityTokenId) {
  console.log("🔄 No Capacity Credit provided, minting a new one...");
  const mintResult = await litContracts.mintCapacityCreditsNFT({
    requestsPerKilosecond: 10,
    daysUntilUTCMidnightExpiration: 1,
  });
  capacityTokenId = mintResult.capacityTokenIdStr;
  console.log(`✅ Minted new Capacity Credit with ID: ${capacityTokenId}`);
} else {
  console.log(
    `ℹ️  Using provided Capacity Credit with ID: ${LIT_CAPACITY_CREDIT_TOKEN_ID}`
  );
}

console.log("🔄 Creating capacityDelegationAuthSig...");
const { capacityDelegationAuthSig } =
  await litNodeClient.createCapacityDelegationAuthSig({
    dAppOwnerWallet: ethersWallet,
    capacityTokenId,
    delegateeAddresses: [address],
    uses: "1",
  });
console.log("✅ Capacity Delegation Auth Sig created");

console.log("🔄 Getting Session Signatures...");
const sessionSigs = await litNodeClient.getSessionSigs({
  chain: "ethereum",
  expiration: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(), // 24 hours
  capabilityAuthSigs: [capacityDelegationAuthSig],
  resourceAbilityRequests: [
    {
      resource: new LitPKPResource("*"),
      ability: LitAbility.PKPSigning,
    },
    {
      resource: new LitActionResource("*"),
      ability: LitAbility.LitActionExecution,
    },
  ],
  authNeededCallback: async ({
    resourceAbilityRequests,
    expiration,
    uri,
  }) => {
    const toSign = await createSiweMessageWithRecaps({
      uri: uri!,
      expiration: expiration!,
      resources: resourceAbilityRequests!,
      walletAddress: address,
      nonce: await litNodeClient!.getLatestBlockhash(),
      litNodeClient,
    });

    return await generateAuthSig({
      signer: ethersWallet,
      toSign,
    });
  },
});
console.log("✅ Got Session Signatures");
  1. If a LIT_CAPACITY_CREDIT_TOKEN_ID is not provided, we mint a new one and use it to create a capacityDelegationAuthSig. This AuthSig is used to pay for usage of the test and production Lit networks.
  2. We then generate session signatures, specifying that our session has the ability to use PKPs for signing and to execute Lit Actions.

Implementing Bitcoin P2PKH

Deriving a BTC Base58 Address from the Public Key

const pubKeyBuffer = Buffer.from(PKP_PUBLIC_KEY, "hex");
const sha256Hash = crypto.createHash('sha256').update(pubKeyBuffer).digest();
const ripemd160Hash = crypto.createHash('ripemd160').update(sha256Hash).digest();
const btcAddress = bitcoin.address.toBase58Check(ripemd160Hash, 0x00);

A Bitcoin address is derived through a hashing algorithm which combines SHA-256 and RIPEMD160. The algorithm first computes the SHA-256 hash of the public key and then the RIPEMD160 hash of the result. We can then derive the Base58Check encoding, adding the mainnet network prefix 0x00.

  1. Take the uncompressed public key, convert it to a Buffer. This is required for the following hashing algorithms. (65 bytes, starting with byte 04)
    1. Expected format: 04 ea ec 6d 85 f9 68 ea e2 4c 0f e0 34 ... 52 more bytes
  2. Compute the SHA-256 hash of the public key Buffer. (32 bytes)
  3. Expected format: bd 09 96 cf f7 5c 36 cf d3 d1 32 6a a9 ... 19 more bytes
  4. Compute the RIPEMD160 hash of the SHA-256 hash. This is the PKH used in P2PKH (Pay-to-PubKey-Hash) addresses. (20 bytes)
    1. Expected format: 8e 12 20 fa 50 f5 2a ef a2 ee 9b 5e ea ca ae 51 ea ae 5a d7
  5. Derive the Base58Check encoding. This adds the defined network prefix 0x00 (Bitcoin mainnet), computes the checksum by performing a double SHA-256 hash on the network-prefixed public key, and returns the network-prefixed public key hash with the checksum appended.
    1. Expected format: 1DxCfrSR4LkTxAamyEoZMHZbT9JpfJqaVj

Fetching the UTXO Information

Here we create an instance of mempoolJS to interact with the Bitcoin blockchain.

const { bitcoin: { addresses, transactions } } = mempoolJS({
  hostname: "mempool.space",
  network: "mainnet",
});

const addressTxsUtxos = await addresses.getAddressTxsUtxo({
  address: btcAddress,
});

const utxo = addressTxsUtxos[0];
const amountToSend = BigInt(utxo.value - 500);
  1. Initialize the mempoolJs library with the hostname and the Bitcoin mainnet as the network.
  2. Fetch the UTXO (Unspent Transaction Output) information for our Bitcoin address.
  3. Specify the first UTXO of the address.
  4. Define the amount to send our destination address as the entire value of the UTXO and subtract 500 Satoshis as the network fee. This ensures our transaction doesn't encounter a dust error and spends the entire UTXO amount.
const utxoTxDetails = await transactions.getTx({ txid: utxo.txid });
const scriptPubKey = utxoTxDetails.vout[utxo.vout].scriptpubkey;

We then derive the scriptpubkey using the transaction details of our UTXO. The scriptPubKey is a vital part of Bitcoin transactions, as it defines how a Bitcoin can be spent. Scripts typically ensure that in order to spend the Bitcoin, the spender must be able to produce a signature belonging to the defined public key.

Building the Transaction

Now that we have collected the necessary information, the next step is building the Bitcoin transaction for our PKP to sign.

const tx = new bitcoin.Transaction();
tx.version = 2;

tx.addInput(Buffer.from(utxo.txid, "hex").reverse(), utxo.vout);
const network = bitcoin.networks.bitcoin;
tx.addOutput(bitcoin.address.toOutputScript(BTC_DESTINATION_ADDRESS, network),
             amountToSend);
const scriptPubKeyBuffer = Buffer.from(scriptPubKey, "hex");
    
const sighash = tx.hashForSignature(
    0,
    bitcoin.script.compile(scriptPubKeyBuffer),
    bitcoin.Transaction.SIGHASH_ALL
);
  1. Initialize a Transaction from the bitcoinjs-lib library.
  2. Set the Transaction version as 2.
  3. Define our network as the Bitcoin mainnet.
  4. Convert the scriptPubKey from a hexadecimal string to a Buffer.
  5. Generate a hash of the transaction to be signed with the bitcoin.Transaction.hashForSignature() method. We provide the index of the input being signed (0), the previous output script of our UTXO, and we use SIGHASH_ALL to specify the signing of all of the inputs and outputs in the transaction.
    1. Expected format: [ 105, 95, 131, 73, 35, 152, 246, 141, 140, 71, 143, 33, 101, 234, 126, 30, 87, 96, 102, 107, 158, 57, 183, 233, 159, 35, 212, 14, 9, 83, 182, 95]

Add an output to our transaction. This involves using the toOutputScript, which converts the destination address to its corresponding output script. We also specify the amount to send the destination address.

"outs": [
    {
      "n": 0,
      "script": {
        "addresses": [
          "34tpDpkBjDZD8tSSfijJjbGS7MzLQKwBxc"
        ],
        "asm": "OP_HASH160 232399ba7086d1f345f69de5c1ca476c031a16f5 OP_EQUAL",
        "hex": "a914232399ba7086d1f345f69de5c1ca476c031a16f587"
      },
      "value": 3419
    }
  ]

Add an input to the transaction. This involves first converting the hex string to a Buffer. The Buffer is then reversed. This is because it is currently in big-endian format, but Bitcoin internally uses little-endian format. We then specify the utxo.vout as the output from the previous transaction being spent by the input in the current transaction.

"ins": [
   {
     "n": 0,
     "script": {
       "asm": "3045022100b97d65eb48e780cae23d4b84ec739f0a7e2de8788cb3df6c00d84fdad4f8c93f022069d212af48b88c4441e3cce00c3b37afb3213a00f76dd7251d4b485db809444a01 eaec6d85f968eae24c0fe034ae1626cca3554a1c57ccaf7572978a2e17e3b9fdcc52eb135616efd50dbebbdeb2c7373f6e571b9ce7b61d80b20144de3b92602c",
       "hex": "483045022100b97d65eb48e780cae23d4b84ec739f0a7e2de8788cb3df6c00d84fdad4f8c93f022069d212af48b88c4441e3cce00c3b37afb3213a00f76dd7251d4b485db809444a0140eaec6d85f968eae24c0fe034ae1626cca3554a1c57ccaf7572978a2e17e3b9fdcc52eb135616efd50dbebbdeb2c7373f6e571b9ce7b61d80b20144de3b92602c"
     },
     "sequence": 4294967295,
     "txid": "6b727883f87ee12a5d0009d61d7b64db096fbd725f9e7e973080816b96edd4bd",
     "witness": []
   }
]

Signing the Transaction with the PKP

To sign the transaction with the PKP, we will execute a Lit Action. In this example, the transaction will only be signed if the current block height on the Bitcoin mainnet is odd.

// Filename: litAction.ts
// @ts-nocheck

const _litActionCode = async () => {
  try {
    const url = "https://mempool.space/api/blocks/tip/height";
    const resp = await fetch(url).then((response) => response.json());

    if (Number(resp) % 2 === 0 ){
      Lit.Actions.setResponse({ response: "Block height is even! Don't sign!"});
      console.log("Current block height:", resp);
      return;
    }

    const sigShare = await LitActions.signEcdsa({ toSign, publicKey, sigName: 'btcSignature' });
    Lit.Actions.setResponse({ response: 'true' });
  } catch (error) {
    Lit.Actions.setResponse({ response: error.message });
  }
};

export const litActionCode = `(${_litActionCode.toString()})();`;
  1. Define the endpoint URL for the HTTP request to fetch the current Bitcoin block height.
  2. In this example, if the block height is even, we will refuse to sign and terminate the Lit Action execution.
  3. If the block height is odd, we will sign the data in the toSign variable using our PKP and set the response as true.

Executing the Lit Action

This Lit Action is executed in our main file with the following method, where we designate the sighash as the content our PKP will sign.

import { litActionCode } from "./litAction";

const litActionResponse = await litNodeClient.executeJs({
  code: litActionCode,
  sessionSigs,
  jsParams: {
    toSign: sighash,
    publicKey: PKP_PUBLIC_KEY,
  },
});

if (litActionResponse.response === "Block height is even! Don't sign!") {
    return "Block height was even; transaction not signed.";
}

Upon successful signing, the litActionResponse variable will appear similar to:

litActionResponse: {
  claims: {},
  signatures: {
    btcSignature: {
      r: 'd50b9c39e72bf0167d8ca769f4d3dcebf985d4330a108cdcbe407d9b88acb5e2',
      s: '62d25cb024bf2eaa52bbf5fd2fbd8e58e964d9724be824c56f1c3204e7fd862c',
      recid: 1,
      signature: '0xd50b9c39e72bf0167d8ca769f4d3dcebf985d4330a108cdcbe407d9b88acb5e262d25cb024bf2eaa52bbf5fd2fbd8e58e964d9724be824c56f1c3204e7fd862c1c',
      publicKey: '04EAEC6D85F968EAE24C0FE034AE1626CCA3554A1C57CCAF7572978A2E17E3B9FDCC52EB135616EFD50DBEBBDEB2C7373F6E571B9CE7B61D80B20144DE3B92602C',
      dataSigned: '695F83492398F68D8C478F2165EA7E1E5760666B9E39B7E99F23D40E0953B65F'
    }
  },
  response: true,
  logs: ''
}

If the block height was even, the transaction was not signed. We are informed of this through the Lit Action response.

Converting from an Ethereum-like Signature to Bitcoin-like

Now that our PKP has successfully signed the transaction, we need to broadcast it to the Bitcoin mainnet. The issue with our current signature is that the formatting is that of an ECDSA signature, which contains the 32-byte components r, s, and signature. We need to use these components to construct a signature that is acceptable for broadcasting on the Bitcoin blockchain.

let r = Buffer.from(litActionResponse.signatures.btcSignature.r, "hex");
let s = Buffer.from(litActionResponse.signatures.btcSignature.s, "hex");
let rBN = new BN(r);
let sBN = new BN(s);
    
const secp256k1 = new EC('secp256k1');
const n = secp256k1.curve.n;

if (sBN.cmp(n.divn(2)) === 1) {
  sBN = n.sub(sBN);
}

r = rBN.toArrayLike(Buffer, 'be', 32);
s = sBN.toArrayLike(Buffer, 'be', 32);

function ensurePositive(buffer: Buffer) {
  if (buffer[0] & 0x80) {
    const newBuffer = Buffer.alloc(buffer.length + 1);
    newBuffer[0] = 0x00;
    buffer.copy(newBuffer, 1);
    return newBuffer;
  }
  return buffer;
}

r = ensurePositive(r);
s = ensurePositive(s);

let derSignature;
try {
  derSignature = bip66.encode(r, s);
} catch (error) {
  console.error('Error during DER encoding:', error);
  throw error;
}
  1. Extract the r and s values from the btcSignature, covnvert them from a hexadecimal string to a Buffer.
  2. Create an instance of the secp256k1 elliptic curve, which is the elliptic curve used in Bitcoin's public key cryptography..
  3. Extract the number of points, or the order (n), on the elliptic curve.
  4. Implement low-S normalization, which ensures that s is less than half of the order. Bitcoin requires this operation to prevent transaction malleability.
  5. Convert the r and s values from a BigNumber into a 32-byte Buffer in big-endian order. This is done so next we can ensure the positivity of the r and s values.
  6. We ensure positivity of the r and s values using the ensurePositive helper function. This function:
    • Checks if the most significant bit (MSB) of the first byte is set (i.e. the number is negative).
    • If so, we construct a new buffer one byte longer than the original.
    • We then prepend 0x00 to ensure the Buffer is positive.
    • The original buffer is copied into the new buffer starting at index 1. This ensures that only the MSB has changed.
    • If the MSB was not set from the beginning, we can return the original buffer.
  7. After ensuring positivity of the r and s values, we can format the signature for the transaction. This involves encoding using the BIP66 (Bitcoin Improvement Proposal 66), which is a standard for encoding ECDSA signatures in Bitcoin. It defines a strict DER (Distinguished Encoding Rules) encoding.

Defining the Input Script

const signatureWithHashType = Buffer.concat([
  derSignature,
  Buffer.from([bitcoin.Transaction.SIGHASH_ALL]),
]);

const scriptSig = bitcoin.script.compile([
  signatureWithHashType,
  Buffer.from(PKP_PUBLIC_KEY, "hex"),
]);

tx.setInputScript(0, scriptSig);
const txHex = tx.toHex();
  1. Append the Bitcoin-formatted signature with the hash type SIGHASH_ALL. Bitcoin requires that the hash type used during signing be appended to the signature. This informs the network how the transaction was hashed and what parts of it are covered by the signature.
  2. We must then compile the scriptSig. The sigScript provides the necessary data to unlock and spend a Bitcoin UTXO. For a P2PKH transaction, the sigScript requires the signature with the SIGHASH type as well as the public key.
  3. We then attach the sigScript to the transaction input, also defining the index as 0.
  4. Finally, we convert the convert the transaction to a hex value. This is what is broadcasted to the Bitcoin blockchain.

Broadcasting the Transaction

Our transaction is finally ready to be broadcasted, and our Bitcoin sent to the destination address. We can do this by invoking the mempoolJS REST API and sending the https://mempool.space/api/tx endpoint a HTTP request containing our transaction hex value.

const broadcastTransaction = async (txHex: string) => {
  try {
    const response = await fetch(BROADCAST_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'text/plain',
      },
      body: txHex,
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Error broadcasting transaction: ${errorText}`);
    }

    const txid = await response.text();
    console.log(`Transaction broadcasted successfully. TXID: ${txid}`);
    return txid;
  } catch (error: any) {
    console.error(error.message);
  }
};

broadcastTransaction(txHex);
  1. Send the HTTP request to the endpoint.
  2. If the response is invalid, throw an error.
  3. If the response is valid, console.log the response (transaction id).
    1. Expected format: Transaction broadcasted successfully. TXID: 57d0430318a389c5ee447ae99b8858179863dd771f64e8aa580672216755f2f5

Next Steps

By the end of this guide you should be capable of sending a Bitcoin P2PKH transaction from a PKP.

Now that you've realized Lit PKPs have the potential to sign Bitcoin transactions, the sky is the limit from here. Adding multiple UTXOs to the inputs, implementing functionality for P2SH or P2WPKH, or writing a more complex trigger for the Lit Action are all possibilities.

If you'd like to learn more about Lit, check out the Lit Protocol Documentation, more specific functionality for PKPs, or advanced usage of Lit Actions.

If you'd like to request an example of another Bitcoin transaction type, please go this form to request it.