進階
Bech32 & Bech32m
Bech32 編碼:SegWit 和 Taproot 地址的現代編碼格式
15 分鐘
概述
Bech32 是一種專為 SegWit 地址設計的編碼格式(BIP-173), 而 Bech32m 是其改進版本(BIP-350),用於 Taproot 地址。 這兩種編碼提供更好的錯誤檢測能力,並且更容易閱讀和輸入。
地址前綴:
主網以 bc1 開頭,測試網以 tb1 開頭。 SegWit v0 (bc1q) 使用 Bech32,Taproot
(bc1p) 使用 Bech32m。
編碼設計
字符集
Bech32 字符集 (32 個字符):
q p z r y 9 x 8 g f 2 t v d w 0 s 3 j n 5 4 k h c e 6 m u a 7 l
設計原則:
- 沒有 0/O 和 I/l (避免混淆)
- 沒有 b (與前綴 bc 混淆)
- 全小寫或全大寫 (不混用)
- 容易區分的字符
每個字符代表 5 bits:
q=0, p=1, z=2, r=3, y=4, 9=5, x=6, 8=7, ...
地址結構:
┌─────────────────────────────────────────────┐
│ bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 │
├───┬──┬─────────────────────────────────┬────┤
│HRP│ 1│ Data Part │Chk │
│bc │分│ witness version + program │sum │
│ │隔│ │ │
└───┴──┴─────────────────────────────────┴────┘
HRP: Human-Readable Part (bc 或 tb)
分隔符: 1 (永遠是 1)
Data: 5-bit 編碼的數據
Checksum: 6 個字符的錯誤檢測碼 Checksum 算法
BCH 碼 (Bose-Chaudhuri-Hocquenghem):
多項式: x^6 + x^4 + x^2 + x + 1
Bech32 常數: 1
Bech32m 常數: 0x2bc830a3
錯誤檢測能力:
- 保證檢測最多 4 個字符錯誤
- 高機率檢測更多錯誤
- 檢測任意位置的單字符錯誤
錯誤定位:
- 可以定位錯誤位置 (某些情況)
- 幫助用戶修正輸入錯誤
vs Base58Check:
- Base58: 4 byte checksum (SHA256)
- Bech32: 6 字符 BCH 碼
- Bech32 錯誤檢測更強 版本對比
Bech32 vs Bech32m
為什麼需要 Bech32m:
Bech32 的弱點:
- 當最後一個字符是 'p' 時
- 插入或刪除 'q' 可能不改變 checksum
- 這是 BCH 碼的數學特性
範例:
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 (有效)
bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4qq... (可能有效!)
影響:
- SegWit v0 (witness version 0) 不受影響
因為長度固定 (20 或 32 bytes)
- Witness v1+ 可能受影響
解決方案 (BIP-350):
- Bech32m 使用不同的常數
- witness v0: 繼續使用 Bech32
- witness v1+: 使用 Bech32m
地址類型:
┌─────────────────────────────────────────┐
│ 類型 │ 前綴 │ 編碼 │ 版本 │
├─────────────────────────────────────────┤
│ P2WPKH │ bc1q │ Bech32 │ v0 │
│ P2WSH │ bc1q │ Bech32 │ v0 │
│ P2TR (Taproot)│ bc1p │ Bech32m │ v1 │
│ 未來版本 │ bc1? │ Bech32m │ v2+ │
└─────────────────────────────────────────┘ TypeScript 實作
編碼函數
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
const BECH32_CONST = 1;
const BECH32M_CONST = 0x2bc830a3;
type Encoding = 'bech32' | 'bech32m';
// 多項式運算
function polymod(values: number[]): number {
const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
let chk = 1;
for (const v of values) {
const top = chk >> 25;
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (let i = 0; i < 5; i++) {
if ((top >> i) & 1) {
chk ^= GEN[i];
}
}
}
return chk;
}
// HRP 擴展
function hrpExpand(hrp: string): number[] {
const result: number[] = [];
for (const c of hrp) {
result.push(c.charCodeAt(0) >> 5);
}
result.push(0);
for (const c of hrp) {
result.push(c.charCodeAt(0) & 31);
}
return result;
}
// 創建 checksum
function createChecksum(
hrp: string,
data: number[],
encoding: Encoding
): number[] {
const values = [...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0];
const constant = encoding === 'bech32' ? BECH32_CONST : BECH32M_CONST;
const polymodResult = polymod(values) ^ constant;
const checksum: number[] = [];
for (let i = 0; i < 6; i++) {
checksum.push((polymodResult >> (5 * (5 - i))) & 31);
}
return checksum;
}
// 編碼
function encode(
hrp: string,
data: number[],
encoding: Encoding
): string {
const checksum = createChecksum(hrp, data, encoding);
const combined = [...data, ...checksum];
let result = hrp + '1';
for (const d of combined) {
result += CHARSET[d];
}
return result;
}
// 將 bytes 轉換為 5-bit 數組
function convertBits(
data: Uint8Array,
fromBits: number,
toBits: number,
pad: boolean
): number[] | null {
let acc = 0;
let bits = 0;
const result: number[] = [];
const maxv = (1 << toBits) - 1;
for (const value of data) {
if (value < 0 || value >> fromBits !== 0) {
return null;
}
acc = (acc << fromBits) | value;
bits += fromBits;
while (bits >= toBits) {
bits -= toBits;
result.push((acc >> bits) & maxv);
}
}
if (pad) {
if (bits > 0) {
result.push((acc << (toBits - bits)) & maxv);
}
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) !== 0) {
return null;
}
return result;
}
// 編碼 SegWit 地址
function encodeSegWitAddress(
hrp: string,
version: number,
program: Uint8Array
): string {
const data = convertBits(program, 8, 5, true);
if (!data) throw new Error('Invalid program');
const encoding = version === 0 ? 'bech32' : 'bech32m';
return encode(hrp, [version, ...data], encoding);
}
// 使用範例
const pubkeyHash = hexToBytes('751e76e8199196d454941c45d1b3a323f1433bd6');
const address = encodeSegWitAddress('bc', 0, pubkeyHash);
console.log(address); // bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 解碼函數
// 驗證 checksum
function verifyChecksum(
hrp: string,
data: number[]
): Encoding | null {
const values = [...hrpExpand(hrp), ...data];
const result = polymod(values);
if (result === BECH32_CONST) return 'bech32';
if (result === BECH32M_CONST) return 'bech32m';
return null;
}
// 解碼
function decode(str: string): {
hrp: string;
data: number[];
encoding: Encoding;
} | null {
// 檢查大小寫混用
if (str !== str.toLowerCase() && str !== str.toUpperCase()) {
return null;
}
str = str.toLowerCase();
// 找到分隔符
const pos = str.lastIndexOf('1');
if (pos < 1 || pos + 7 > str.length || str.length > 90) {
return null;
}
const hrp = str.slice(0, pos);
const dataStr = str.slice(pos + 1);
// 解碼數據
const data: number[] = [];
for (const c of dataStr) {
const idx = CHARSET.indexOf(c);
if (idx === -1) return null;
data.push(idx);
}
// 驗證 checksum
const encoding = verifyChecksum(hrp, data);
if (!encoding) return null;
// 移除 checksum
return {
hrp,
data: data.slice(0, -6),
encoding
};
}
// 解碼 SegWit 地址
function decodeSegWitAddress(
hrp: string,
address: string
): { version: number; program: Uint8Array } | null {
const decoded = decode(address);
if (!decoded || decoded.hrp !== hrp) return null;
const { data, encoding } = decoded;
if (data.length < 1) return null;
const version = data[0];
// 驗證版本和編碼的匹配
if (version === 0 && encoding !== 'bech32') return null;
if (version !== 0 && encoding !== 'bech32m') return null;
// 轉換回 8-bit
const program = convertBits(new Uint8Array(data.slice(1)), 5, 8, false);
if (!program) return null;
// 驗證長度
if (program.length < 2 || program.length > 40) return null;
if (version === 0 && program.length !== 20 && program.length !== 32) {
return null;
}
return { version, program: new Uint8Array(program) };
}
// 使用範例
const decoded = decodeSegWitAddress('bc', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4');
console.log(decoded);
// { version: 0, program: Uint8Array(20) [...] } 地址類型
SegWit v0 地址
P2WPKH (Pay-to-Witness-Public-Key-Hash):
- 版本: 0
- 程式長度: 20 bytes
- 程式: HASH160(公鑰)
- 地址範例: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
P2WSH (Pay-to-Witness-Script-Hash):
- 版本: 0
- 程式長度: 32 bytes
- 程式: SHA256(witness script)
- 地址範例: bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3
識別方式:
bc1q + 39 字符 = P2WPKH (42 總長)
bc1q + 59 字符 = P2WSH (62 總長) Taproot 地址
P2TR (Pay-to-Taproot):
- 版本: 1
- 程式長度: 32 bytes
- 程式: x-only 公鑰 (32 bytes)
- 編碼: Bech32m
地址範例:
bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr
結構:
┌─────────────────────────────────────────────────────┐
│ bc1p... │
│ ├─ bc1: 主網前綴 │
│ ├─ p: witness version 1 (編碼為 'p') │
│ └─ ...: 32-byte x-only 公鑰 + checksum │
└─────────────────────────────────────────────────────┘
長度: 62 字符 (與 P2WSH 相同) 錯誤檢測
錯誤類型
Bech32 可檢測的錯誤:
1. 單字符替換
bc1qw508... → bc1qw5O8... ✗ 檢測到
2. 相鄰字符交換
bc1qw508... → bc1qw058... ✗ 檢測到
3. 單字符插入/刪除
bc1qw508... → bc1qw5088... ✗ 檢測到
4. 多字符錯誤 (最多 4 個)
保證檢測
5. 任意長度的突發錯誤 (最多 4 字符)
保證檢測
錯誤更正限制:
- 可以檢測但通常無法自動更正
- 可以提示可能的錯誤位置
- 需要人工確認更正 錯誤定位
// 嘗試定位錯誤
function locateErrors(address: string): number[] | null {
const decoded = decode(address);
if (decoded) return []; // 沒有錯誤
// 嘗試每個位置的每個可能替換
const errors: number[] = [];
const chars = address.split('');
const pos = address.lastIndexOf('1');
for (let i = pos + 1; i < chars.length; i++) {
const original = chars[i];
for (const replacement of CHARSET) {
if (replacement === original) continue;
chars[i] = replacement;
const test = chars.join('');
if (decode(test)) {
errors.push(i);
break;
}
}
chars[i] = original;
}
return errors.length > 0 ? errors : null;
}
// 使用範例
const wrongAddress = 'bc1qw5O8d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4';
const errorPositions = locateErrors(wrongAddress);
console.log('可能的錯誤位置:', errorPositions); QR 碼優化
Bech32 的 QR 碼優勢:
字母數字模式:
- QR 碼有專門的字母數字模式
- 支持: 0-9, A-Z, 空格, $%*+-./:
- 比二進制模式效率高 45%
Bech32 設計:
- 全大寫時符合 QR 字母數字模式
- BC1QW508... (大寫有效)
- 更小的 QR 碼
vs Base58:
- Base58 包含小寫字母
- 必須使用二進制模式
- 相同數據需要更大的 QR 碼
大小比較 (相同 20 byte payload):
┌───────────────────────────────────────┐
│ 編碼 │ 長度 │ QR 模式 │ 模組 │
├───────────────────────────────────────┤
│ Base58 │ 34 字符 │ 二進制 │ ~100 │
│ Bech32 │ 42 字符 │ 字母數字│ ~80 │
└───────────────────────────────────────┘
建議: 生成 QR 碼時使用大寫 地址驗證
interface AddressValidation {
valid: boolean;
type?: 'p2wpkh' | 'p2wsh' | 'p2tr' | 'unknown';
network?: 'mainnet' | 'testnet' | 'regtest';
error?: string;
}
function validateBech32Address(address: string): AddressValidation {
// 嘗試解碼
const decoded = decode(address.toLowerCase());
if (!decoded) {
return { valid: false, error: 'Invalid checksum or format' };
}
const { hrp, data, encoding } = decoded;
// 驗證網路
let network: AddressValidation['network'];
if (hrp === 'bc') network = 'mainnet';
else if (hrp === 'tb') network = 'testnet';
else if (hrp === 'bcrt') network = 'regtest';
else {
return { valid: false, error: `Unknown HRP: ${hrp}` };
}
// 提取版本和程式
const version = data[0];
const program = convertBits(new Uint8Array(data.slice(1)), 5, 8, false);
if (!program) {
return { valid: false, error: 'Invalid program encoding' };
}
// 驗證版本與編碼匹配
if (version === 0 && encoding !== 'bech32') {
return { valid: false, error: 'v0 must use bech32 encoding' };
}
if (version > 0 && encoding !== 'bech32m') {
return { valid: false, error: 'v1+ must use bech32m encoding' };
}
// 確定類型
let type: AddressValidation['type'];
if (version === 0 && program.length === 20) {
type = 'p2wpkh';
} else if (version === 0 && program.length === 32) {
type = 'p2wsh';
} else if (version === 1 && program.length === 32) {
type = 'p2tr';
} else {
type = 'unknown';
}
return { valid: true, type, network };
}
// 使用範例
console.log(validateBech32Address('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'));
// { valid: true, type: 'p2wpkh', network: 'mainnet' }
console.log(validateBech32Address('bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr'));
// { valid: true, type: 'p2tr', network: 'mainnet' } 編碼比較
| 特性 | Base58Check | Bech32 | Bech32m |
|---|---|---|---|
| 字符集 | 58 個 | 32 個 | 32 個 |
| 大小寫 | 混用 | 單一 | 單一 |
| 錯誤檢測 | 4 byte SHA256 | BCH 碼 | BCH 碼 |
| 錯誤定位 | 否 | 是 | 是 |
| QR 優化 | 否 | 是 | 是 |
| 用途 | P2PKH, P2SH | SegWit v0 | Taproot+ |
相關資源
已複製連結