# HushChat Protocol Spec (2026-05-06)

Scope:
- Document the memo protocol observed in `external/SilentDragonXLite` for a future safe ImGui implementation.
- Batch 1 implements header parsing/validation only.
- Batch 2 implements transaction-output grouping for validated header/payload memo pairs only.
- Batch 3 implements read-only transaction extraction as sanitized refresh metadata behind `DRAGONX_ENABLE_CHAT`.
- Batch 4 implements pre-decryption validation/error typing only.
- Batch 5 implements decrypt-input byte boundary preparation and fixture readiness only.
- Batch 6 implements non-sensitive cross-wallet fixture shape verification only.
- Batch 7 implements fixture-file loading for pending and ready compatibility vectors.
- Batch 8 implements a strict import checklist for replacing pending fixture placeholders only when all required categories are supplied.
- Batch 9 implements a no-plaintext SDXL seed/public-key projection verifier for ready fixture files.
- Batch 10 implements an inert corrupted-authentication-failure readiness scaffold for future secretstream auth failure checks.
- Batch 11 implements a strict ready-fixture replacement dry-run report for real non-sensitive SDXL vectors.
- Batch 12 implements a redacted capture-manifest validator for staged real-vector directories.
- These batches do not decrypt, store, render, or send chat messages.

## Reference Sources

- `external/SilentDragonXLite/src/chatmodel.cpp`
  - `MainWindow::createHeaderMemo(...)`
  - `MainWindow::createTxFromChatPage()`
  - `MainWindow::createTxForSafeContactRequest()`
- `external/SilentDragonXLite/src/controller.cpp`
  - `Controller::refreshTransactions()` outgoing/incoming memo handling
- `external/SilentDragonXLite/src/DataStore/ChatDataStore.*`
- `external/SilentDragonXLite/src/Model/ChatItem.*`
- `external/SilentDragonXLite/src/Model/ContactItem.*`
- `external/SilentDragonXLite/src/Model/ContactRequest.*`

## Transaction Shape

SilentDragonXLite sends HushChat data as shielded transactions with paired memo outputs:

1. Header memo: UTF-8 JSON object beginning with `{`.
2. Payload memo: non-JSON string paired with the same transaction.

For encrypted chat messages, the payload memo is libsodium secretstream ciphertext encoded as hex. For contact requests, the payload memo is plaintext request text in the reference implementation.

The reference may also add unrelated dust/noise memo outputs around the chat pair, so a future parser must group by transaction, output order/position, and validated header metadata rather than assuming the transaction contains only two outputs.

## Header JSON

`createHeaderMemo(...)` emits short field names to preserve memo space:

```json
{
  "h": 1,
  "v": 0,
  "z": "zs-reply-address",
  "cid": "conversation-id",
  "t": "Memo",
  "e": "48 hex chars for message headers, empty for contact requests",
  "p": "64 hex chars"
}
```

Fields:
- `h`: integer header number, starting at `1`.
- `v`: integer HushChat version. Current reference uses `0`.
- `z`: sender reply z-address.
- `cid`: conversation/contact id.
- `t`: type string. Known values are `Memo` and `Cont`.
- `e`: hex-encoded `crypto_secretstream_xchacha20poly1305_HEADERBYTES` value. Present for `Memo`, empty for `Cont` in the reference.
- `p`: hex-encoded `crypto_kx_PUBLICKEYBYTES` public key generated from `crypto_kx_seed_keypair()`.

## Type Semantics

`Memo`:
- Header includes non-empty `e` with 24 bytes encoded as 48 hex characters.
- Payload memo is ciphertext encoded as hex.
- Reference derives a deterministic keypair from the wallet/chat password, derives a shared session key with the peer public key, and decrypts via `crypto_secretstream_xchacha20poly1305_pull()`.

`Cont`:
- Header includes empty `e`.
- Payload memo is the contact request text.
- Header `z`, `cid`, and `p` are used to create/update a contact record.

## SilentDragonXLite Decryption Audit

Reference initialization:
- `main.cpp` calls `sodium_init()` at startup and aborts if libsodium cannot initialize.

