跳至主要內容
入門

Base58Check

Base58Check 編碼:比特幣傳統地址的編碼格式

12 分鐘

概述

Base58Check 是比特幣早期地址使用的編碼格式,由中本聰設計。 它基於 Base58,並添加了版本前綴和校驗碼。雖然現在推薦使用 Bech32, 但 Base58Check 仍廣泛用於 P2PKH 和 P2SH 地址。

地址前綴: P2PKH 地址以 1 開頭,P2SH 地址以 3 開頭。 這些前綴由版本字節決定。

字符集

Base58 字符

Base58 字符集 (58 個字符):

123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz

排除的字符:
- 0 (零) 和 O (大寫 O) - 容易混淆
- I (大寫 I) 和 l (小寫 L) - 容易混淆
- + 和 / - 在某些格式中有特殊意義

設計原則:
- 人類可讀
- 避免視覺上相似的字符
- 不含標點符號
- 可以雙擊選中整個地址

vs Base64:
┌─────────────────────────────────────────┐
│ Base64: A-Z, a-z, 0-9, +, /            │
│ 問題: 0/O, I/l 混淆, +/ 在 URL 中問題   │
├─────────────────────────────────────────┤
│ Base58: 移除了容易混淆的字符            │
│ 更適合人類閱讀和輸入                    │
└─────────────────────────────────────────┘

字符映射

const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';

// 字符到值的映射
const ALPHABET_MAP: { [key: string]: number } = {};
for (let i = 0; i < BASE58_ALPHABET.length; i++) {
  ALPHABET_MAP[BASE58_ALPHABET[i]] = i;
}

// 值到字符
function valueToChar(value: number): string {
  return BASE58_ALPHABET[value];
}

// 字符到值
function charToValue(char: string): number {
  const value = ALPHABET_MAP[char];
  if (value === undefined) {
    throw new Error(`Invalid Base58 character: ${char}`);
  }
  return value;
}

編碼過程

Base58 編碼

Base58 編碼原理:

輸入: 二進制數據 (bytes)
輸出: Base58 字串

過程:
1. 將 bytes 視為一個大整數 (big-endian)
2. 反覆除以 58,取餘數
3. 餘數映射到 Base58 字符
4. 處理前導零 (轉換為 '1')

範例:
輸入: 0x00ab2f (3 bytes)
= 0 × 256² + 171 × 256 + 47
= 43823

43823 ÷ 58 = 755 餘 43 → 'k'
755 ÷ 58 = 13 餘 1   → '2'
13 ÷ 58 = 0 餘 13    → 'E'

結果: "E2k" (反轉)

前導零處理:
0x00 → '1'
0x0000 → '11'

TypeScript 實作

// Base58 編碼
function base58Encode(bytes: Uint8Array): string {
  // 計數前導零
  let zeros = 0;
  for (const byte of bytes) {
    if (byte !== 0) break;
    zeros++;
  }

  // 分配足夠的空間
  const size = Math.ceil(bytes.length * 138 / 100) + 1;
  const b58 = new Uint8Array(size);

  // 將 bytes 視為大整數進行轉換
  let length = 0;
  for (const byte of bytes) {
    let carry = byte;
    let i = 0;

    for (let j = size - 1; (carry !== 0 || i < length) && j >= 0; j--, i++) {
      carry += 256 * b58[j];
      b58[j] = carry % 58;
      carry = Math.floor(carry / 58);
    }

    length = i;
  }

  // 跳過前導零
  let it = size - length;
  while (it < size && b58[it] === 0) {
    it++;
  }

  // 轉換為字串
  let result = '1'.repeat(zeros);
  for (; it < size; it++) {
    result += BASE58_ALPHABET[b58[it]];
  }

  return result;
}

// Base58 解碼
function base58Decode(str: string): Uint8Array {
  if (str.length === 0) {
    return new Uint8Array(0);
  }

  // 計數前導 '1'
  let zeros = 0;
  for (const char of str) {
    if (char !== '1') break;
    zeros++;
  }

  // 分配空間
  const size = Math.ceil(str.length * 733 / 1000) + 1;
  const b256 = new Uint8Array(size);

  // 轉換
  let length = 0;
  for (const char of str) {
    let carry = ALPHABET_MAP[char];
    if (carry === undefined) {
      throw new Error(`Invalid character: ${char}`);
    }

    let i = 0;
    for (let j = size - 1; (carry !== 0 || i < length) && j >= 0; j--, i++) {
      carry += 58 * b256[j];
      b256[j] = carry % 256;
      carry = Math.floor(carry / 256);
    }

    length = i;
  }

  // 跳過前導零
  let it = size - length;
  while (it < size && b256[it] === 0) {
    it++;
  }

  // 組合結果
  const result = new Uint8Array(zeros + (size - it));
  result.fill(0, 0, zeros);
  for (let i = zeros; it < size; i++, it++) {
    result[i] = b256[it];
  }

  return result;
}

