進階
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 相關資源
已複製連結