跳至主要內容
進階

Compact Serialization

深入了解比特幣的緊湊序列化格式,包括 CompactSize、VarInt 和其他編碼方式。

10 分鐘

比特幣使用多種緊湊序列化格式來減少數據大小。理解這些格式對於解析原始交易 和區塊數據至關重要。

CompactSize (VarInt)

CompactSize 是比特幣中最常見的可變長度整數編碼。它用於表示交易中的 輸入/輸出數量、腳本長度等。

編碼規則

值範圍 格式 位元組數
0 - 252 [value] 1
253 - 0xFFFF [0xFD][value LE16] 3
0x10000 - 0xFFFFFFFF [0xFE][value LE32] 5
0x100000000 - 0xFFFFFFFFFFFFFFFF [0xFF][value LE64] 9
// 編碼示例
100      -> [0x64]                           // 1 byte
255      -> [0xFD][0xFF][0x00]               // 3 bytes
65535    -> [0xFD][0xFF][0xFF]               // 3 bytes
70000    -> [0xFE][0x70][0x11][0x01][0x00]   // 5 bytes

// C++ 實現
void WriteCompactSize(std::vector& data, uint64_t nSize)
{
    if (nSize < 253) {
        data.push_back(nSize);
    } else if (nSize <= 0xFFFF) {
        data.push_back(0xFD);
        WriteLE16(data, nSize);
    } else if (nSize <= 0xFFFFFFFF) {
        data.push_back(0xFE);
        WriteLE32(data, nSize);
    } else {
        data.push_back(0xFF);
        WriteLE64(data, nSize);
    }
}

Python 解碼範例

def read_compact_size(stream):
    """從位元組流讀取 CompactSize"""
    first = stream.read(1)[0]

    if first < 253:
        return first
    elif first == 253:
        return int.from_bytes(stream.read(2), 'little')
    elif first == 254:
        return int.from_bytes(stream.read(4), 'little')
    else:  # 255
        return int.from_bytes(stream.read(8), 'little')

def write_compact_size(value):
    """將整數編碼為 CompactSize"""
    if value < 253:
        return bytes([value])
    elif value <= 0xFFFF:
        return bytes([0xFD]) + value.to_bytes(2, 'little')
    elif value <= 0xFFFFFFFF:
        return bytes([0xFE]) + value.to_bytes(4, 'little')
    else:
        return bytes([0xFF]) + value.to_bytes(8, 'little')

交易序列化

交易使用 CompactSize 來表示可變長度欄位:

Transaction 序列化:
├── version (4 bytes, LE)
├── [marker=0x00, flag=0x01]  // SegWit only
├── input_count (CompactSize)
├── inputs[]
│   ├── prev_txid (32 bytes)
│   ├── prev_vout (4 bytes, LE)
│   ├── script_length (CompactSize)
│   ├── scriptSig (variable)
│   └── sequence (4 bytes, LE)
├── output_count (CompactSize)
├── outputs[]
│   ├── value (8 bytes, LE)
│   ├── script_length (CompactSize)
│   └── scriptPubKey (variable)
├── witness[] (SegWit only)
│   └── per input:
│       ├── item_count (CompactSize)
│       └── items[]
│           ├── item_length (CompactSize)
│           └── item_data (variable)
└── locktime (4 bytes, LE)

nBits (Compact Target)

區塊頭中的難度目標使用特殊的緊湊格式:

// nBits 格式
// [exponent (1 byte)][coefficient (3 bytes)]

// 轉換公式
target = coefficient × 256^(exponent - 3)

// 示例: nBits = 0x1d00ffff (創世區塊)
exponent = 0x1d = 29
coefficient = 0x00ffff
target = 0x00ffff × 256^(29-3)
       = 0x00000000ffff0000000000000000000000000000000000000000000000000000

// Python 實現
def nbits_to_target(nbits):
    exponent = nbits >> 24
    coefficient = nbits & 0x007fffff

    if exponent <= 3:
        target = coefficient >> (8 * (3 - exponent))
    else:
        target = coefficient << (8 * (exponent - 3))

    # 處理負數標誌
    if nbits & 0x00800000:
        target = -target

    return target

Compact Block Short IDs

BIP-152 使用 6 位元組的短 ID 來表示交易:

// 短 ID 計算
short_id = SipHash-2-4(k0, k1, wtxid) & 0xffffffffffff

// k0, k1 來自區塊頭和 nonce
key = SHA256(block_header || nonce)
k0 = key[0:8]
k1 = key[8:16]

// 這允許用 6 bytes 代替 32 bytes 的 txid
// 在大多數情況下不會發生碰撞

Script 數字編碼

比特幣腳本使用特殊的數字表示法:

// 小整數優化
OP_0         -> 0
OP_1..OP_16  -> 1..16
OP_1NEGATE   -> -1

// 一般整數 (Script Number)
// 最小編碼,小端序,符號位在最高位

0      -> []            // 空
1      -> [0x01]
127    -> [0x7f]
128    -> [0x80, 0x00]  // 需要額外字節避免負數解釋
255    -> [0xff, 0x00]
256    -> [0x00, 0x01]
-1     -> [0x81]
-127   -> [0xff]
-128   -> [0x80, 0x80]

// Python 實現
def encode_script_number(n):
    if n == 0:
        return b''

    negative = n < 0
    n = abs(n)
    result = []

    while n:
        result.append(n & 0xff)
        n >>= 8

    if result[-1] & 0x80:
        result.append(0x80 if negative else 0x00)
    elif negative:
        result[-1] |= 0x80

    return bytes(result)

Bech32 五位元編碼

Bech32 地址使用 5-bit 分組:

// 8-bit 到 5-bit 轉換
// 每 8 個 bytes 變成 13 個 5-bit 值

def convertbits(data, frombits, tobits):
    acc = 0
    bits = 0
    result = []
    maxv = (1 << tobits) - 1

    for value in data:
        acc = (acc << frombits) | value
        bits += frombits
        while bits >= tobits:
            bits -= tobits
            result.append((acc >> bits) & maxv)

    if bits:
        result.append((acc << (tobits - bits)) & maxv)

    return result

// 示例
witness_program = bytes.fromhex("751e76e8...")
five_bit = convertbits(witness_program, 8, 5)

PSBT 鍵值編碼

PSBT(BIP-174)使用鍵值對格式:

PSBT 結構:
├── magic: "psbt" + 0xff
├── global_map
│   └── key-value pairs
├── per_input_maps[]
│   └── key-value pairs
└── per_output_maps[]
    └── key-value pairs

每個 key-value pair:
├── key_length (CompactSize)
├── key_type (1 byte)
├── key_data (variable)
├── value_length (CompactSize)
└── value_data (variable)

map 結束符: 0x00

常見陷阱

  • 位元組序:大多數值是小端序(Little Endian),但雜湊值顯示為大端序
  • 非規範編碼:CompactSize 應使用最小編碼,否則可能被拒絕
  • 符號處理:Script 數字使用符號-量值表示法
  • 長度限制:某些欄位有最大長度限制(如腳本 10,000 bytes)
已複製連結
已複製到剪貼簿