跳至主要內容
進階

Wallet Descriptors

輸出描述符:標準化錢包備份和地址派生的現代方法

18 分鐘

概述

輸出描述符(Output Descriptors)是一種描述如何從密鑰派生比特幣地址的標準化語言。 它解決了傳統 BIP-39/44 錢包備份的局限性,可以精確描述任何類型的腳本和派生路徑。

為什麼需要描述符? 種子詞只告訴你「密鑰是什麼」,但不告訴你「如何使用它們」。 描述符完整定義了腳本類型、派生路徑和多簽結構。

基本語法

結構

SCRIPT(KEY)#checksum

SCRIPT = 腳本函數(pkh, wpkh, sh, wsh, tr, multi, sortedmulti...)
KEY = 密鑰表達式(公鑰、xpub、派生路徑)
checksum = 8 字符校驗碼

腳本函數

函數 說明 地址格式
pk(KEY) P2PK(直接公鑰) 無地址
pkh(KEY) P2PKH 1...
wpkh(KEY) P2WPKH(原生 SegWit) bc1q...
sh(wpkh(KEY)) P2SH-P2WPKH(兼容 SegWit) 3...
tr(KEY) P2TR(Taproot) bc1p...
multi(k,KEY1,KEY2,...) k-of-n 多簽 -
sortedmulti(k,...) 排序的多簽 -

密鑰表達式

原始公鑰

// 壓縮公鑰
pkh(02e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35)

// 未壓縮公鑰(不推薦)
pkh(04e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35...)

擴展密鑰 (xpub/xprv)

// 主公鑰
wpkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)