Reference chat key material:
- Wallet/chat passphrase input is wrapped with fixed strings (`DRGX` + user passphrase + `SDXL`).
- The wrapped passphrase is hashed with `blake3_PW(...)`.
- `crypto_pwhash(...)` is then called with `crypto_pwhash_OPSLIMIT_SENSITIVE`, `crypto_pwhash_MEMLIMIT_SENSITIVE`, and `crypto_pwhash_ALG_DEFAULT`, producing `crypto_box_SEEDBYTES` bytes.
- The resulting bytes are hex-encoded and stored in `ChatDataStore::_password` for later HushChat operations.

Reference `Memo` send path:
- `ChatDataStore::getPassword()` returns the stored hex chat key string.
- The Qt code passes `passphraseHash.toUtf8()` directly as the seed input to `crypto_kx_seed_keypair(pk, sk, seed)`.
- The peer public key is loaded from contact/address-book metadata using `QByteArray::fromHex(...)`.
- Sending uses `crypto_kx_server_session_keys(server_rx, server_tx, pk, sk, peer_pk)` and encrypts with `server_tx`.
- `crypto_secretstream_xchacha20poly1305_init_push(&state, header, server_tx)` creates the 24-byte stream header.
- `crypto_secretstream_xchacha20poly1305_push(..., TAG_FINAL)` encrypts the UTF-8 message, producing `crypto_secretstream_xchacha20poly1305_ABYTES + plaintext_length` bytes.
- The header memo stores `e` as the stream header hex and `p` as the sender public key hex.
- The payload memo stores ciphertext hex.

Reference outgoing-history decrypt path:
- Re-derives the local deterministic keypair from `ChatDataStore::getPassword()`.
- Looks up the peer public key by address.
- Uses `crypto_kx_server_session_keys(...)` and decrypts with `server_tx`.
- Uses `crypto_secretstream_xchacha20poly1305_init_pull(&state, header, server_tx)` and `crypto_secretstream_xchacha20poly1305_pull(...)`.
- Stores decrypted text as `ChatItem` in `ChatDataStore`.

Reference incoming decrypt path:
- Parses header memo fields `cid`, `t`, `z`, `e`, and `p` from transaction memo JSON.
- Stores/caches `cid`, reply z-address, secretstream header, and sender public key.
- Re-derives the local deterministic keypair from `ChatDataStore::getPassword()`.
- Uses `crypto_kx_client_session_keys(client_rx, client_tx, pk, sk, peer_pk)` and decrypts with `client_rx`.
- Uses `crypto_secretstream_xchacha20poly1305_init_pull(&state, header, client_rx)` and `crypto_secretstream_xchacha20poly1305_pull(...)`.
- Stores decrypted text/contact request text as `ChatItem` in `ChatDataStore`.

Open questions before real decryption:
- Confirm the `crypto_pwhash(...)` salt input length and derivation semantics from the BLAKE3 output. The reference passes a pointer derived from a string without an explicit salt object.
- Confirm outgoing-vs-incoming role selection (`server_tx` for sends/outgoing history, `client_rx` for received messages) against cross-wallet fixtures.
- Decide whether new DragonX-originated messages must preserve SilentDragonXLite's UTF-8-hex seed behavior forever or can support a versioned decoded-byte seed later.
- Decide whether Batch 3 metadata fields `cid` and reply z-address are safe to persist, hash, or keep transient only.
- Decide how corrupted ciphertext/header failures should surface to users without logging memo contents or plaintext.

## Batch 1 Parser Contract

Implemented parser: `dragonx::chat::parseHushChatHeaderMemo()`.

The parser accepts only the clear, structural header format:
- memo must be non-empty JSON object text beginning with `{`
- maximum memo size is 512 bytes
- required fields must exist and have expected JSON types
- only version `0` is accepted
- only `Memo` and `Cont` types are accepted
- public key must be 32 bytes encoded as 64 hex characters
- `Memo` must include a 24 byte secretstream header encoded as 48 hex characters
- `Cont` must have an empty secretstream header

The parser intentionally does not:
- decrypt ciphertext
- derive keys
- parse contact request payload text
- write chat storage
- expose UI
- log memo contents

## Batch 2 Transaction Grouping Contract

Implemented grouping helper: `dragonx::chat::groupHushChatMemoOutputs()`.