Base58Check

結構

Base58Check 結構:

┌─────────────────────────────────────────────────────┐
│              Base58Check 編碼                       │
├──────────┬─────────────────────────┬────────────────┤
│ Version  │        Payload          │   Checksum     │
│ (1 byte) │       (20+ bytes)       │   (4 bytes)    │
└──────────┴─────────────────────────┴────────────────┘

Checksum 計算:
checksum = SHA256(SHA256(version + payload))[:4]

完整流程:
1. 準備 version + payload
2. 計算雙重 SHA256
3. 取前 4 bytes 作為 checksum
4. 連接: version + payload + checksum
5. Base58 編碼

版本字節:
┌───────────────────────────────────────────────────┐
│ 版本   │ 前綴 │ 用途                              │
├───────────────────────────────────────────────────┤
│ 0x00   │ 1    │ P2PKH 主網地址                    │
│ 0x05   │ 3    │ P2SH 主網地址                     │
│ 0x6F   │ m/n  │ P2PKH 測試網地址                  │
│ 0xC4   │ 2    │ P2SH 測試網地址                   │
│ 0x80   │ 5/K/L│ WIF 私鑰 (主網)                   │
│ 0xEF   │ 9/c  │ WIF 私鑰 (測試網)                 │
│ 0x0488B21E │ xpub │ BIP-32 擴展公鑰               │
│ 0x0488ADE4 │ xprv │ BIP-32 擴展私鑰               │
└───────────────────────────────────────────────────┘

實作

import * as crypto from 'crypto';

// SHA256
function sha256(data: Uint8Array): Uint8Array {
  return new Uint8Array(
    crypto.createHash('sha256').update(data).digest()
  );
}

// 雙重 SHA256
function doubleSha256(data: Uint8Array): Uint8Array {
  return sha256(sha256(data));
}

// Base58Check 編碼
function base58CheckEncode(version: number, payload: Uint8Array): string {
  // 構建完整數據
  const data = new Uint8Array(1 + payload.length);
  data[0] = version;
  data.set(payload, 1);

  // 計算 checksum
  const hash = doubleSha256(data);
  const checksum = hash.slice(0, 4);

  // 組合並編碼
  const full = new Uint8Array(data.length + 4);
  full.set(data);
  full.set(checksum, data.length);

  return base58Encode(full);
}

// Base58Check 解碼
function base58CheckDecode(address: string): {
  version: number;
  payload: Uint8Array;
} {
  const decoded = base58Decode(address);

  if (decoded.length < 5) {
    throw new Error('Invalid Base58Check: too short');
  }

  // 分離數據和 checksum
  const data = decoded.slice(0, -4);
  const checksum = decoded.slice(-4);

  // 驗證 checksum
  const hash = doubleSha256(data);
  const expectedChecksum = hash.slice(0, 4);

  for (let i = 0; i < 4; i++) {
    if (checksum[i] !== expectedChecksum[i]) {
      throw new Error('Invalid checksum');
    }
  }

  return {
    version: data[0],
    payload: data.slice(1)
  };
}

// 創建 P2PKH 地址
function createP2PKHAddress(
  publicKeyHash: Uint8Array,
  network: 'mainnet' | 'testnet' = 'mainnet'
): string {
  const version = network === 'mainnet' ? 0x00 : 0x6f;
  return base58CheckEncode(version, publicKeyHash);
}

// 創建 P2SH 地址
function createP2SHAddress(
  scriptHash: Uint8Array,
  network: 'mainnet' | 'testnet' = 'mainnet'
): string {
  const version = network === 'mainnet' ? 0x05 : 0xc4;
  return base58CheckEncode(version, scriptHash);
}

// 使用範例
const pubkeyHash = hexToBytes('89abcdefabbaabbaabbaabbaabbaabbaabbaabba');
const address = createP2PKHAddress(pubkeyHash);
console.log(address); // 1DpZ...

const decoded = base58CheckDecode(address);
console.log('Version:', decoded.version); // 0
console.log('Payload:', bytesToHex(decoded.payload));

WIF 私鑰格式

結構

WIF (Wallet Import Format):

未壓縮公鑰 WIF:
┌──────────┬────────────────────────────────┐
│ 0x80     │ 32-byte 私鑰                   │
└──────────┴────────────────────────────────┘
結果: 以 '5' 開頭,51 字符

壓縮公鑰 WIF:
┌──────────┬────────────────────────────────┬──────┐
│ 0x80     │ 32-byte 私鑰                   │ 0x01 │
└──────────┴────────────────────────────────┴──────┘
結果: 以 'K' 或 'L' 開頭,52 字符

