Guide to Creating a Partially Signed Bitcoin Transaction (PSBT) for Developers

This guide will walk you through the steps to create and sign a Partially Signed Bitcoin Transaction (PSBT) using a combination of JavaScript libraries and React components. The provided code example is comprehensive, but we’ll break it down step-by-step to ensure you understand each part.

Table of Contents

  1. Understanding Partially Signed Bitcoin Transactions (PSBT)
  2. Step-by-Step Guide
  3. Full Code Example

1. Understanding Partially Signed Bitcoin Transactions (PSBT)

A Partially Signed Bitcoin Transaction (PSBT) is a standard format for handling Bitcoin transactions that require multiple signatures or other complex signing workflows. PSBTs enable the separation of transaction creation and signing, allowing for more flexible and secure transaction management.

Key Features of PSBT

  • Interoperability: PSBTs can be used across different wallets and software.
  • Flexibility: Allows for multiple parties to contribute signatures to a single transaction.
  • Security: Enhances security by enabling hardware wallets and other secure devices to sign transactions without exposing private keys.

2. Step-by-Step Guide to Creating a PSBT

This section provides a detailed step-by-step guide to creating a PSBT using the bitcoinjs-lib library in JavaScript. We’ll break down each part of the provided code example and explain what it does.

Step 1: Import Necessary Libraries and Functions

First, we import the necessary libraries and helper functions for working with Bitcoin transactions, data conversions, hashing, elliptic curve cryptography, and React components.

  • bitcoinjs-lib: A popular JavaScript library for working with Bitcoin transactions.
  • @stacks/common: Provides utility functions for handling data conversions.
  • @noble/hashes/sha256: Implements the SHA-256 hashing algorithm.
  • @bitcoinerlab/secp256k1: Provides elliptic curve cryptography functions for Bitcoin.
  • @dynamic-labs/sdk-react-core: Provides context for accessing wallet information.
  • @dynamic-labs/wallet-connector-core: Checks if the wallet connector is a Bitcoin connector.
  • useState: A React hook for managing state in a functional component.
