fix: encode msgpack structs as named maps so app info and kv values can gain fields - #1030
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes cross-version compatibility for RA-TLS certificate AppInfo and gateway KV value serialization by switching MessagePack struct encoding from positional arrays to named maps, allowing fields to be added without breaking older readers.
Changes:
- Encode RA-TLS
AppInfoextension usingrmp_serde::to_vec_namedand update decode error context to correctly reference msgpack. - Encode gateway KV values using
rmp_serde::encode::to_vec_namedto make KV replication/restart data forwards-compatible. - Add regression tests in both crates to validate mixed-version decoding across positional (legacy) and named-map (new) encodings.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| dstack/ra-tls/src/traits.rs | Fixes the decode error context to accurately describe msgpack decoding. |
| dstack/ra-tls/src/cert.rs | Switches RA-TLS app info extension encoding to named maps and adds cross-version encoding tests. |
| dstack/gateway/src/kv/mod.rs | Switches KV value encoding to named maps and adds legacy/new roundtrip and compatibility tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
AppInfois serialized into thePHALA_RATLS_APP_INFOcertificate extension withrmp_serde::to_vec, which encodes structs as positional MessagePack arrays. Fieldnames never reach the wire, so the compatibility contract is the field order and count.
Under that contract a struct cannot gain a field: the array length changes and readers
built against the previous definition reject it outright.
c4ea8110d("feat(compose): support multiple init scripts") appendedinit_script_hashestoAppInfo, taking it from 8 fields to 9.#[serde(default)]onthat field makes an old certificate readable by a new binary, but nothing makes a
new certificate readable by an old one:
The failure is not confined to app info.
ra-rpcdecodes the extension for every prpccall that carries a client certificate and propagates the error:
So a CVM built from
nextregistering against a v0.5.11 gateway would have every prpccall rejected, not just lose app info.
dstack-util/src/system_setup.rs:502,521setsext_app_info: true, so that is the normal registration path.The same encoding is used for gateway KV values (
gateway/src/kv/mod.rs), where the 11value structs replicate between gateways and are read back after restart — the same
freeze applies there, across a mixed-version cluster.
Scope of exposure
Nothing broken has shipped. The extension has carried exactly one layout since it was
introduced:
AppInfoin certAPP_INFOabsent fromcert.rs)f1ba0a22)next(unreleased)The earlier 14 → 13 field churn at v0.5.0 → v0.5.1 predates embedding and produced no wire
format. So there is a single legacy layout to stay compatible with, and the window to fix
this closes when 0.6.0 ships.
Fix
Encode as a MessagePack map keyed by field name (
to_vec_named) at both sites. Thismoves the contract from field order to field names, which readers can skip when unknown
and default when absent.
The decode side needs no change and no version negotiation: serde's derived
Deserializeimplements bothvisit_seqandvisit_map, so a single reader acceptseither form. That is what makes this a one-way migration rather than a flag day —
already-deployed v0.5.6+ binaries can read the new encoding without being rebuilt.
Verification
All four directions across the v0.5.11 boundary, exercised by the new tests:
array had incorrect length)Tests added:
ra-tls:legacy_positional_app_info_still_decodespins a golden 94-byte fixture of thev0.5.6–v0.5.11 positional layout and asserts it decodes, with
init_script_hashesdefaulting to
None.named_app_info_decodes_against_legacy_field_setdecodes thisbuild's output into a struct that reproduces the 8-field layout, standing in for an
unrebuilt peer.
app_info_survives_a_certificate_round_tripgoes through a realCertRequest→get_app_info().gateway: equivalent legacy/reduced-reader pair plusnested_tagged_enums_and_custom_codecs_survive_both_encodings, which coversDnsCredential— the only value type nesting an internally tagged enum(
#[serde(tag = "type")]) and aserde(with)codec.Confirmed
serde_human_bytesis unaffected:to_vec_nameddoes not flipis_human_readable, so#[serde(with = "hex_bytes")]fields still emit MessagePackbin(
0xC4) rather than hex strings, and JSON output is unchanged.cargo test -p ra-tls -p dstack-gateway -p ra-rpc -p dstack-attest -p dstack-utilpasses;cargo fmt --all --checkclean; clippy clean apart from a pre-existingmanual_repeat_nwarning ingateway/src/pp.rs:254.Cost
AppInfogrows from 86 to 203 bytes inside the certificate extension. Gateway KV valuesare small structs with a similar constant factor.
Not in this PR
wavekv's snapshot (node.rs:161) also uses positional MessagePack, but it lives in aseparate repository and already has
magic+versionwith an enforced check, so it canmigrate explicitly by bumping
SnapshotFile::VERSION. Its WAL uses bincode, which has nonamed mode at all; it is protected instead by a magic/version header, length-prefixed
records, and a CRC32 over the canonical encoding that turns a format drift into a loud
startup failure.