Inputs:
- one transaction's memo outputs as `(position, memo)` records
- output `position` should be the wallet/RPC output position where available
- equal positions retain caller order as a stable fallback

Output:
- zero or more validated HushChat header/payload pairs
- grouping issues for malformed or incomplete HushChat-looking data
- ignored memo count for unrelated dust/noise payloads

Grouping rules:
- outputs are processed in ascending output position
- non-header memos before any valid header are ignored as dust/noise
- memos larger than 512 bytes produce `OversizedMemo` and are skipped
- memos beginning with `{` must parse as valid HushChat headers or produce `InvalidHeader`
- a valid header opens a pending pair
- a valid `Memo` header pairs with the next non-header memo that is non-empty, <=512 bytes, even-length hex
- a valid `Cont` header pairs with the next non-header memo that is non-empty and <=512 bytes
- non-matching payload candidates after a pending `Memo` header are ignored so unrelated dust/noise can appear before ciphertext
- a second valid header before a payload produces `DuplicateHeader`; the previous header also produces `MissingPayload`, then the new header becomes pending
- a valid header left pending at end-of-transaction produces `MissingPayload`

The grouping helper intentionally does not:
- decrypt ciphertext
- inspect contact request payload text
- decide whether a pair belongs to the local wallet
- write storage
- trigger notifications/UI
- run from the transaction refresh pipeline while `DRAGONX_ENABLE_CHAT` is off

## Batch 3 Transaction Extraction Contract

Implemented extractor: `dragonx::chat::extractHushChatTransactionMetadata()`.

Inputs:
- one transaction id
- one transaction's memo outputs as `(position, memo)` records
- an explicit feature-enabled switch, defaulting to the build-time `DRAGONX_ENABLE_CHAT` value

Output when enabled:
- zero or more sanitized transaction metadata records
- sanitized grouping issues for HushChat-looking malformed data
- ignored memo count for unrelated dust/noise payloads

Sanitized metadata fields:
- transaction id
- HushChat type (`Memo` or `Cont`)
- conversation id
- reply z-address
- header output position
- payload output position
- payload memo byte length only

The extractor intentionally does not expose:
- payload memo contents
- secretstream header hex
- peer public key hex
- decrypted plaintext
- derived keys

Refresh integration:
- `NetworkRefreshService::TransactionRefreshResult` carries transient `hushChatMetadata` records.
- Received shielded memo outputs and outgoing `z_viewtransaction` memo outputs are eligible for extraction only while `DRAGONX_ENABLE_CHAT` is enabled.
- The default build leaves `hushChatMetadata` empty and does not change transaction history, caches, wallet state, UI, sends, or logs.
- `applyTransactionRefreshResult()` currently ignores chat metadata, so no Batch 3 data is persisted.

## Batch 4 Pre-Decryption Safety Contract

Implemented preflight helper: `dragonx::chat::validateHushChatMemoDecryptPreflight()`.

Inputs:
- parsed HushChat header metadata
- ciphertext payload memo hex
- an explicit feature-enabled switch, defaulting to the build-time `DRAGONX_ENABLE_CHAT` value

Output:
- `ok=true` only when the input is structurally safe to hand to a future decrypt implementation
- typed errors for feature-disabled, non-`Memo` header, invalid header number/version, missing reply address/conversation id, malformed secretstream header, malformed public key, empty ciphertext, oversized ciphertext, odd-length ciphertext, non-hex ciphertext, and truncated ciphertext
- decoded ciphertext byte length only on success

Validation rules:
- no work is performed while `DRAGONX_ENABLE_CHAT` is off
- only `Memo` headers are valid for decrypt preflight; `Cont` remains plaintext/contact-request handling for later batches
- secretstream header metadata must remain 24 bytes encoded as 48 hex characters
- peer public key metadata must remain 32 bytes encoded as 64 hex characters
- ciphertext memo must be non-empty, <=512 bytes as memo text, even-length hex, and decode to more than `crypto_secretstream_xchacha20poly1305_ABYTES` bytes

The preflight helper intentionally does not:
- call libsodium
- derive keypairs or session keys
- decode or return ciphertext bytes
- decrypt plaintext
- store chat/contact data
- render UI
- send messages
- log memo contents or plaintext

