[v0.1/P1] Version the pre-preface FRLY carrier envelope #332

Closed
opened 2026-09-21 23:17:30 +09:00 by natsukium · 1 comment
Owner

Problem

The optional FRLY carrier block is parsed before the FLIS version preface, but its format has no version field. The reference freezes it “per protocol major”; the receiver cannot use that major to select a carrier parser because the major is only available after the carrier has already been parsed.

The existing payload length makes unknown formats skippable in principle, but without a format discriminator every change to the carrier payload is indistinguishable from malformed version 1 data. The current “reject trailing bytes” rule also leaves no additive extension point.

#139 settled the entry/payload caps and degrade behavior. This issue does not reopen those choices; it addresses format evolution before the first byte layout freezes.

Proposed resolution

Add a carrier format version to the fixed envelope before the first public baseline, for example:

FRLY | format_version | payload_length | payload

Version 1 retains the current entry encoding and the frozen 4096-entry / 1 MiB limits. A receiver that recognizes the envelope but not format_version can consume payload_length, skip the block, warn, and continue to the FLIS preface with no carrier environment. This matches the existing safe-degrade policy for an unsendable carrier block.

The field widths and unknown-version behavior must be specified exactly; the example above is not a field-size decision.

Acceptance criteria

  • The carrier envelope has an explicit format discriminator available before payload decoding.
  • Unknown carrier versions can be skipped by length and degrade to no carrier data without desynchronizing the following FLIS preface.
  • Version 1 preserves #139's limits and relay byte-pump architecture.
  • Tests cover no block, known version 1, unknown version, truncated header/payload, and resumption at FLIS.
  • Reference docs no longer claim that the unavailable protocol major selects the carrier parser.
  • The rationale and a future-version procedure are recorded in the IPC explanation.
  • The layout change lands before the first immutable wire baseline.

Evidence

  • crates/felis-protocol/src/preface.rs
  • crates/felis-transport/src/preface.rs
  • docs/reference/ipc.md (“Relay carrier block”)
  • docs/explanation/architecture/ipc.md (“Carrier choices”)

Related: #5, #115, #139.

## Problem The optional `FRLY` carrier block is parsed before the `FLIS` version preface, but its format has no version field. The reference freezes it “per protocol major”; the receiver cannot use that major to select a carrier parser because the major is only available after the carrier has already been parsed. The existing payload length makes unknown formats skippable in principle, but without a format discriminator every change to the carrier payload is indistinguishable from malformed version 1 data. The current “reject trailing bytes” rule also leaves no additive extension point. #139 settled the entry/payload caps and degrade behavior. This issue does not reopen those choices; it addresses format evolution before the first byte layout freezes. ## Proposed resolution Add a carrier format version to the fixed envelope before the first public baseline, for example: ```text FRLY | format_version | payload_length | payload ``` Version 1 retains the current entry encoding and the frozen 4096-entry / 1 MiB limits. A receiver that recognizes the envelope but not `format_version` can consume `payload_length`, skip the block, warn, and continue to the `FLIS` preface with no carrier environment. This matches the existing safe-degrade policy for an unsendable carrier block. The field widths and unknown-version behavior must be specified exactly; the example above is not a field-size decision. ## Acceptance criteria - [ ] The carrier envelope has an explicit format discriminator available before payload decoding. - [ ] Unknown carrier versions can be skipped by length and degrade to no carrier data without desynchronizing the following `FLIS` preface. - [ ] Version 1 preserves #139's limits and relay byte-pump architecture. - [ ] Tests cover no block, known version 1, unknown version, truncated header/payload, and resumption at `FLIS`. - [ ] Reference docs no longer claim that the unavailable protocol major selects the carrier parser. - [ ] The rationale and a future-version procedure are recorded in the IPC explanation. - [ ] The layout change lands before the first immutable wire baseline. ## Evidence - `crates/felis-protocol/src/preface.rs` - `crates/felis-transport/src/preface.rs` - `docs/reference/ipc.md` (“Relay carrier block”) - `docs/explanation/architecture/ipc.md` (“Carrier choices”) Related: #5, #115, #139.
Author
Owner

