Opp: define message header semantics and derive them in buildenv#506
Opp: define message header semantics and derive them in buildenv#506heifner wants to merge 2 commits into
Conversation
opp.proto now specifies the MessageHeader normatively: every checksum/id is keccak256 over the field-complete canonical encoding of its sub-message, message_id is the header checksum with its first 8 bytes replaced by the big-endian sequence number (previous message's + 1; a stream starts at 1 with an empty previous_message_id), all OPP wire timestamps are milliseconds since the Unix epoch, endpoints stays reserved-default (routing derives from the proven delivery chain), and data_size must equal the data length with receivers sizing by the data itself. Remove encoding_flags and the EncodingFlags/Endianness/HashAlgorithm/LengthEncoding types: no implementation reads them, parse conventions cannot be self-described (they must be known before the field is readable), and a self-declared hash algorithm is a downgrade knob. Per-chain hash selection, if ever needed, belongs on the sysio.chains registry row. Slot 4 is reserved, and the Envelope's previously removed slots gain real reserved statements so reuse fails at protoc. sysio.msgch buildenv derives the semantic header on every (re)build inside the trim loop, chains message ids per outpost through a new last_message_id column on the one-deep outenvelopes row, and emits real millisecond timestamps (WIRE's half-second blocks make sub-second resolution meaningful; seconds-unit table columns are unchanged). The canonical codec drops the removed field and gains shared derivation helpers for the emitter, the tests, and the upcoming inbound validator. Golden vectors are regenerated with spec-derived headers -- vector B chains from A at both the envelope and message level -- and the identical values are to be pinned in the wire-solana and wire-ethereum vector suites, which re-confirm them against the Rust and Solidity codecs. The outbound chain tests additionally assert the full header derivation, sequence continuity, and the stored stream tip.
| /// The big-endian sequence number carried in the first 8 bytes of a wire `message_id`. Empty | ||
| /// (stream genesis) — and any out-of-spec id shorter than 8 bytes — reads as 0, so the successor | ||
| /// message's sequence number is 1. | ||
| inline uint64_t message_sequence(const std::vector<char>& message_id) { |
There was a problem hiding this comment.
Could we make this reject non-canonical lengths rather than mapping them? The proto permits previous_message_id to be empty only at stream genesis and otherwise requires 32 bytes. As written, every 1–7-byte value aliases genesis and every length of at least 8 bytes is accepted as a sequence source. PR #508 calls this helper during semantic-header validation without a separate exact-length check, so a malformed header can still recompute as valid. Returning an optional/error for lengths other than 0 or 32 (or enforcing that before this helper) would keep the verifier strict.
There was a problem hiding this comment.
Done — message_sequence now returns std::optional<uint64_t>, nullopt for every length other than 0 or 32 (898e5aa here), and #508's validator drops on nullopt before deriving the expected id (c79a619), with a new forgery case where the checksum and id are honestly re-derived over a 5-byte previous_message_id so the drop is attributable to the length rule alone. buildenv hard-checks its own stored tip — corrupted own-state fails loudly, unlike the inbound never-throw path. The outpost emit-side mirrors only ever read their own well-formed stored tips, so they keep the plain form.
A wire message id is either empty (stream genesis) or exactly 32 bytes; message_sequence previously read any id shorter than 8 bytes as genesis and any longer one as a sequence source, so a verifier deriving the expected message_id through it would accept headers whose previous_message_id carries a non-canonical length. It now returns std::optional and yields nullopt for every length other than 0 or 32. buildenv hard-checks the sequence of its own stored tip -- a non-canonical length there is corrupted own-state and fails loudly, unlike the inbound never-throw path.
Part 1 of SEC-102 (WSA-005 item 3): standardize the OPP envelope semantic header so every emitter populates it identically and the depot can eventually validate it.
opp.proto: normativeMessageHeadersemantics — keccak256 over the field-complete canonical encoding forpayload_checksum/header_checksum,message_id= header checksum with the first 8 bytes replaced by the big-endian sequence number (matching the SolidityOPPCommonderivation), millisecond timestamps (the documented intent; only the emit sites had drifted to seconds),endpointsreserved-default,data_size == len(data).encoding_flags(and its orphaned enum types) removed withreserved 4;: never read by any implementation, cannot self-describe parse conventions, and a self-declared hash algorithm is a downgrade knob — per-chain hash selection belongs on thesysio.chainsregistry row if ever needed.sysio.msgch::buildenvderives the full header on every (re)build inside the trim loop and chains message ids per outpost via a newlast_message_idcolumn on the one-deepoutenvelopesrow.opp_canonical_codec.hppgains shared derivation helpers (encode(MessagePayload/MessageHeader),header_digest,derive_message_id,message_sequence) reused by the emitter, the test oracle, and the follow-up inbound validator.SEC-102 PR set (review in this order): this PR (spec + depot emit) → Wire-Network/wire-solana#370 (Solana emit population) → Wire-Network/wire-ethereum#137 (Ethereum ms units + vector re-confirmation) → #508 (depot enforcement, stacked on this branch).