// 帶派生路徑
wpkh(xpub.../84'/0'/0'/0/*)

// 私鑰(用於簽名)
wpkh(xprv9s21ZrQH143K3GJpoapnV8SFfuZaEJq2Gs...)

派生路徑

語法: [fingerprint/path]xpub.../derivation

範例:
[d34db33f/84'/0'/0']xpub6ERApfZwU.../0/*

- d34db33f: 主密鑰指紋(4 bytes)
- 84'/0'/0': 帳戶派生路徑(硬化)
- 0/*: 外部鏈(接收地址),通配符

通配符

* = 匹配任何索引(0, 1, 2, ...)
*' 或 *h = 硬化派生

範例:
wpkh([d34db33f/84'/0'/0']xpub.../0/*)
- /0/* = 接收地址 (m/84'/0'/0'/0/0, /0/1, /0/2...)
- /1/* = 找零地址 (m/84'/0'/0'/1/0, /1/1, /1/2...)

常見描述符

BIP-84 (Native SegWit)

// 接收地址
wpkh([d34db33f/84'/0'/0']xpub6ERApfZwU.../0/*)

// 找零地址
wpkh([d34db33f/84'/0'/0']xpub6ERApfZwU.../1/*)

BIP-86 (Taproot)

// 接收地址
tr([d34db33f/86'/0'/0']xpub6BgBgse.../0/*)

// 找零地址
tr([d34db33f/86'/0'/0']xpub6BgBgse.../1/*)

BIP-49 (Nested SegWit)

// P2SH-P2WPKH
sh(wpkh([d34db33f/49'/0'/0']xpub6CUGRUo.../0/*))

BIP-44 (Legacy)

// P2PKH
pkh([d34db33f/44'/0'/0']xpub6BosfCn.../0/*)

多簽描述符

2-of-3 多簽

// P2SH 多簽
sh(multi(2,
  [aabbccdd/48'/0'/0'/1']xpub6E1.../0/*,
  [11223344/48'/0'/0'/1']xpub6E2.../0/*,
  [55667788/48'/0'/0'/1']xpub6E3.../0/*
))

// P2WSH 多簽(原生 SegWit)
wsh(multi(2,
  [aabbccdd/48'/0'/0'/2']xpub6E1.../0/*,
  [11223344/48'/0'/0'/2']xpub6E2.../0/*,
  [55667788/48'/0'/0'/2']xpub6E3.../0/*
))

排序多簽

// sortedmulti 確保公鑰順序一致
wsh(sortedmulti(2,
  xpub6E1.../0/*,
  xpub6E2.../0/*,
  xpub6E3.../0/*
))

// 無論提供順序如何,生成相同的地址

Taproot 描述符

Key Path Only

// 簡單 Taproot(只有 key path)
tr([d34db33f/86'/0'/0']xpub.../0/*)

Script Tree

// 帶腳本樹的 Taproot
tr(KEY, {
  {pk(KEY_A), pk(KEY_B)},
  pk(KEY_C)
})

// 結構:
//        root
//       /    \
//    branch   pk(C)
//    /    \
// pk(A)  pk(B)

複雜 Taproot

// 多種花費路徑
tr(INTERNAL_KEY, {
  // 2-of-2 多簽
  multi_a(2, KEY1, KEY2),
  // 或時間鎖後單簽
  and_v(v:pk(KEY3), older(144))
})

Bitcoin CLI 操作

創建描述符錢包

# 創建描述符錢包
bitcoin-cli createwallet "my_wallet" false false "" false true

# 獲取錢包描述符
bitcoin-cli listdescriptors

# 結果
{
  "wallet_name": "my_wallet",
  "descriptors": [
    {
      "desc": "wpkh([d34db33f/84'/0'/0']xpub.../0/*)#checksum",
      "timestamp": 1700000000,
      "active": true,
      "internal": false,
      "range": [0, 999]
    },
    {
      "desc": "wpkh([d34db33f/84'/0'/0']xpub.../1/*)#checksum",
      "timestamp": 1700000000,
      "active": true,
      "internal": true,
      "range": [0, 999]
    }
  ]
}

導入描述符

# 導入單個描述符
bitcoin-cli importdescriptors '[{
  "desc": "wpkh([d34db33f/84h/0h/0h]xpub.../0/*)#checksum",
  "timestamp": "now",
  "range": [0, 999],
  "watchonly": true,
  "active": true
}]'

# 導入多簽
bitcoin-cli importdescriptors '[{
  "desc": "wsh(sortedmulti(2,[fp1]xpub1.../0/*,[fp2]xpub2.../0/*))#checksum",
  "timestamp": "now",
  "range": 1000,
  "active": true
}]'

獲取描述符資訊

# 解析描述符
bitcoin-cli getdescriptorinfo "wpkh([d34db33f/84'/0'/0']xpub.../0/*)"

# 結果
{
  "descriptor": "wpkh([d34db33f/84'/0'/0']xpub.../0/*)#abc12345",
  "checksum": "abc12345",
  "isrange": true,
  "issolvable": true,
  "hasprivatekeys": false
}

# 派生地址
bitcoin-cli deriveaddresses "wpkh([d34db33f/84'/0'/0']xpub.../0/*)#checksum" "[0,5]"

# 結果
[
  "bc1q...",  # 索引 0
  "bc1q...",  # 索引 1
  ...
]

TypeScript 實作

解析描述符

interface ParsedDescriptor {
  scriptType: string;
  keys: KeyExpression[];
  isRange: boolean;
  checksum?: string;
}

interface KeyExpression {
  fingerprint?: string;
  path?: string;
  key: string;
  derivation?: string;
}

function parseDescriptor(descriptor: string): ParsedDescriptor {
  // 移除校驗碼
  const checksumMatch = descriptor.match(/#([a-z0-9]{8})$/);
  const checksum = checksumMatch?.[1];
  const desc = checksumMatch
    ? descriptor.slice(0, -9)
    : descriptor;

  // 解析腳本類型
  const scriptMatch = desc.match(/^(\w+)\((.+)\)$/);
  if (!scriptMatch) {
    throw new Error('Invalid descriptor format');
  }

  const [, scriptType, content] = scriptMatch;

  // 解析密鑰表達式
  const keys = parseKeyExpressions(content);

  const isRange = descriptor.includes('/*');

  return {
    scriptType,
    keys,
    isRange,
    checksum,
  };
}

function parseKeyExpressions(content: string): KeyExpression[] {
  // 簡化的解析邏輯
  const keyPattern = /\[([a-f0-9]{8})\/([^\]]+)\]([a-zA-Z0-9]+)(\/[0-9*'h\/]+)?/g;
  const keys: KeyExpression[] = [];
  let match;

  while ((match = keyPattern.exec(content)) !== null) {
    keys.push({
      fingerprint: match[1],
      path: match[2],
      key: match[3],
      derivation: match[4],
    });
  }

  return keys;
}

使用 bitcoinjs-lib

import * as bitcoin from 'bitcoinjs-lib';
import BIP32Factory from 'bip32';
import * as ecc from 'tiny-secp256k1';

const bip32 = BIP32Factory(ecc);

function deriveAddressesFromDescriptor(
  xpub: string,
  scriptType: 'wpkh' | 'pkh' | 'tr',
  startIndex: number,
  count: number
): string[] {
  const node = bip32.fromBase58(xpub);
  const addresses: string[] = [];

  for (let i = startIndex; i < startIndex + count; i++) {
    const child = node.derive(0).derive(i);
    const pubkey = child.publicKey;

    let address: string;
    switch (scriptType) {
      case 'wpkh':
        address = bitcoin.payments.p2wpkh({
          pubkey,
          network: bitcoin.networks.bitcoin,
        }).address!;
        break;
      case 'pkh':
        address = bitcoin.payments.p2pkh({
          pubkey,
          network: bitcoin.networks.bitcoin,
        }).address!;
        break;
      case 'tr':
        address = bitcoin.payments.p2tr({
          internalPubkey: pubkey.slice(1, 33),
          network: bitcoin.networks.bitcoin,
        }).address!;
        break;
    }

    addresses.push(address);
  }

  return addresses;
}

計算校驗碼

const CHECKSUM_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';

function descriptorChecksum(descriptor: string): string {
  const INPUT_CHARSET =
    '0123456789()\'[],/*abcdefgh@:$%{}' +
    'IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~' +
    'ijklmnopqrstuvwxyzABCDEFGH`#"\' ';

  function polymod(values: number[]): bigint {
    const GENERATOR = [
      0xf5dee51989n,
      0xa9fdca3312n,
      0x1bab10e32dn,
      0x3706b1677an,
      0x644d626ffdn,
    ];

    let c = 1n;
    for (const v of values) {
      const c0 = c >> 35n;
      c = ((c & 0x7ffffffffn) << 5n) ^ BigInt(v);
      for (let i = 0; i < 5; i++) {
        if ((c0 >> BigInt(i)) & 1n) {
          c ^= GENERATOR[i];
        }
      }
    }
    return c;
  }

  const values: number[] = [];
  for (const c of descriptor) {
    const pos = INPUT_CHARSET.indexOf(c);
    if (pos === -1) throw new Error('Invalid character');
    values.push(pos & 31);
    values.push(pos >> 5);
  }
  values.push(...[0, 0, 0, 0, 0, 0, 0, 0]);

  const polymodValue = polymod(values) ^ 1n;
  let checksum = '';
  for (let i = 0; i < 8; i++) {
    checksum += CHECKSUM_CHARSET[
      Number((polymodValue >> BigInt(5 * (7 - i))) & 31n)
    ];
  }

  return checksum;
}

備份最佳實踐

完整備份

備份應包含:
1. 種子詞(助記詞)
2. 描述符(包含腳本類型和路徑)
3. 主密鑰指紋
4. 創建時間戳

範例備份文件:
{
  "format": "descriptor-wallet-backup",
  "version": 1,
  "created": "2024-01-01T00:00:00Z",
  "descriptors": [
    {
      "desc": "wpkh([d34db33f/84'/0'/0']xpub.../0/*)#checksum",
      "label": "Receive",
      "range": [0, 999]
    },
    {
      "desc": "wpkh([d34db33f/84'/0'/0']xpub.../1/*)#checksum",
      "label": "Change",
      "range": [0, 999]
    }
  ]
}

從描述符恢復

# 創建空錢包
bitcoin-cli createwallet "restored" true true "" false true

# 導入描述符
bitcoin-cli -rpcwallet=restored importdescriptors '[
  {
    "desc": "wpkh([d34db33f/84h/0h/0h]xpub.../0/*)#checksum",
    "timestamp": 1609459200,
    "range": 1000,
    "active": true,
    "internal": false
  },
  {
    "desc": "wpkh([d34db33f/84h/0h/0h]xpub.../1/*)#checksum",
    "timestamp": 1609459200,
    "range": 1000,
    "active": true,
    "internal": true
  }
]'

# 重新掃描區塊鏈
bitcoin-cli -rpcwallet=restored rescanblockchain 700000

相關資源

已複製連結
已複製到剪貼簿