# MiniFuzz in JAM Conformance Testing 實作細節整理 [Source Code](https://github.com/davxy/jam-conformance/blob/main/fuzz-proto/minifuzz/minifuzz.py) [Document](https://github.com/davxy/jam-conformance/blob/main/fuzz-proto/README.md) ## schema 使用 JAM codec 的訊息 schema(fuzz-v1.asn 對應的 coder/decoder)。 接收/回傳 decode/encode message ### Types: | Request | Response | 說明 | |---------------|-----------|------| | **PeerInfo** | PeerInfo | 連線握手、版本協商(需驗證 `fuzz_version` / `jam_version` / `fuzz_features`) | | **Initialize**| StateRoot | 初始化或重置 target 狀態,回傳初始狀態 root | | **ImportBlock** | StateRoot / Error | 匯入 block:<br> - 成功 → 回傳新的 state root <br> - 失敗(無效 block 等)→ 回 Error | | **GetState** | State | 回傳完整 key-value 狀態(當 fuzzer 偵測 state root mismatch 時觸發) | | *(Error)* | *(特殊)* | 只在協議允許的錯誤時使用(如 ImportBlock 失敗) | 溝通 message 皆是 length-prefix + JAM codec。 ### raw_to_json - 功能:把收到的 **原始位元組資料 (raw bytes)** 依照 JAM codec decode。 - 輸出:回傳一個 dict,內容包含 **message_kind** (例如 `peer_info`, `state_root`, `error` …) 與其對應的欄位資料。 - 用途:讓後續程式能依照 message 種類與欄位做驗證或比對。 ## 基本通訊 1. Unix domain socket 保持單一連線、同步 request-response 2. 訊息 framing:每個訊息用 4-byte little-endian 前綴長度(uint32 LE),接著完整 payload bytes。 3. Sequential:收到 request -> 處理並回應 -> 下一個 request ## 握手(PeerInfo) 連線建立後,target 必須等待 fuzzer 傳送 PeerInfo 訊息,處理完畢後回傳自己的 PeerInfo。 這是最初的握手程序,主要檢查雙方的協議版本與功能支援是否一致。 Reponse parse 後 包含 peer_info ### PeerInfo 結構 - fuzz_version:Fuzzer 協議版本 (須與預期一致) - jam_version:JAM 協議版本 (須與預期一致) - fuzz_features:bitmask,標示支援的功能 ```python target_peer_info = response["peer_info"] ``` fuzz_version, jam_version 需 match ```python # Check Fuzz version fuzz_version_got = target_peer_info.get("fuzz_version") fuzz_version_exp = precomputed_peer_info.get("fuzz_version") if fuzz_version_exp != fuzz_version_got: print(f"Unexpected Fuzzer protocol version. Expected: {fuzz_version_exp}, Got: {fuzz_version_got}") return False # Check JAM version jam_version_got = target_peer_info.get("jam_version") jam_version_exp = precomputed_peer_info.get("jam_version") if jam_version_exp != jam_version_got: print(f"Unexpected JAM protocol version. Expected: {jam_version_exp}, Got: {jam_version_got}") return False return True ``` fuzz_features 要用 bitmask(FEATURE_ANCESTRY(1), FEATURE_FORKS(2)),不得包含其他bit 。 ```python features_got = target_peer_info.get("fuzz_features") if features_got & ~FEATURES_MASK != 0: print(f"Unexpected Fuzzer features enabled: 0x{features_got:08x}") print("Valid features:") print(f"- Ancestry: 0x{FEATURE_ANCESTRY:08x}") print(f"- Forks: 0x{FEATURE_FORKS:08x}") return False ``` ## 其他訊息比對 (General Message Match) 除了 peer_info 這類需要額外欄位檢查的訊息,其餘所有訊息 都必須比對各欄位和預先定義的結果 完全一致。 ``` python # All other messages must match is_matching = response == precomputed if not is_matching: print("Unexpected target response") print("--------------------------") print("Expected:") print(json.dumps(precomputed, indent=4)) print("---") print("Returned:") print(json.dumps(response, indent=4)) return is_matching ``` 適用訊息種類 error 和 peer_info 以外的 type ## Initialize - 接收 Initialize:將 fuzzer 設為提供的初始 state。 - 回應當前 head 的 state root ## ImportBlock - ImportBlock,把 block 套用到目前 head - 成功:回 StateRoot (Fuzzer 會比對) - 失敗(block invalid / protocol-defined failure):回 Error(保持連線,等待 request)。 ## GetState - 要求 target 回傳完整 key/value state。 ## Error 處理 - 協議內錯誤(block invalid):回 Kind:Error 和 Error message(保持連線可繼續下一輪)。 ### Features - FEATURE_ANCESTRY: 在 Initialize 處理 ancestors, - FEATURE_FORKS:能接受同一 parent block 下的多個 mutations ### Minifuzz CLI Flags | Flag | Type | Default | 說明 | |------------------|--------|-------------------------|------| | `-d, --trace-dir` | str | 必須在 cli 提供 | 指定包含預先產生的 message 檔案目錄 | | `-s, --spec` | str | `tiny` (`tiny` / `full`) | 指定使用的 spec(tiny / full) | | `-v, --verbose` | bool | `false` | 開啟詳細輸出,印出完整 JSON 訊息 | | `--target-sock` | str | `/tmp/jam_target.sock` | 目標 target 綁定的 Unix domain socket 路徑 | | `--stop-after` | int | `1000` | 最多處理多少組 fuzzer/target 檔案 pair 後停止 | ### 主要程式 Flow - 1. 讀取 fuzzer binary 檔 使用 open(..., 'rb') 將整個 .bin 檔案載入 request_bytes(原始 bytes)。 ```python try: with open(fuzzer_file, 'rb') as f: request_bytes = f.read() except Exception as e: print(f"Error reading fuzzer file '{fuzzer_file}': {e}") break ``` - 2. decode(轉成可讀 JSON / dict) raw_to_json(request_bytes):把 JAM codec 的 raw bytes decode 成 Python dict。 print_message(...):在 verbose 模式下印出完整 JSON;否則僅印 <message_kind>。 ```python try: request_msg = raw_to_json(request_bytes) print_message(request_msg, True, args.verbose) except Exception as e: print(f"Error decoding precomputed fuzzer request: {e}") break ``` - 3. 傳送(framing 規則) framing 規則:4-byte little-endian 長度前綴(struct.pack('<I', length)) + payload bytes。 ```python try: request_len = len(request_bytes) request_len_bytes = struct.pack('<I', request_len) sock.sendall(request_len_bytes) sock.sendall(request_bytes) if args.verbose: print(f"Sent {request_len} bytes from fuzzer file") except Exception as e: print(f"Error sending fuzzer request: {e}") break ``` - 4. 接收回應(framing) 先用 recv(4) 讀 4 bytes length。 再用 loop 持續 recv 直到讀到 response_len bytes(recv 不是一次讀完的保證)。 ```python try: response_len_bytes = sock.recv(4) if len(response_len_bytes) != 4: print("Error: Could not read response length") break response_len = struct.unpack('<I', response_len_bytes)[0] response_data = b'' while len(response_data) < response_len: chunk = sock.recv(response_len - len(response_data)) if not chunk: print("Error: Connection closed while reading response") break response_data += chunk if args.verbose: print(f"Received {response_len} bytes response") except Exception as e: print(f"Error reading target response: {e}") break ``` - 5. decode target 回應(轉成可讀 JSON / dict) 用 raw_to_json(response_data) 把 target 回傳 bytes decode 成 dict。 <message_kind>+ 跟收到的內容。 ```python try: response_msg = raw_to_json(response_data) print_message(response_msg, False, args.verbose) except Exception as e: print(f"Error decoding target response: {e}") break ``` - 6. 讀取預期回應檔並比對 每個 fuzzer_xxx.bin 對應一個 target_xxx.bin(同序列 index)作為該 request 的預期回應。 把 target_xxx.bin decode 成 precomputed_response_msg,呼叫 response_check(...) 做比對: peer_info 有特別規則(檢查 fuzz_version、jam_version、fuzz_features bitmask)。 其他 message 預期完全相等(字典 equality)。 若比對失敗則停止(break),方便後續調查差異。 ```python try: with open(target_file, 'rb') as f: precomputed_data = f.read() precomputed_response_msg = raw_to_json(precomputed_data) except Exception as e: print(f"Error reading precomputed target file '{target_file}': {e}") break if not response_check(response_msg, precomputed_response_msg): break ``` ## Questions & Discussions 整理當前實作 ### Each file fuzz.go → CLI,直接呼叫 library: - fuzz.NewFuzzServer → 啟 server - fuzz.NewFuzzClient → 建 client - client.Handshake / ImportBlock / SetState / GetState → 發 request client.go → 測試端,送 request 給 server server.go → 接收 request,轉發給 service service.go → 呼叫 JAM 本體的邏輯(store, stf, merklization) messages.go → 定義 client/server 之間的協議格式 decode/encode byte/json errors.go → 錯誤定義 ### Client functions #### Message framing - [len (4 bytes LE)] + [type (1 byte)] + payload #### Handshake client 啟動時 handshake,拿到 server 的 PeerInfo。 #### ImportBlock Send ImportBlock (Block) → Get StateRoot #### SetState Send (Header, StateKeyVals) → Get StateRoot #### GetState - Go:Send HeaderHash → Get StateKeyVals ### To discuss #### Error Handling(增加 Error Type?) - Python fuzzer 預期 Server 遇到錯誤時回 Error + 訊息。 - Go 目前只有 local error log,沒送 Message 回 client。 Go: ```go const ( MessageType_PeerInfo MessageType = 0 MessageType_ImportBlock MessageType = 1 MessageType_SetState MessageType = 2 MessageType_GetState MessageType = 3 MessageType_State MessageType = 4 MessageType_StateRoot MessageType = 5 ) ``` Python: ``` PeerInfo ImportBlock SetState (Initialize) GetState State StateRoot ``` #### Naming Alignment - Python:Initialize - Go:SetState #### GetState 限制 Go GetState 只支援 parent block 的 state : 符合期望? ### Server functions #### Message framing - 接收:[len (4 bytes LE)] + [type (1 byte)] + payload → decode - 回傳:encode Message → [len (4 bytes LE)] + [type (1 byte)] + payload #### Handshake - Input:Client PeerInfo - Output:Server 回 PeerInfo -> function:Handshake() #### ImportBlock - Input:Client 送 Block - 處理:Service.ImportBlock(block) → 驗證 parent hash / state_root → 更新 state - Output:回傳 StateRoot #### SetState (Initialize) - Input:Client 送 (Header, StateKeyVals) - 處理:Service.SetState(header, state) → 更新 store → Merklization 得到新的 StateRoot - Output:回傳 StateRoot #### GetState - Input:Client 送 HeaderHash - 處理:Service.GetState(hash) → 查 store 的狀態 - Output:回傳完整的 StateKeyVals ### Service.go #### Handshake - Input:Client 傳入一份 PeerInfo - 處理:忽略 Client 的內容(需加入check bit?),直接讀取本地 config.Config 生成 PeerInfo - Output:Server 端的 PeerInfo #### ImportBlock - Input:Client 傳入 Block - 取出 store 最新 block → 算出最新 block hash - 驗證 block.Header.Parent 是否對得上 - 驗證 block.Header.ParentStateRoot 是否對得上 - 如果驗證成功 → 把 block 存進 store → 跑 STF (RunSTF()) → 算出新的 StateRoot - Output:新的 StateRoot #### SetState (Initialize) - Input:Client 傳入 (Header, StateKeyVals) - StateKeyVals → State - 設定到 store PosteriorState - Merklization 算出 StateRoot - Output:新的 StateRoot #### GetState - Input:Client 傳入 HeaderHash - 檢查 hash 是否等於當前 ProcessingBlockPointer 的 parent - 如果不相符 → return error - 如果符合 → 取出當前 PosteriorStates - Encode 成 StateKeyVals - Output:StateKeyVals #### Error Handling - 目前 Service 會 return error,交給 server.go 處理。 - server.go 沒有把 error 包裝成 Error message 回給 client,導致 fuzzer 端會 mismatch。