import { DynamicWidget, useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { isBitcoinWallet } from '@dynamic-labs/bitcoin';

import * as bitcoin from 'bitcoinjs-lib';
import { hexToBytes, utf8ToBytes } from '@stacks/common';
import { sha256 } from '@noble/hashes/sha256';
import ecc from '@bitcoinerlab/secp256k1';
import { useState } from "react";

Step 2: Initialize the ECC Library

Before we can work with elliptic curve cryptography (ECC) functions in Bitcoin transactions, we need to initialize the ECC library. This step is crucial for enabling cryptographic operations such as signing and verifying transactions.

bitcoin.initEccLib(ecc);

  • Purpose: The bitcoinjs-lib library relies on an ECC library to perform cryptographic operations. By default, bitcoinjs-lib does not include an ECC library to keep the core library lightweight and modular. We need to explicitly provide an ECC implementation.
  • Library Used: In this example, we use the @bitcoinerlab/secp256k1 library, which is an implementation of the secp256k1 elliptic curve, commonly used in Bitcoin.
  • Initialization: We pass the ecc object from the @bitcoinerlab/secp256k1 library to the bitcoin.initEccLib function to initialize the ECC library. This allows bitcoinjs-lib to use the ECC functions provided by the @bitcoinerlab/secp256k1 library.
bitcoin.initEccLib(ecc);

Step 3: Define Constants and Helper Functions

Next, we define some constants and helper functions that will be used throughout the process. These include a message tag for BIP-322, a hashed version of this tag, and values for a dummy transaction input.

BIP-322 Message Tag and Hash

BIP-322 is a Bitcoin Improvement Proposal that defines a standard way to sign and verify arbitrary messages using Bitcoin keys. To ensure the message being signed is unique and recognizable as a BIP-322 message, a specific tag (bip322MessageTag) is used. This tag is hashed twice using SHA-256 to create a unique identifier (messageTagHash).

const bip322MessageTag = 'BIP0322-signed-message';

const messageTagHash = Uint8Array.from([
  ...sha256(utf8ToBytes(bip322MessageTag)),
  ...sha256(utf8ToBytes(bip322MessageTag)),
]);

Dummy Transaction Input Values

The bip322TransactionToSignValues object contains placeholder values for a previous transaction output. These values are used to construct a virtual transaction that is not actually spending real Bitcoins but is required to prepare the PSBT.

  • prevoutHash: A 32-byte array of zeros, representing the hash of a non-existent previous transaction.
  • prevoutIndex: Set to 0xffffffff, a special value indicating that this is not a real transaction output.
  • sequence: Set to 0, indicating the sequence number of the transaction input.
export const bip322TransactionToSignValues = {
  prevoutHash: hexToBytes('0000000000000000000000000000000000000000000000000000000000000000'),
  prevoutIndex: 0xffffffff,
  sequence: 0,
};

Hashing the BIP-322 Message

The hashBip322Message function takes a message (as a Uint8Array or a string) and hashes it using the SHA-256 algorithm, along with the messageTagHash. This ensures that the message is uniquely identified as a BIP-322 message.

export function hashBip322Message(message) {
  return sha256(
    Uint8Array.from([...messageTagHash, ...(isString(message) ? utf8ToBytes(message) : message)])
  );
}

function isString(value) {
  return typeof value === 'string';
}

Step 4: Generate the PSBT

The generatePsbt function creates a Partially Signed Bitcoin Transaction (PSBT) using the provided address and message.

Step-by-Step Explanation:

  1. Generate Script: Convert the Bitcoin address into a scriptPubKey. This script is used to determine the conditions under which the Bitcoin can be spent.
  2. Hash the Message: Hash the message using the BIP-322 message hash function. This ensures the message is uniquely identified and securely hashed.
  3. Create ScriptSig: Create the scriptSig with the hashed message. The scriptSig is part of the transaction input and contains the conditions required to spend the UTXO.
  4. Virtual Transaction: Create a virtual transaction to spend, setting the necessary inputs and outputs. This virtual transaction simulates spending a UTXO without actually spending real Bitcoins.
  5. Create PSBT: Create a PSBT object and add the necessary inputs and outputs. This PSBT will be signed by the wallet.
const generatePsbt = (address, message) => {
  const { prevoutHash, prevoutIndex, sequence } = bip322TransactionToSignValues;

  // Generate the script for the given address
  const script = bitcoin.address.toOutputScript(
    address,
    bitcoin.networks.bitcoin
  );

  // Hash the message
  const hash = hashBip322Message(message);

  // Create the scriptSig with the hashed message
  const commands = [0, Buffer.from(hash)];
  const scriptSig = bitcoin.script.compile(commands);

  // Create a virtual transaction to spend
  const virtualToSpend = new bitcoin.Transaction();
  virtualToSpend.version = 0;
  virtualToSpend.addInput(Buffer.from(prevoutHash), prevoutIndex, sequence, scriptSig);
  virtualToSpend.addOutput(script, 0);

  // Create the PSBT
  const virtualToSign = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
  virtualToSign.setVersion(0);
  const prevTxHash = virtualToSpend.getHash();
  const prevOutIndex = 0;
  const toSignScriptSig = bitcoin.script.compile([bitcoin.script.OPS.OP_RETURN]);
  try {
    virtualToSign.addInput({
      hash: prevTxHash,
      index: prevOutIndex,
      sequence: 0,
      witnessUtxo: { script, value: 0 },
    });
  } catch (e) {
    console.log(e);
    throw e;
  }

 

 virtualToSign.addOutput({ script: toSignScriptSig, value: 0 });
  return virtualToSign.toBase64();
}

Step 5: Create the SignMessageViaTransaction Component

The SignMessageViaTransaction component handles the interaction with the wallet to sign the PSBT.

Step-by-Step Explanation:

  1. useDynamicContext: Hook to access the primary wallet from the context. This provides information about the connected wallet.
  2. useState: React hook to manage the message state. This allows us to keep track of the message that we want to sign.
  3. Check Wallet Connector: Ensure the wallet connector supports Bitcoin. If the wallet does not support Bitcoin, we return null and do not render the component.
  4. signMessageViaTransaction: Function to generate the PSBT, request the wallet to sign it, and handle the signed PSBT. This function includes:
    • Get Address: Get the Bitcoin address from the wallet.
    • Generate PSBT: Call the generatePsbt function to create the PSBT.
    • Sign PSBT: Request the wallet to sign the PSBT using the provided parameters.
    • Handle Signed PSBT: Log the signed PSBT or handle any errors that occur during signing.
const SignMessageViaTransaction = () => {
  const { primaryWallet } = useDynamicContext();
  const [message, setMessage] = useState('Hello World');

  if (!isBitcoinWallet(primaryWallet)) return null;

  const signMessageViaTransaction = async () => {
    // Get the Bitcoin address from the wallet
    const address = await primaryWallet.getAddress();
    
    // Generate the PSBT
    const psbt = generatePsbt(address, message);
    
    // Define the parameters for signing the PSBT
    const params = {
      allowedSighash: [1], // Only allow SIGHASH_ALL
      unsignedPsbtBase64: psbt, // The unsigned PSBT in Base64 format
      signature: [{
        address, // The address that is signing
        signingIndexes: [0] // The index of the input being signed
      }]
    };

    try {
      // Request the wallet to sign the PSBT
      const signedPsbt = await primaryWallet.signPsbt(params);
      console.log(signedPsbt); // Log the signed PSBT
    } catch (e) {
      console.log(e); // Handle any errors that occur during signing
    }
  }

  return (
    <div>
      <button onClick={signMessageViaTransaction}>Sign Transaction Via Message</button>
    </div>
  )
}

Step 6: Create the Main Component

The Main component renders the DynamicWidget for wallet connection and the SignMessageViaTransaction component.

Step-by-Step Explanation:

  1. DynamicWidget: This component handles the wallet connection UI, allowing users to connect their wallets.
  2. SignMessageViaTransaction: This component handles the signing of the PSBT via the connected wallet.
const Main = () => {
  return (
    <div className="min-h-screen bg-gradient-to-b from-gray-900 to-black flex flex-col items-center justify-center text-white">
      <DynamicWidget />
      <SignMessageViaTransaction />
    </div>
  );
}

export default Main;

Full Code Example

Here is the full code example that incorporates all the steps explained above:

import { DynamicWidget, useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { isBitcoinWallet } from '@dynamic-labs/bitcoin';

import * as bitcoin from 'bitcoinjs-lib';
import { hexToBytes, utf8ToBytes } from '@stacks/common';
import { sha256 } from '@noble/hashes/sha256';
import ecc from '@bitcoinerlab/secp256k1';
import { useState } from "react";

bitcoin.initEccLib(ecc);

function isString(value) {
  return typeof value === 'string';
}

const bip322MessageTag = 'BIP0322-signed-message';

const messageTagHash = Uint8Array.from([
  ...sha256(utf8ToBytes(bip322MessageTag)),
  ...sha256(utf8ToBytes(bip322MessageTag)),
]);

export const bip322TransactionToSignValues = {
  prevoutHash: hexToBytes('0000000000000000000000000000000000000000000000000000000000000000'),
  prevoutIndex: 0xffffffff,
  sequence: 0,
};

export function hashBip322Message(message) {
  return sha256(
    Uint8Array.from([...messageTagHash, ...(isString(message) ? utf8ToBytes(message) : message)])
  );
}

const generatePsbt = (address, message) => {
  const { prevoutHash, prevoutIndex, sequence } = bip322TransactionToSignValues;

  // Generate the script for the given address
  const script = bitcoin.address.toOutputScript(
    address,
    bitcoin.networks.bitcoin
  );

  // Hash the message
  const hash = hashBip322Message(message);

  // Create the scriptSig with the hashed message
  const commands = [0, Buffer.from(hash)];
  const scriptSig = bitcoin.script.compile(commands);

  // Create a virtual transaction to spend
  const virtualToSpend = new bitcoin.Transaction();
  virtualToSpend.version = 0;
  virtualToSpend.addInput(Buffer.from(prevoutHash), prevoutIndex, sequence, scriptSig);
  virtualToSpend.addOutput(script, 0);

  // Create the PSBT
  const virtualToSign = new bitcoin.Psbt({ network: bitcoin.networks.bitcoin });
  virtualToSign.setVersion(0);
  const prevTxHash = virtualToSpend.getHash();
  const prevOutIndex = 0;
  const toSignScriptSig = bitcoin.script.compile([bitcoin.script.OPS.OP_RETURN]);

  try {
    virtualToSign.addInput({
      hash: prevTxHash,
      index: prevOutIndex,
      sequence: 0,
      witnessUtxo: { script, value: 0 },
    });
  } catch (e) {
    console.log(e);
    throw e;
  }

  virtualToSign.addOutput({ script: toSignScriptSig, value: 0 });
  return virtualToSign.toBase64();
}
  
const SignMessageViaTransaction = () => {
  const { primaryWallet } = useDynamicContext();
  const [message, setMessage] = useState('Hello World');

  if (!isBitcoinWallet(primaryWallet)) {
    return null;
  }

  const signMessageViaTransaction = async () => {
    // Get the Bitcoin address from the wallet
    const address = await primaryWallet.address;
    
    // Generate the PSBT
    const psbt = generatePsbt(address, message);
    
    // Define the parameters for signing the PSBT
    const params = {
      allowedSighash: [1], // Only allow SIGHASH_ALL
      unsignedPsbtBase64: psbt, // The unsigned PSBT in Base64 format
      signature: [{
        address, // The address that is signing
        signingIndexes: [0] // The index of the input being signed
      }]
    };

    try {
      // Request the wallet to sign the PSBT
      const signedPsbt = await primaryWallet.signPsbt(params);
      console.log(signedPsbt); // Log the signed PSBT
    } catch (e) {
      console.log(e); // Handle any errors that occur during signing
    }
  }

  return (
    <div>
      <button onClick={signMessageViaTransaction}>Sign Transaction Via Message</button>
    </div>
  )
}

const Main = () => {
  return (
    <div className="min-h-screen bg-gradient-to-b from-gray-900 to-black flex flex-col items-center justify-center text-white">
      <DynamicWidget />
      <SignMessageViaTransaction />
    </div>
  );
}

export default Main;