GardenBlockchain

Derive four Bitcoin address forms from one key

A bitcoinjs-lib 6.1.5-era example for P2PKH, nested SegWit, native SegWit, and Taproot.

A private key can participate in several Bitcoin output constructions. The earlier example derives legacy P2PKH, nested P2SH-P2WPKH, native P2WPKH, and a Taproot key-path address from one key. These are different scripts and encodings, not four unrelated private keys.

Form What the example constructs
P2PKH A legacy public-key-hash payment
P2SH-P2WPKH A SegWit redeem script wrapped by P2SH
P2WPKH A native SegWit public-key-hash output
P2TR A tweaked x-only public-key output

Historical TypeScript source

Read the address derivation notebook
//     "bitcoinjs-lib": "^6.1.5",
//     "dotenv": "^16.0.3",
//     "ecpair": "^2.1.0",
//     "tiny-secp256k1": "^2.2.3"
// https://bitcoin.design/guide/glossary/address/
// https://en.bitcoin.it/wiki/Invoice_address

require('dotenv').config();

import { ECPairFactory, ECPairInterface } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import * as bitcoin from 'bitcoinjs-lib';
import { toXOnly } from 'bitcoinjs-lib/src/psbt/bip371';

const ECPair = ECPairFactory(ecc);
bitcoin.initEccLib(ecc);

const isDev = process.env.NODE_ENV === 'development';
const network = isDev ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;

/**
 * Prepare wallet from a Wallet Import Format (WIF) private key
 * Each network -> Each address has own private key.
 */
export const initKeyPair = (WIF = process.env.WALLET_PRIVATE_KEY) => {
  if (!WIF) throw new Error('WALLET_PRIVATE_KEY is not set');
  // Decode WIF private key
  const keyPair = ECPair.fromWIF(WIF, network);
  return {
    keyPair,
    publicKey: keyPair.publicKey,
  };
};

// Native Segwit P2WPKH, bech32 address type with 42 chars. Start with tb1q or bc1q
// Ex: tb1q2fzh9f7ss8eu56fnv2zhr56a0wd3g64xp7d4xk
export const getNativeSegwitP2WPKH = (publicKeyBuffer: Buffer) => {
  return bitcoin.payments.p2wpkh({
    pubkey: publicKeyBuffer,
    network: network,
  });
};

// Nested Segwit P2SH(P2WPKH) with 35 chars
// Ex: 2N2EUQ12PhPr9Q7UKdp4xSReKgcVGUSUaYp
export const getNestedSegwitP2SH_P2WPKH = (publicKeyBuffer: Buffer) => {
  return bitcoin.payments.p2sh({
    redeem: bitcoin.payments.p2wpkh({ pubkey: publicKeyBuffer, network: network }),
    network: network,
  });
};

// P2PKH type with 34 chars
// Ex: mo1xu79z1354YicBa5EXwMh7HrCAfRdPSW
export const getLegacyP2PKH = (publicKeyBuffer: Buffer) => {
  return bitcoin.payments.p2pkh({ pubkey: publicKeyBuffer, network: network });
};

// Taproot P2TR with 62 chars. Start with tb1p or bc1p
// Ex: tb1pmamsssqlv0j7dda8g04phcsn0qzndy6kff4hq293e85drju0f7sq5uzedx
export const getTaprootP2TR = (keyPair: bitcoin.Signer) => {
  function tweakSigner(signer: bitcoin.Signer, opts: any = {}): bitcoin.Signer {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion,@typescript-eslint/ban-ts-comment
    // @ts-ignore
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    let privateKey: Uint8Array | undefined = signer.privateKey!;
    if (!privateKey) throw new Error('Private key is required for tweaking signer!');

    if (signer.publicKey[0] === 3) privateKey = ecc.privateNegate(privateKey);

    function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer {
      return bitcoin.crypto.taggedHash('TapTweak', Buffer.concat(h ? [pubKey, h] : [pubKey]));
    }
    const tweakedPrivateKey = ecc.privateAdd(privateKey, tapTweakHash(toXOnly(signer.publicKey), opts.tweakHash));
    if (!tweakedPrivateKey) throw new Error('Invalid tweaked private key!');

    return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), {
      network: opts.network,
    });
  }
  // Tweak the original keypair
  const tweakedSigner = tweakSigner(keyPair, { network });
  // Generate an address from the tweaked public key
  return bitcoin.payments.p2tr({
    pubkey: toXOnly(tweakedSigner.publicKey),
    network,
  });
};

export const initWallet = () => {
  const { keyPair, publicKey } = initKeyPair();

  console.log('Native Segwit Address:', getNativeSegwitP2WPKH(publicKey).address);
  console.log('Nested Segwit Address:', getNestedSegwitP2SH_P2WPKH(publicKey).address);
  console.log('Legacy Address:', getLegacyP2PKH(publicKey).address);
  console.log('Taproot Address:', getTaprootP2TR(keyPair).address);

  // Native Segwit Address: tb1q0u54lxn4gtfhntkzqlg3c0n3xgrcwynezuhuw4
  // Nested Segwit Address: 2N2EUQ12PhPr9Q7UKdp4xSReKgcVGUSUaYp
  // Legacy Address: ms7Kg6REg2cA3f953gv1G6n79QB4Bg2hgu
  // Taproot Address: tb1p43ucsc2h6pfw08xsszgjjjz9tjn9ck4ezg5fxuvlu8mghwhgxyfsne5n3g
};

The source records bitcoinjs-lib 6.1.5, ecpair 2.1.0, and tiny-secp256k1 2.2.3 ranges. It imports an internal bitcoinjs-lib helper, so its module path is version-sensitive. No private key appears in the listing; WIF is read from an environment variable.

The Taproot branch handles parity and the TapTweak before constructing the output. Address prefix/length comments describe these examples and are not general validators for every script or network.

The source selects testnet only when NODE_ENV equals development; every other value selects mainnet. That default matters before any reuse. No key was loaded, derived address generated, or network request made for this revision. Source: Gist revision.

← Back to the garden