測試網:
- 版本: 0xEF
- 未壓縮: 以 '9' 開頭
- 壓縮: 以 'c' 開頭

實作

// 私鑰轉 WIF
function privateKeyToWIF(
  privateKey: Uint8Array,
  compressed: boolean = true,
  network: 'mainnet' | 'testnet' = 'mainnet'
): string {
  if (privateKey.length !== 32) {
    throw new Error('Private key must be 32 bytes');
  }

  const version = network === 'mainnet' ? 0x80 : 0xef;

  let payload: Uint8Array;
  if (compressed) {
    payload = new Uint8Array(33);
    payload.set(privateKey);
    payload[32] = 0x01;
  } else {
    payload = privateKey;
  }

  return base58CheckEncode(version, payload);
}

// WIF 轉私鑰
function wifToPrivateKey(wif: string): {
  privateKey: Uint8Array;
  compressed: boolean;
  network: 'mainnet' | 'testnet';
} {
  const { version, payload } = base58CheckDecode(wif);

  let network: 'mainnet' | 'testnet';
  if (version === 0x80) {
    network = 'mainnet';
  } else if (version === 0xef) {
    network = 'testnet';
  } else {
    throw new Error(`Unknown WIF version: ${version}`);
  }

  let privateKey: Uint8Array;
  let compressed: boolean;

  if (payload.length === 33 && payload[32] === 0x01) {
    privateKey = payload.slice(0, 32);
    compressed = true;
  } else if (payload.length === 32) {
    privateKey = payload;
    compressed = false;
  } else {
    throw new Error('Invalid WIF payload length');
  }

  return { privateKey, compressed, network };
}

// 使用範例
const privkey = hexToBytes(
  '0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d'
);

const wif = privateKeyToWIF(privkey, true);
console.log('WIF:', wif); // 5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ

const decoded = wifToPrivateKey(wif);
console.log('Compressed:', decoded.compressed);

擴展密鑰

BIP-32 擴展密鑰 (78 bytes):

┌───────────────────────────────────────────────────────────┐
│ Version    │ Depth │ Fingerprint │ Child  │ Chain  │ Key │
│ (4 bytes)  │(1 byte)│ (4 bytes)   │ Number │ Code   │     │
│            │       │             │(4 bytes)│(32 bytes)│(33)│
└───────────────────────────────────────────────────────────┘

版本:
- 0x0488B21E: xpub (主網公鑰)
- 0x0488ADE4: xprv (主網私鑰)
- 0x043587CF: tpub (測試網公鑰)
- 0x04358394: tprv (測試網私鑰)

其他衍生標準:
- ypub/yprv: BIP-49 (P2WPKH-nested-in-P2SH)
- zpub/zprv: BIP-84 (native P2WPKH)

編碼結果:
- xpub: 111 字符
- xprv: 111 字符

地址驗證

interface AddressValidation {
  valid: boolean;
  type?: 'p2pkh' | 'p2sh';
  network?: 'mainnet' | 'testnet';
  error?: string;
}

function validateBase58Address(address: string): AddressValidation {
  try {
    const { version, payload } = base58CheckDecode(address);

    // 檢查 payload 長度
    if (payload.length !== 20) {
      return { valid: false, error: 'Invalid payload length' };
    }

    // 確定類型和網路
    let type: 'p2pkh' | 'p2sh';
    let network: 'mainnet' | 'testnet';

    switch (version) {
      case 0x00:
        type = 'p2pkh';
        network = 'mainnet';
        break;
      case 0x05:
        type = 'p2sh';
        network = 'mainnet';
        break;
      case 0x6f:
        type = 'p2pkh';
        network = 'testnet';
        break;
      case 0xc4:
        type = 'p2sh';
        network = 'testnet';
        break;
      default:
        return { valid: false, error: `Unknown version: ${version}` };
    }

    return { valid: true, type, network };
  } catch (e) {
    return { valid: false, error: (e as Error).message };
  }
}

// 通用地址驗證 (支持 Base58 和 Bech32)
function validateBitcoinAddress(address: string): {
  valid: boolean;
  format?: 'base58' | 'bech32';
  type?: string;
  network?: string;
  error?: string;
} {
  // 嘗試 Bech32
  if (address.toLowerCase().startsWith('bc1') ||
      address.toLowerCase().startsWith('tb1')) {
    const result = validateBech32Address(address);
    return { ...result, format: 'bech32' };
  }

  // 嘗試 Base58
  const result = validateBase58Address(address);
  return { ...result, format: result.valid ? 'base58' : undefined };
}

編碼效率比較

特性 Hex Base64 Base58
字符集大小 16 64 58
每字符 bits 4 6 ~5.86
20 bytes 長度 40 字符 28 字符 ~28 字符
人類可讀
混淆字符 有 (0/O, I/l)

相關資源

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