雲端 Mac 上的 iOS Merkle 證明驗收:分塊完整性與樹形邊界

雲端 Mac 上的 iOS Merkle 證明驗收:分塊完整性與樹形邊界

iOS 離線資源採用分塊下載時,即使每一塊都通過驗證,組合後仍可能是錯誤版本:用戶端可能接受了另一份清單的區塊,或是產生端與驗證端對奇數節點採用了不同規則。將驗證器放在雲端 Mac 的固定 Swift 環境中,先用小型樹驗收協定,再接入實際下載流程,比起下載完成後只比對一次整體雜湊,更容易定位問題。

固定樹協定與可信輸入

本文使用 SHA-256。葉節點計算為 H(0x00 || 原始區塊位元組),內部節點計算為 H(0x01 || 左雜湊 || 右雜湊);前綴用來區分這兩種輸入。雜湊值以原始的 32 位元組串接,不串接十六進位字串。

每層若剩下一個未配對的節點,就直接提升,不複製自身。證明由葉節點往根節點排列,只包含實際存在的兄弟節點;左右方向則依葉節點索引與目前層的寬度推導。

清單必須綁定資源版本、分塊長度、總位元組數、葉節點數量與根。最後一塊可以短於標準區塊長度;空資源的處理方式應另行定義,不應由驗證器自行猜測。

Merkle 證明回答的是「這個區塊是否屬於指定的根」,而不是「這個根由誰發布」。根與葉節點數量必須來自可信管道,或已通過簽章驗證的清單。

實作嚴格的證明驗證器

在雲端 Mac 上將以下內容儲存為 MerkleCheck.swift。先執行 xcrun swift --version 記錄工具鏈,再執行 xcrun swift MerkleCheck.swift。程式碼只使用系統框架,不需要下載第三方套件。

import Foundation
import CryptoKit

func leaf(_ bytes: Data) -> Data {
    Data(SHA256.hash(data: Data([0]) + bytes))
}

func parent(_ left: Data, _ right: Data) -> Data {
    Data(SHA256.hash(data: Data([1]) + left + right))
}

func verify(
    chunk: Data,
    index: Int,
    count: Int,
    siblings: [Data],
    root: Data
) -> Bool {
    guard count > 0, index >= 0, index < count,
          root.count == 32,
          siblings.allSatisfy({ $0.count == 32 })
    else { return false }

    var value = leaf(chunk)
    var position = index
    var width = count
    var consumed = 0

    while width > 1 {
        let hasSibling = position % 2 == 1 || position < width - 1
        if hasSibling {
            guard consumed < siblings.count else { return false }
            let sibling = siblings[consumed]
            value = position % 2 == 0
                ? parent(value, sibling)
                : parent(sibling, value)
            consumed += 1
        }
        position /= 2
        width = width / 2 + width % 2
    }

    return consumed == siblings.count && value == root
}

func require(_ condition: Bool, _ name: String) {
    guard condition else {
        fatalError("Check failed: \(name)")
    }
}

let a = Data("A".utf8)
let b = Data("B".utf8)
let c = Data("C".utf8)
let ab = parent(leaf(a), leaf(b))
let root = parent(ab, leaf(c))

require(verify(chunk: c, index: 2, count: 3,
               siblings: [ab], root: root), "odd promotion")
require(verify(chunk: a, index: 0, count: 3,
               siblings: [leaf(b), leaf(c)], root: root), "left branch")
require(!verify(chunk: Data("X".utf8), index: 2, count: 3,
                siblings: [ab], root: root), "tampered chunk")
require(!verify(chunk: c, index: 2, count: 3,
                siblings: [ab, ab], root: root), "excess proof")
require(!verify(chunk: c, index: 3, count: 3,
                siblings: [ab], root: root), "invalid index")

成功時腳本會安靜結束,失敗時則回傳非零狀態。將結束狀態存檔即可,不要以搜尋某句成功訊息的方式取代結束碼檢查。

用小型樹涵蓋樹形邊界

在三葉樹中,第三個葉節點會在第一層直接提升,因此只需要一個兄弟雜湊。若產生端改為複製最後一個節點,算出的根就會不同;這不是網路錯誤,也不應靠重試解決。

將腳本移入測試目標後,至少補齊下列檢查:

範例使用同一套雜湊函式建立根,因此只能驗證控制流程。還應透過獨立實作產生固定測試向量,將區塊位元組、根與證明的十六進位值提交至版本庫,避免產生器與驗證器共用同一個錯誤而未被發現。若不同位置的內容完全相同,單憑區塊位元組無法區分身分;需要綁定位置時,應在協定中加入索引,並明確定義整數編碼方式。

在寫入前完成驗證

下載層先依清單核對區塊長度與索引,再驗證證明。通過後,才寫入該資源版本的暫存目錄;所有區塊到齊後,檢查總長度並發布完成標記。不要讓介面將「檔案已存在」視為「資源可用」。

證明是不可信輸入,解析前應限制回應本文大小、節點數量與雜湊長度。有效證明的長度最多隨樹的層數成長,不應接受無上限的陣列。也要限制原始區塊大小,避免尚未開始驗證,就因載入整個區塊而耗盡記憶體。

重試必須沿用同一份清單快照。若下載期間發現新版本,應結束舊任務,或繼續下載舊版本,不能拿新根驗證舊區塊。驗收紀錄只需記下資源版本、區塊索引與失敗類別,不應輸出區塊內容或存取憑證。

建立可解讀的迴歸測試結果

在固定工具鏈上,分別執行正確與惡意測試向量。正確輸入遭到拒絕時,優先檢查奇數節點的處理規則、證明順序與位元組編碼;錯誤輸入被接受時,先檢查是否漏驗索引、剩餘證明節點,或根的可信來源。

最後進行一次端到端故障注入:替換暫存區塊中的一個位元組,確認該區塊遭到拒絕,且未產生完成標記;接著還原正確區塊,確認任務能夠繼續。驗收標準不是「雜湊程式碼執行完畢」,而是任何未通過證明驗證的資料,都不能進入可供讀取的資源集合。

常見問題

Merkle 根可以和資源一起下載嗎?

可以一起傳輸,但必須透過可信管道或已驗證簽章的清單,認證根及葉節點數量。若內容與根能被同時替換,證明仍可能通過。

奇數層最後一個節點需要複製自身嗎?

本文協定將該節點直接提升,不消耗相鄰節點的雜湊。複製自身是另一種規則,產生端與驗證端必須一致,不能混用。

把建置工作交給資料中心

租用專屬實體 Mac mini,將 Xcode 建置與 macOS CI 部署在全年穩定運作的主機上。依工作需求挑選規格,再依團隊所在地選擇節點。

選擇雲端 Mac

把建置工作交給資料中心

專屬實體 Mac mini,全年 365 天穩定運作。