Decision: do, small; the version goes at the head of the payload, the 8-byte header stays (reviewed)

Why it matters: ipc.md promises relay ↔ daemon skew never needs negotiation ("a relay that prepends the block works against any v1 daemon"), and that skew is routine after a package upgrade against a warm daemon. Today any change to the payload encoding would break that promise for the whole major.

Shape: header unchanged (FRLY | u32 payload_length). Payload becomes u16 format_version | <encoding selected by version>; version 1 is today's u32 entry_count | entries under #139's caps. The daemon reads the whole payload by length exactly as now (length cap checked before allocation), then the version: known → decode; unknown → warn, continue to FLIS with no carrier (the same degrade as a relay that prepends nothing). Skipping is free because the payload is already consumed by length, and the exact-consumption rule is untouched. Version 0 is malformed.

Why the header is not widened: an old daemon would read a wider header's bytes as the length, and a length of 65536 against a shorter stream leaves both peers waiting. With the version in the payload, migration is fail-fast in both directions: an old decode_payload reads 0x00 0x0v .. as an entry count ≥ 65536 > 4096 and closes with OverCap before reading an entry; an old relay's versionless payload starts 0x00 0x00 (the high half of a small count), which the new daemon reads as version 0 and closes. Remedies per direction in CHANGELOG.md: new relay / warm old daemon → restart the daemon; old relay / new daemon → upgrade the relay side, which in the normal deployment is the same package on the same host (felis-daemon relay is exec'd fresh per SSH connection), so one package upgrade plus one daemon restart covers both.

Docs: REQ-104a/104c and ipc.md are reworded so the envelope and version word are the frozen-for-all-time part of the preface layer and the version selects the entry encoding; "frozen for the protocol major" goes, since the major arrives after the block. Rejected alternatives recorded: widened header, new magic per layout. Tests: no block / v1 / unknown version skipped and FLIS resumed / version 0 / old-writer bytes vs new reader / new-writer bytes vs the old count cap / truncated header / truncated payload. Independent of the proto batch.

## Decision: do, small; the version goes at the head of the payload, the 8-byte header stays (reviewed) Why it matters: `ipc.md` promises relay ↔ daemon skew never needs negotiation ("a relay that prepends the block works against any v1 daemon"), and that skew is routine after a package upgrade against a warm daemon. Today any change to the payload encoding would break that promise for the whole major. Shape: header unchanged (`FRLY | u32 payload_length`). Payload becomes `u16 format_version | <encoding selected by version>`; version 1 is today's `u32 entry_count | entries` under #139's caps. The daemon reads the whole payload by length exactly as now (length cap checked before allocation), then the version: known → decode; unknown → warn, continue to `FLIS` with no carrier (the same degrade as a relay that prepends nothing). Skipping is free because the payload is already consumed by length, and the exact-consumption rule is untouched. Version 0 is malformed. Why the header is **not** widened: an old daemon would read a wider header's bytes as the length, and a length of 65536 against a shorter stream leaves both peers waiting. With the version in the payload, migration is fail-fast in both directions: an old `decode_payload` reads `0x00 0x0v ..` as an entry count ≥ 65536 > 4096 and closes with `OverCap` before reading an entry; an old relay's versionless payload starts `0x00 0x00` (the high half of a small count), which the new daemon reads as version 0 and closes. Remedies per direction in `CHANGELOG.md`: new relay / warm old daemon → restart the daemon; old relay / new daemon → upgrade the relay side, which in the normal deployment is the same package on the same host (`felis-daemon relay` is exec'd fresh per SSH connection), so one package upgrade plus one daemon restart covers both. Docs: REQ-104a/104c and `ipc.md` are reworded so the envelope and version word are the frozen-for-all-time part of the preface layer and the version selects the entry encoding; "frozen for the protocol **major**" goes, since the major arrives after the block. Rejected alternatives recorded: widened header, new magic per layout. Tests: no block / v1 / unknown version skipped and `FLIS` resumed / version 0 / old-writer bytes vs new reader / new-writer bytes vs the old count cap / truncated header / truncated payload. Independent of the proto batch.
Commenting is not possible because the repository is archived.
No description provided.