#!/usr/bin/env python3 """ Generate a C++ cross-check header from the reference implementation (motalib). Emits test/test_ota/mota_vectors.h containing a real .mota built by the Python tool plus the expected parse results and a merkle proof, so the device-side C++ core (src/helpers/ota/) is verified to agree byte-for-byte with the host packager. Run: ./meshcore/bin/python tools/mota/gen_vectors.py """ from __future__ import annotations import random import sys from pathlib import Path import motalib as ml ROOT = Path(__file__).resolve().parents[2] OUT = ROOT / "test" / "test_ota" / "mota_vectors.h" sys.path.insert(0, str(ROOT / "scripts")) from zopfli_compress import raw_deflate_compress # noqa: E402 def _carr(name, data: bytes) -> str: body = ",".join(str(b) for b in data) return f"static const uint8_t {name}[{len(data)}] = {{{body}}};\n" def _u16arr(name: str, values: list[int]) -> str: body = ",".join(str(value) for value in values) return f"static const uint16_t {name}[{len(values)}] = {{{body}}};\n" def build_full(): random.seed(42) fw = bytes(random.getrandbits(8) for _ in range(5 * 1024 + 137)) # 6 blocks, last short image, _ = ml.ensure_endf(fw) m = ml.build_manifest( target_id=0x11223344, fw_version=ml.pack_version("1.16.0"), image_size=len(image), payload=image, block_size=1024, image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL, is_full=True, hw_id="TESTHW") return ml.build_container(m, image), m, image def emit_proof_case(idx, count) -> str: """A tree of `count` arbitrary 4-byte leaves + every block's proof, computed by the reference.""" random.seed(1000 + count) leaves = [bytes(random.getrandbits(8) for _ in range(4)) for _ in range(count)] root = ml.merkle_root(leaves) blob = b"" offs, nsibs = [], [] for i in range(count): sib = ml.proof_siblings(leaves, i) assert ml.verify_proof(leaves[i], i, ml.merkle_proof(leaves, i), root, count), (count, i) offs.append(len(blob)) nsibs.append(len(sib) // 4) blob += sib p = f"T{idx}" out = [ f"static const uint32_t {p}_COUNT = {count}u;", _carr(f"{p}_LEAVES", b"".join(leaves)), _carr(f"{p}_ROOT", root), f"static const uint16_t {p}_POFF[{count}] = {{{','.join(str(o) for o in offs)}}};", f"static const uint8_t {p}_PNSIB[{count}] = {{{','.join(str(n) for n in nsibs)}}};", _carr(f"{p}_PBLOB", blob), ] return "\n".join(out) def main(): blob, m, image = build_full() leaves = ml.leaf_hashes(image, 1024) assert ml.merkle_root(leaves) == m.merkle_root proof_idx = 2 if m.block_count > 2 else 0 siblings = ml.proof_siblings(leaves, proof_idx) assert ml.verify_proof(leaves[proof_idx], proof_idx, ml.merkle_proof(leaves, proof_idx), m.merkle_root, len(leaves)) lines = [ "// AUTO-GENERATED by tools/mota/gen_vectors.py - do not edit by hand.", "// Cross-check vectors: a real .mota from the reference packager (motalib.py).", "#pragma once", "#include ", "", _carr("MOTA_VEC", blob), f"static const uint32_t MOTA_VEC_LEN = {len(blob)};", f"static const uint32_t EXP_TARGET_ID = 0x{m.target_id:08x}u;", f"static const uint32_t EXP_FW_VERSION = 0x{m.fw_version:08x}u;", f"static const uint32_t EXP_IMAGE_SIZE = {m.image_size}u;", f"static const uint32_t EXP_PAYLOAD_SIZE = {m.payload_size}u;", f"static const uint32_t EXP_BLOCK_COUNT = {m.block_count}u;", f"static const uint8_t EXP_BLOCK_SIZE_LOG2 = {m.block_size_log2};", f"static const uint8_t EXP_CODEC_ID = {m.codec_id};", _carr("EXP_MERKLE_ROOT", m.merkle_root), _carr("EXP_IMAGE_HASH", m.image_hash), _carr("EXP_HW_ID", m.hw_id), f"static const uint32_t PROOF_INDEX = {proof_idx}u;", f"static const uint8_t PROOF_NSIB = {len(siblings)//4};", _carr("PROOF_SIBLINGS", siblings), ] # exhaustive per-index proof cases for several tricky counts (deep promotion chains) counts = [5, 7, 8, 65, 100, 255, 256] for i, c in enumerate(counts): lines.append(emit_proof_case(i, c)) lines.append("struct ProofCase { uint32_t count; const uint8_t* leaves; const uint8_t* root;" " const uint16_t* poff; const uint8_t* pnsib; const uint8_t* pblob; };") rows = ",".join(f"{{T{i}_COUNT,T{i}_LEAVES,T{i}_ROOT,T{i}_POFF,T{i}_PNSIB,T{i}_PBLOB}}" for i in range(len(counts))) lines.append(f"static const ProofCase PROOF_CASES[] = {{{rows}}};") lines.append(f"static const int N_PROOF_CASES = {len(counts)};") # small-block signed .mota for the host transfer simulation (each block fits one packet => no # fragmentation needed to validate the manager/protocol end-to-end). Deterministic signing key. from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey random.seed(99) sim_fw = bytes(random.getrandbits(8) for _ in range(2000)) sim_image, _ = ml.ensure_endf(sim_fw) sim_priv = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) sm = ml.build_manifest(target_id=0xCAFEBABE, fw_version=ml.pack_version("3.0.0"), image_size=len(sim_image), payload=sim_image, block_size=128, image_hash=ml.mh32(sim_image), codec_id=ml.CODEC_FULL, is_full=True, sign_priv=sim_priv) sim_blob = ml.build_container(sm, sim_image) lines.append("// small-block signed .mota for the OtaManager host transfer simulation") lines.append(_carr("SIM_MOTA", sim_blob)) lines.append(f"static const uint32_t SIM_MOTA_LEN = {len(sim_blob)};") lines.append(f"static const uint32_t SIM_TARGET_ID = 0x{sm.target_id:08x}u;") # 1 KB-block signed .mota: each logical block spans MULTIPLE LoRa DATA fragments (1024 / 160 = 7), # so this exercises the multi-fragment reassembly + split proof path the device actually uses. sm1k = ml.build_manifest(target_id=0xCAFEBABE, fw_version=ml.pack_version("3.0.0"), image_size=len(sim_image), payload=sim_image, block_size=1024, image_hash=ml.mh32(sim_image), codec_id=ml.CODEC_FULL, is_full=True, sign_priv=sim_priv) sim1k_blob = ml.build_container(sm1k, sim_image) lines.append("// 1 KB-block signed .mota (multi-fragment per block) for the reassembly transfer test") lines.append(_carr("SIM_MOTA_1K", sim1k_blob)) lines.append(f"static const uint32_t SIM_MOTA_1K_LEN = {len(sim1k_blob)};") lines.append(f"static const uint32_t SIM_MOTA_1K_BLOCKS = {sm1k.block_count};") # 2 KiB-block signed .mota: one full block plus a short final block. The payload is deliberately # compressible so its independently generated raw-RFC1951 streams exercise both extended-length # DEFLATE descriptors, including the rule that a short tail still uses the manifest's 2 KiB profile. sim2k_fw_len = 2 * 1024 + 137 sim2k_phrase = b"MeshCore 2 KiB transport DEFLATE vector. " sim2k_fw = (sim2k_phrase * ((sim2k_fw_len + len(sim2k_phrase) - 1) // len(sim2k_phrase)))[:sim2k_fw_len] sim2k_image, _ = ml.ensure_endf(sim2k_fw) sm2k = ml.build_manifest(target_id=0xCAFEBABE, fw_version=ml.pack_version("3.1.0"), image_size=len(sim2k_image), payload=sim2k_image, block_size=2048, image_hash=ml.mh32(sim2k_image), codec_id=ml.CODEC_FULL, is_full=True, sign_priv=sim_priv) sim2k_blob = ml.build_container(sm2k, sim2k_image) compressed = bytearray() compressed_offsets = [] compressed_lengths = [] for offset in range(0, len(sim2k_image), 2048): block = sim2k_image[offset:offset + 2048] encoded = raw_deflate_compress(block) assert 0 < len(encoded) < len(block), (len(block), len(encoded)) compressed_offsets.append(len(compressed)) compressed_lengths.append(len(encoded)) compressed.extend(encoded) assert len(compressed_offsets) == sm2k.block_count lines.append("// 2 KiB-block signed .mota: one full block plus a short tail") lines.append(_carr("SIM_MOTA_2K", sim2k_blob)) lines.append(f"static const uint32_t SIM_MOTA_2K_LEN = {len(sim2k_blob)};") lines.append(f"static const uint32_t SIM_MOTA_2K_BLOCKS = {sm2k.block_count};") lines.append(_carr("SIM_MOTA_2K_DEFLATED", bytes(compressed))) lines.append(_u16arr("SIM_MOTA_2K_DEFLATED_OFFSETS", compressed_offsets)) lines.append(_u16arr("SIM_MOTA_2K_DEFLATED_LENGTHS", compressed_lengths)) # detools sequential+crle delta vector: base image, the real detools 0.53.0 patch, and the # expected target image. The native test applies DT_PATCH to DT_BASE with the *vendored detools # C decoder* (src/helpers/ota/detools) and must reproduce DT_TARGET byte-for-byte -- proving the # on-device delta apply path uses detools, not a reimplementation. import io import detools random.seed(2024) dt_base_body = bytes(random.getrandbits(8) for _ in range(3000)) dt_tgt = bytearray(dt_base_body) for off in (137, 138, 139, 1500, 2999): # localized edits, version-bump style dt_tgt[off] ^= 0x5A dt_tgt += bytes(random.getrandbits(8) for _ in range(200)) # small appended tail dt_base_img, _ = ml.ensure_endf(dt_base_body) dt_tgt_img, _ = ml.ensure_endf(bytes(dt_tgt)) fp = io.BytesIO() detools.create_patch(io.BytesIO(dt_base_img), io.BytesIO(dt_tgt_img), fp, patch_type="sequential", compression="crle") dt_patch = fp.getvalue() lines.append("// detools sequential+crle delta: apply DT_PATCH to DT_BASE -> DT_TARGET") lines.append(_carr("DT_BASE", dt_base_img)) lines.append(f"static const uint32_t DT_BASE_LEN = {len(dt_base_img)};") lines.append(_carr("DT_PATCH", dt_patch)) lines.append(f"static const uint32_t DT_PATCH_LEN = {len(dt_patch)};") lines.append(_carr("DT_TARGET", dt_tgt_img)) lines.append(f"static const uint32_t DT_TARGET_LEN = {len(dt_tgt_img)};") # detools IN-PLACE+crle delta (nRF52 single-slot apply): apply DT_IP_PATCH to a memory region # holding DT_IP_BASE -> region[0:to_size] == DT_IP_TARGET. The native test runs the vendored # in-place decoder over a DT_IP_MEM-byte RAM buffer, exactly as the bootloader will over flash. DT_IP_MEM, DT_IP_SEG = 0x8000, 0x1000 ip_patch_io = io.BytesIO() detools.create_patch(io.BytesIO(dt_base_img), io.BytesIO(dt_tgt_img), ip_patch_io, patch_type="in-place", memory_size=DT_IP_MEM, segment_size=DT_IP_SEG, compression="crle") dt_ip_patch = ip_patch_io.getvalue() lines.append("// detools in-place+crle delta: apply DT_IP_PATCH over a DT_IP_MEM buffer holding DT_IP_BASE") lines.append(_carr("DT_IP_BASE", dt_base_img)) lines.append(f"static const uint32_t DT_IP_BASE_LEN = {len(dt_base_img)};") lines.append(_carr("DT_IP_PATCH", dt_ip_patch)) lines.append(f"static const uint32_t DT_IP_PATCH_LEN = {len(dt_ip_patch)};") lines.append(_carr("DT_IP_TARGET", dt_tgt_img)) lines.append(f"static const uint32_t DT_IP_TARGET_LEN = {len(dt_tgt_img)};") lines.append(f"static const uint32_t DT_IP_MEM = {DT_IP_MEM}u;") OUT.parent.mkdir(parents=True, exist_ok=True) OUT.write_text("\n".join(lines) + "\n") print(f"wrote {OUT}") print(f" blob={len(blob)}B blocks={m.block_count} proof_idx={proof_idx} nsib={len(siblings)//4}") print(f" proof cases: counts={counts}") print(f" detools delta: base={len(dt_base_img)}B target={len(dt_tgt_img)}B seq_patch={len(dt_patch)}B inplace_patch={len(dt_ip_patch)}B") if __name__ == "__main__": main()