## Batch 5 Decrypt-Input Boundary Contract

Implemented helpers:
- `dragonx::chat::decodeHushChatHexBytes()`
- `dragonx::chat::prepareHushChatDecryptInput()`
- `dragonx::chat::inspectHushChatDecryptFixtureReadiness()`
- `dragonx::chat::hushChatSessionKeySelectionForDirection()`

Inputs:
- stored chat key material as the 64-character hex string currently stored by SilentDragonXLite `ChatDataStore::getPassword()`
- parsed `Memo` header metadata
- ciphertext payload memo hex
- decrypt direction (`Incoming` or `Outgoing`)
- an explicit feature-enabled switch, defaulting to the build-time `DRAGONX_ENABLE_CHAT` value

Prepared fields:
- `stored_chat_key_bytes`: decoded 32 bytes from the stored chat key hex string
- `seed_bytes`: SilentDragonXLite-compatible `crypto_kx_seed_keypair()` seed bytes
- `peer_public_key_bytes`: decoded 32 bytes from header field `p`
- `stream_header_bytes`: decoded 24 bytes from header field `e`
- `ciphertext_bytes`: decoded ciphertext payload bytes
- `session_key_selection`: `ClientRx` for incoming messages, `ServerTx` for outgoing-history messages
- `plaintext_capacity`: ciphertext byte length minus `crypto_secretstream_xchacha20poly1305_ABYTES`

Seed compatibility decision:
- SilentDragonXLite stores a 32-byte `crypto_pwhash(...)` output as 64 lowercase hex characters.
- SilentDragonXLite then passes `ChatDataStore::getPassword().toUtf8()` directly to `crypto_kx_seed_keypair(...)`.
- Because `crypto_kx_seed_keypair(...)` consumes 32 bytes, DragonX's compatibility contract prepares `seed_bytes` as the first 32 UTF-8 bytes of the stored 64-character hex string.
- The decoded 32-byte stored key is kept separate as `stored_chat_key_bytes` so future code cannot accidentally conflate the two interpretations.

Validation rules:
- no work is performed while `DRAGONX_ENABLE_CHAT` is off
- stored chat key hex must decode to exactly 32 bytes
- Batch 4 preflight must pass before any payload decoding
- peer public key must decode to exactly 32 bytes
- stream header must decode to exactly 24 bytes
- ciphertext must decode to the preflight-reported byte length
- incoming direction selects `client_rx`; outgoing-history direction selects `server_tx`
- fixture readiness requires all prepared byte fields to have exact lengths and ciphertext to contain more than secretstream authentication overhead

The Batch 5 helpers intentionally do not:
- call libsodium
- derive keypairs or session keys
- decrypt plaintext
- store key material, ciphertext, chat, or contact data outside temporary result objects
- render UI
- send messages
- log key material, memo contents, ciphertext bytes, or plaintext

Required cross-wallet fixtures before real decryption:
- SilentDragonXLite outgoing `Memo` fixture with stored chat key hex, local public key, peer public key, header memo, ciphertext memo, direction, expected plaintext byte length, and a non-sensitive plaintext hash.
- SilentDragonXLite incoming `Memo` fixture with the same metadata plus role expectation (`client_rx`).
- Round-trip fixture between SilentDragonXLite and DragonX confirming the UTF-8-hex seed projection produces the observed public key.
- Corrupted fixture variants for bad peer public key, bad stream header, truncated ciphertext, and authentication failure.
- Contact-request fixture proving `Cont` remains outside encrypted `Memo` decrypt handling.

## Batch 6 Cross-Wallet Fixture Contract

Implemented verifier: `dragonx::chat::verifyHushChatCompatibilityFixture()`.

Static fixture fields:
- fixture id
- stored chat key hex
- local public key hex
- peer public key hex
- header memo
- ciphertext memo
- direction (`Incoming` or `Outgoing`)
- expected session key selection (`ClientRx` or `ServerTx`)
- expected stored chat key byte length
- expected seed byte length
- expected local public key byte length
- expected peer public key byte length
- expected stream header byte length
- expected ciphertext byte length
- expected plaintext byte length
- optional expected plaintext hash hex

Verifier behavior:
- no work is performed while `DRAGONX_ENABLE_CHAT` is off
- fixture id must be present so test/report output can identify the vector without logging memo contents
- local public key hex must decode to exactly 32 bytes
- peer public key hex must decode to exactly 32 bytes
- header memo is parsed through `parseHushChatHeaderMemo()`
- `(header memo, ciphertext memo)` is grouped through `groupHushChatMemoOutputs()`
- only `Memo` fixtures are accepted; `Cont` fixtures are rejected as outside encrypted decrypt handling
- for incoming fixtures, header field `p` must match the fixture peer public key
- for outgoing-history fixtures, header field `p` must match the fixture local public key
- decrypt input is prepared through `prepareHushChatDecryptInput()` with the fixture peer public key kept separate from the header public key
- readiness is checked through `inspectHushChatDecryptFixtureReadiness()`
- expected byte lengths and expected role must match the prepared/readiness output exactly
- optional plaintext hash must be hex text only; the verifier records its byte length but never evaluates plaintext

The Batch 6 verifier intentionally does not:
- derive a keypair
- derive session keys
- call libsodium
- decrypt ciphertext
- compare plaintext
- persist fixture data
- render UI
- send messages
- log key material, memo contents, ciphertext bytes, plaintext hashes, or plaintext

Remaining requirements before real decryption:
- collect real SilentDragonXLite incoming and outgoing `Memo` fixtures using non-sensitive plaintext and recorded hashes
- verify that the observed local public key matches the UTF-8-hex seed projection in an isolated test vector
- add corrupted authentication-failure fixtures that pass structural validation but must fail future `crypto_secretstream_xchacha20poly1305_pull()`
- decide how fixture hashes will be generated and compared without retaining plaintext in the repository
- keep contact-request fixtures separate from encrypted `Memo` fixtures

## Batch 7 Fixture File Import Contract

Checked-in fixture files live under `tests/fixtures/hushchat/`. The current files are intentionally `pending` placeholders for real, non-sensitive SilentDragonXLite vectors. They prove the import format and required categories without committing key material, memo contents, ciphertext bytes, plaintext, or made-up compatibility data.

The loader-supported format is JSON. The field names are intentionally TOML-friendly and the equivalent TOML shape is documented below for future fixture capture tooling.

Required top-level fields:
- `schema`: must be `dragonx.hushchat.compat-fixture.v1`
- `kind`: one of `incoming_memo`, `outgoing_memo`, `seed_public_key_projection`, `corrupted_auth_failure`, or `cont_exclusion`
- `status`: `pending` or `ready`
- `id`: required for `pending` files
- `pending_reason`: required for `pending` files and must explain what real SDXL data is still missing
- `fixture`: required for `ready` files

Ready JSON shape:

```json
{
  "schema": "dragonx.hushchat.compat-fixture.v1",
  "status": "ready",
  "kind": "incoming_memo",
  "fixture": {
    "id": "sdxl-incoming-memo-example",
    "stored_chat_key_hex": "<64 hex chars from SDXL test wallet>",
    "local_public_key_hex": "<64 hex chars>",
    "peer_public_key_hex": "<64 hex chars>",
    "header_memo": "<header memo JSON string>",
    "ciphertext_memo": "<payload memo hex string>",
    "direction": "Incoming",
    "expected": {
      "session_key_selection": "ClientRx",
      "stored_chat_key_bytes": 32,
      "seed_bytes": 32,
      "local_public_key_bytes": 32,
      "peer_public_key_bytes": 32,
      "stream_header_bytes": 24,
      "ciphertext_bytes": 21,
      "plaintext_bytes": 4,
      "plaintext_hash_hex": "<optional 64 hex chars>"
    }
  }
}
```

Equivalent TOML shape for future tooling:

```toml
schema = "dragonx.hushchat.compat-fixture.v1"
status = "ready"
kind = "incoming_memo"

[fixture]
id = "sdxl-incoming-memo-example"
stored_chat_key_hex = "<64 hex chars from SDXL test wallet>"
local_public_key_hex = "<64 hex chars>"
peer_public_key_hex = "<64 hex chars>"
header_memo = "<header memo JSON string>"
ciphertext_memo = "<payload memo hex string>"
direction = "Incoming"

[fixture.expected]
session_key_selection = "ClientRx"
stored_chat_key_bytes = 32
seed_bytes = 32
local_public_key_bytes = 32
peer_public_key_bytes = 32
stream_header_bytes = 24
ciphertext_bytes = 21
plaintext_bytes = 4
plaintext_hash_hex = "<optional 64 hex chars>"
```

Ready `incoming_memo`, `outgoing_memo`, `seed_public_key_projection`, and `corrupted_auth_failure` fixture files are accepted only after the Batch 6 verifier succeeds. That means the file is parsed through the Batch 1 header parser, Batch 2 memo grouper, Batch 4 preflight checks, Batch 5 decrypt-input preparation, and Batch 6 readiness/byte-length/role checks. `cont_exclusion` is accepted only when the verifier rejects the fixture as `NonMemoHeader`, proving contact requests stay outside encrypted Memo decrypt handling.

The fixture loader does not derive keys, call libsodium, decrypt, compare plaintext, persist chat data, render UI, send messages, or log key material/memo contents/plaintext/ciphertext bytes. It reports `pending` for missing real vectors and `verified` only for structurally ready vectors.

Exact data still needed from SilentDragonXLite before changing pending files to ready:
- a non-sensitive incoming `Memo` vector with stored chat key hex, local public key hex, peer public key hex, header memo, ciphertext memo, expected `ClientRx` role, expected byte lengths, and optional plaintext hash
- a non-sensitive outgoing-history `Memo` vector with the same structural fields and expected `ServerTx` role
- a seed/public-key projection vector proving the SDXL UTF-8-hex seed input produces the recorded local public key, without committing passphrase or plaintext
- a corrupted authentication-failure vector that passes structural validation but must fail future secretstream pull authentication
- a `Cont` vector proving contact requests remain excluded from encrypted Memo decrypt preparation
- the exact capture command/procedure from SDXL so future vectors are reproducible without exposing sensitive wallet data

## Batch 8 Fixture Import Checklist Contract

Implemented import gate:
- `dragonx::chat::hushChatRequiredCompatibilityFixtureKinds()`
- `dragonx::chat::inspectHushChatCompatibilityFixtureImportChecklist()`
- developer tool target `HushChatFixtureCheck`

The import checklist is the conservative replacement path for the pending JSON placeholders. It accepts a caller-supplied candidate file for each expected category and loads every file through `loadHushChatCompatibilityFixtureFile()`. It reports replacement-ready only when:
- all five required categories are supplied exactly once
- no required category is missing
- no duplicate category candidate is supplied
- no supplied file is malformed or unreadable
- no file remains `pending`
- each non-`Cont` ready file passes the Batch 7 loader and Batch 6 structural verifier
- each non-`Cont` ready file passes the Batch 9 seed/public-key projection verifier
- the `corrupted_auth_failure` ready file is marked structurally ready for a future secretstream authentication-failure check, without decrypting or authenticating it now
- the `cont_exclusion` ready file is accepted only by being excluded from encrypted Memo handling

The current checked-in placeholder directory is expected to pass only with pending allowed:

```sh
./build/bin/HushChatFixtureCheck --allow-pending tests/fixtures/hushchat
```

Before replacing pending placeholders with real vectors, the strict check must pass:

```sh
./build/bin/HushChatFixtureCheck tests/fixtures/hushchat
```

The tool intentionally prints only category names, file basenames, status/error names, and aggregate counts. It does not print key material, memo contents, plaintext, ciphertext bytes, plaintext hashes, or derived bytes. The helper/tool does not derive session keys, decrypt, persist chat data, render UI, send messages, or enable `DRAGONX_ENABLE_CHAT` in the app; Batch 9 uses libsodium only for the local seed/public-key projection check.

Capture checklist for real SDXL fixtures is tracked in `tests/fixtures/hushchat/IMPORT_CHECKLIST.md`. Pending files may be replaced only with non-sensitive, disposable-wallet vectors that pass the strict checklist. If any category is pending, missing, malformed, mismatched, duplicated, or not verified, the replacement gate remains closed.

## Batch 9 Seed/Public-Key Projection Contract

Implemented verifier:
- `dragonx::chat::verifyHushChatSeedPublicKeyProjection()`

The projection verifier is a no-plaintext compatibility check for ready non-`Cont` fixture files. It accepts the existing fixture fields only:
- fixture id
- stored SDXL chat key hex string
- declared local public key hex
- expected stored chat key byte length
- expected seed byte length
- expected local public key byte length

Verification rules:
- no work is performed while `DRAGONX_ENABLE_CHAT` is off unless a caller explicitly supplies the test/import feature switch
- fixture id must be present
- stored chat key hex must decode to the expected 32-byte stored key length
- seed bytes are exactly the first 32 UTF-8 bytes of the stored 64-character SDXL chat key hex string
- seed byte length must match the fixture's declared expected seed length
- local public key hex must decode to the expected 32-byte local public key length
- libsodium is initialized only for this local public-key projection check
- `crypto_kx_seed_keypair()` is called with the SDXL-compatible seed bytes
- only the projected public key is compared to the declared local public key
- the derived secret key, temporary seed bytes, and temporary projected public key buffer are zeroed before returning

The verifier intentionally does not:
- derive session keys
- decrypt ciphertext
- compare plaintext
- store chat/contact data
- render UI
- send messages
- log or return key material, memo contents, plaintext, ciphertext bytes, plaintext hashes, seed bytes, public key bytes, or secret key bytes

Batch 9 integration:
- `inspectHushChatCompatibilityFixtureImportChecklist()` now requires all four non-`Cont` ready fixtures to pass the seed/public-key projection verifier.
- The developer checker reports only the aggregate `seed_projected` count.
- Pending files still block strict replacement until real SilentDragonXLite vectors are supplied.

## Batch 10 Corrupted Authentication-Failure Scaffold

Implemented scaffold:
- `dragonx::chat::inspectHushChatCorruptedAuthFailureReadiness()`

The corrupted-auth scaffold is an inert marker for ready `corrupted_auth_failure` fixture files. It exists so a real non-sensitive SilentDragonXLite corrupted vector can be carried through the current structural pipeline and reserved for a future `crypto_secretstream_xchacha20poly1305_pull()` authentication-failure assertion.

Readiness rules:
- no work is performed while `DRAGONX_ENABLE_CHAT` is off unless a caller explicitly supplies the test/import feature switch
- pending fixtures are rejected as not ready
- only `corrupted_auth_failure` fixture files are accepted
- the fixture file must already be parsed and verified through the Batch 7 loader and Batch 6 fixture verifier
- the fixture must already pass the Batch 9 seed/public-key projection verifier
- success sets `structurally_ready_for_future_auth_check=true`
- success sets `requires_future_secretstream_auth_failure=true`
- success keeps `decrypted=false` and `authenticated=false`

The scaffold intentionally does not:
- derive session keys
- initialize a secretstream pull state
- decrypt ciphertext
- authenticate ciphertext
- compare plaintext
- store chat/contact data
- render UI
- send messages
- log or return key material, memo contents, plaintext, ciphertext bytes, plaintext hashes, seed bytes, public key bytes, or secret key bytes

Batch 10 integration:
- `inspectHushChatCompatibilityFixtureImportChecklist()` now requires the corrupted fixture to be structurally ready for future auth-failure validation before replacement can be ready.
- The developer checker reports aggregate `future_auth_required` and `auth_structural_ready` counts, and per-file labels only for those states.
- These counts do not mean the fixture has decrypted or authenticated; they mean it is ready for a future auth-failure assertion once decrypt support exists.

## Batch 11 Strict Replacement Dry-Run Report

Implemented dry-run workflow:
- `dragonx::chat::inspectHushChatCompatibilityFixtureReplacementDryRun()`
- `./build/bin/HushChatFixtureCheck --replacement-dry-run <fixture-file-or-directory>...`

The dry-run workflow wraps the Batch 10 import checklist and reports whether a supplied ready-vector directory would be eligible to replace the checked-in pending placeholders. It is strict by design: the CLI refuses `--replacement-dry-run` when `--allow-pending` is also supplied, and the dry-run result only sets `would_replace=true` when the underlying checklist is fully `replacement_ready`.

