Skip to content

Opp: define message header semantics and derive them in buildenv#506

Open
heifner wants to merge 2 commits into
masterfrom
fix/sec-102-envelope-header-semantics
Open

Opp: define message header semantics and derive them in buildenv#506
heifner wants to merge 2 commits into
masterfrom
fix/sec-102-envelope-header-semantics

Conversation

@heifner

@heifner heifner commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

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: normative MessageHeader semantics — keccak256 over the field-complete canonical encoding for payload_checksum/header_checksum, message_id = header checksum with the first 8 bytes replaced by the big-endian sequence number (matching the Solidity OPPCommon derivation), millisecond timestamps (the documented intent; only the emit sites had drifted to seconds), endpoints reserved-default, data_size == len(data).
  • encoding_flags (and its orphaned enum types) removed with reserved 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 the sysio.chains registry row if ever needed.
  • sysio.msgch::buildenv derives the full header on every (re)build inside the trim loop and chains message ids per outpost via a new last_message_id column on the one-deep outenvelopes row.
  • opp_canonical_codec.hpp gains 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.
  • Golden vectors regenerated with spec-derived headers; the identical pins go into the wire-solana and wire-ethereum vector suites next, which re-confirm them against the Rust and Solidity codecs.

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).

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants