# ADR 005: Algorithm-tagged persistence and byte-preserving identities

## Status

Accepted on 2026-10-13; implementation follows the [delivery sequence](../TODO-wire-format.zh.md#交付顺序). This describes the target, not completed implementation or acceptance. It extends ADR 004 from content algorithms to every persistent and authenticated structure.

## Context

Positional encoding cannot skip new fields. Decoding and re-encoding also discards unknown fields that contribute to a record's identity. The [confirmed requirements](../requirements.zh.md#格式与算法升级) require protobuf, authentication of original bytes, algorithm tags, a repository policy, member capabilities and local schema versions. The project is unpublished: this transition rejects previous positional encodings and magic values. Readers retain support for protobuf format 1 and subsequent older formats; field numbers and meanings are never reused.

## Decision

1. Use proto3 package `cipherfs.v1` in `crates/cipherfs-core/proto/cipherfs.proto`. Commit `src/wire/generated.rs`; `scripts/projects.sh proto` generates it in a container using `prost-build` and pure Rust `protox`. Normal builds consume committed code; generation uses the official [compile_fds API](https://docs.rs/prost-build/0.14.4/prost_build/struct.Config.html#method.compile_fds), which does not invoke protoc. Put every proto comment above its declaration and reserve retired field numbers. Encode ordered maps as sorted repeated entries and reject duplicate keys.
2. `HashedBytes` and `SignedBytes` retain a body encoded exactly once by its author. Versions, retirement, enrollment, recovery approval and authority records preserve this body. Authenticate before decoding; never re-encode to verify identity. Database reopening, snapshots and forwarding copy the original bytes. Materialized member names, roles and capabilities do not mutate enrollment preimages. Inventory commitments have a separate body with proofs outside it. Pairing retains its existing keyed BLAKE3 MAC/SAS over transmitted original transcript bytes, without adding signatures.
3. `Digest`, `Signature` and `PublicKey` carry an algorithm and value. Preserve hash IDs Blake3=1, Sha256=2, Sha512=3 and AEAD IDs XChaCha20Poly1305=1, Aes256Gcm=2, ChaCha20Poly1305=3. Ed25519 signatures use 1; a separate public-key enum uses Ed25519=1 and X25519=2. Argon2id KDF uses 1. Text uses Bitcoin base58 with leading zeros and strict canonical lengths: `b3`/`sha256` 32 bytes, `sha512` 64, Ed25519 public keys 32/signatures 64, X25519 public keys 32. Random 16-byte identities are neither digests nor public keys.
4. Publish English and Chinese registries with numeric IDs, text names, active/deprecated status and optional `deprecated_after`. Register public keys separately from signatures, and assign explicit chunking IDs to the existing FastCDC/Fixed strategies without changing existing hash/AEAD IDs. Reject unknown, unimplemented and expired algorithms without fallback. Tags also cover local AEAD and X25519 sealed boxes. Preserve existing derivation labels.
5. `Genesis.algorithm_policy` pins hash, signature, KDF and AEAD algorithms. Initially select implemented algorithms; validate all records and envelopes against the policy. Bootstrap authenticates the original Genesis segment against the independent TrustAnchor before taking its policy, then checks everything in that segment. A rejected candidate must not persist keys, members, authority or a frontier.
6. Use segment magic `CFSSEG02` and snapshot magic `CFSSNP03`, including upload inventories that use snapshot envelopes. Follow magic with a big-endian `u32 format`; only format 1 is implemented initially. Headers identify public-key, signature and AEAD algorithms and retain author, epoch, sequence/frontier summaries and authority bindings. Authenticate original header bytes, including magic, format, algorithm tags, nonce and authority, in both the signature and AEAD AAD. Bounded header parsing grants no authority. Preserve the existing 32MiB segment, snapshot and proof limits. Recovery objects and configuration use their own protobuf types and envelopes.
7. Enrollment and recovery enrollment preimages bind `max_format >= 1`. Add an optional capability change to the existing self-authored `MemberUpdate`. Exclude revoked members. Empty-repository bootstrap writes local format 1; normal writes select the minimum of actual local support and all active members' latest advertisements. Stale local advertisements cannot select an unimplemented format. Reject higher formats before decryption, candidate-key staging or application, block that device's later segments, continue independent devices and return an explicit error. Do not advance the blocked frontier; dependent segments remain Deferred. Snapshots cannot silently bypass this check.
8. Store metadata/CAS database values and `VaultConfig` as protobuf, with a settings `schema: u32`. Initialize schema only for an explicitly new database. Existing databases with missing schema, unsupported newer schema or unreadable data fail rather than rebuild. Perform sequential lower-schema migrations atomically and roll back failures. Preserve which tables are encrypted; tag local AEAD envelopes and configuration KDF.
9. Canonical names are `i/ed25519.<b58>/<seq:016x>-b3.<b58>-<len>`, `p/<first-two-digest-value-characters>/b3.<b58>-<len>`, `n/ed25519.<b58>-b3.<b58>-<len>`, `u/b3.<b58>-<len>` and `r/b3.<epoch-b58>-b3.<b58>-<len>`. Random IDs use `v/<b58>` and `s/<b58>` with exactly 16 decoded bytes. Snapshot names identify the publisher; the authenticated payload continues to commit to and verify original frontier bytes and their digest. Diagnose and ignore unknown-algorithm objects; they cannot authorize data or justify GC deletion.

## Policy transitions reserved for later

A future transition record must be authorized by the current policy and contain its predecessor identity, the new set, an activation frontier and required format. All active devices advertise support before activation. Old-policy writes precede activation and new-policy writes follow it. Authenticate old records under their original policy and retain their algorithm tags and original bytes. This work does not implement policy-transition records, re-encryption, background migration or new cryptographic algorithms.

## Alternatives

- Positional encoding with a new magic for every added field does not provide field evolution or preservation of unknown-field identities.
- Deterministic re-encoding still loses fields unknown to the reader and cannot replace authentication of original bytes.
- Build-time protoc adds a host tool dependency; container generation with pure Rust and committed output provides a reproducible alternative.

## Consequences and acceptance

Keeping an original body costs memory but permits authenticated forwarding of unknown fields. Materialized state must remain separate from authenticated records. Forward extension does not imply that old programs understand new semantics.

Each step passes `scripts/projects.sh test`, receives its own commit and only then permits the next step. Hosts use only `check`, `clippy` and `format`. Golden vectors under `crates/cipherfs-core/tests/vectors/` contain public identifiers, signatures, ciphertext and synthetic metadata, never private keys, repository keys, credentials or user plaintext. Check exact bytes, identities and signatures; any one-byte mutation fails. Bodies with unknown fields remain byte-identical after import, reopening and snapshot export.

Real device fixtures advertising 1/2 must write format 1. Format 3 is rejected without advancing that device or its successors, while independent devices continue. Rejected bootstrap leaves no candidate keys. Remove direct and transitive positional-codec dependencies by removing the unused session dependency and using the already-locked endpoint library version with unchanged discovery and protocol behavior. Record actual runs in the implementation audit.