Replacement is refused if any required vector is:
- missing
- duplicated
- unreadable or malformed
- supplied under the wrong expected kind
- still marked `pending`
- not verified through the fixture loader and structural contracts
- unable to pass the SDXL seed/public-key projection check
- a `corrupted_auth_failure` vector that is not structurally ready for a future auth-failure check
- a `cont_exclusion` vector that is not excluded from encrypted `Memo` handling

The replacement report is intentionally redacted. It may print only category names, file basenames, status/error names, boolean decision flags, and aggregate counts. It must not print fixture ids, stored chat key material, memo contents, secretstream headers, public key bytes, plaintext hashes, plaintext, ciphertext bytes, seed bytes, derived secret keys, or derived session keys.

The dry-run workflow intentionally does not:
- copy or overwrite fixture files
- derive session keys
- initialize a secretstream pull state
- decrypt ciphertext
- authenticate ciphertext
- compare plaintext
- store chat/contact data
- render UI
- send messages
- enable `DRAGONX_ENABLE_CHAT` in the app

## Batch 12 Capture Manifest Workflow

Implemented manifest validator:
- `dragonx::chat::validateHushChatCaptureManifest()`
- `dragonx::chat::loadHushChatCaptureManifestFile()`
- `./build/bin/HushChatFixtureCheck --validate-capture-manifest <manifest-file-or-directory>`

The capture manifest is a redacted provenance and handling record for a staged directory of real disposable SilentDragonXLite fixture files. It does not validate fixture contents and does not replace the Batch 11 strict replacement dry run. Instead, it verifies that the staged directory has the required metadata before strict fixture validation is used.

The checked-in template is `tests/fixtures/hushchat/templates/capture-manifest.template.json`. The template is intentionally marked `template`; a staged copy must be named `capture-manifest.json`, set top-level `status` to `staged`, and set each required category status to `ready` before the validator accepts it.

Required manifest fields:
- schema `dragonx.hushchat.capture-manifest.v1`
- top-level status `staged`
- redacted manifest id
- staged fixture directory name
- dry-run command containing `HushChatFixtureCheck --replacement-dry-run`
- provenance object with source client `SilentDragonXLite`, source client version or commit, capture date, network, and capture method
- handling object with all required safety flags set to `true`
- exactly one category entry for each required fixture kind, each with a staged filename and status `ready`

Rejected manifest conditions:
- unreadable, non-object, or malformed JSON
- wrong schema
- template or unknown status
- missing provenance, handling, dry-run, fixture-directory, or category metadata
- missing, duplicate, or unknown required category entries
- any known prohibited fixture-data field such as `fixture`, `stored_chat_key_hex`, `header_memo`, `ciphertext_memo`, `plaintext`, `passphrase`, private-key fields, wallet-file fields, derived-key fields, or session-key fields

The manifest validator prints only redacted status/error names, counts, category names, basenames, and boolean safety flags. It intentionally does not inspect or print fixture contents, key material, memo contents, plaintext, ciphertext bytes, plaintext hashes, derived keys, session keys, or wallet files. It also does not copy files, decrypt, authenticate, derive session keys, store chat/contact data, render UI, send messages, or enable `DRAGONX_ENABLE_CHAT` in the app.

The staged-vector workflow is:

```sh
./build/bin/HushChatFixtureCheck --validate-capture-manifest /path/to/staged/hushchat-fixtures
./build/bin/HushChatFixtureCheck --replacement-dry-run /path/to/staged/hushchat-fixtures
```

Both commands must succeed before replacing the checked-in pending placeholders.

## Remaining Implementation Batches

1. Capture real non-sensitive SilentDragonXLite fixture files and replace pending placeholders only after the Batch 12 manifest validator and Batch 11 strict dry-run report both succeed.
2. Add the real secretstream auth-failure assertion for ready corrupted fixtures after decrypt support exists.
3. Implement encrypted message decryption only after fixture confirmation.
4. Add wallet-scoped chat/contact storage.
5. Add read-only ImGui Chat UI.
6. Add contact request actions.
7. Add send/composer flow after receive-side validation.