Skip to content

feat(msgpack): add ASAPv1 self-describing envelope to wire format#63

Draft
GordonYuanyc wants to merge 11 commits into
mainfrom
feat/msgpack-magic-id
Draft

feat(msgpack): add ASAPv1 self-describing envelope to wire format#63
GordonYuanyc wants to merge 11 commits into
mainfrom
feat/msgpack-magic-id

Conversation

@GordonYuanyc

@GordonYuanyc GordonYuanyc commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Overview

Adds the ASAPv1 envelope to every serialized sketch binary, making the wire
format self-describing and carrying the hash metadata needed by out-of-process
consumers.

[ b"ASAPv1" | version:u8 | kind_id_len:u8 | kind_id:bytes | metadata_len:u32_be | metadata:msgpack | msgpack_payload ]
  • b"ASAPv1" — 6-byte ASCII sentinel, encoded as 0x41 0x53 0x41 0x50 0x76 0x31
  • version — envelope layout version; currently 0x02
  • kind_id_len + kind_id — variable-length sketch discriminant
  • metadata_len + metadata — compact MessagePack metadata block

The sketch payload structure is unchanged; metadata is wrapper-level.

Metadata

Current metadata is a compact MessagePack array in this field order:

[
  metadata_version,
  hash_spec_present,
  hash_profile_id,
  hash_algorithm,
  seed_list,
  canonical_seed_index,
  matrix_seed_index,
  hydra_seed_index,
  univmon_bottom_layer_seed_index,
  seed_derivation,
  input_encoding
]

For the standard ProjectASAP hash profile:

  • metadata_version = 1
  • hash_profile_id = "projectasap.xxh3.seedlist.v1"
  • hash_algorithm = "xxh3_64_128"
  • seed_list = [0xcafe3553, 0xade3415118, 0x8cc70208, 0x2f024b2b, 0x451a3df5, 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, 0xcbbb9d5d, 0x629a292a, 0x9159015a, 0x152fecd8, 0x67332667, 0x8eb44a87, 0xdb0c2e0d]
  • canonical_seed_index = 5
  • matrix_seed_index = 0
  • hydra_seed_index = 6
  • univmon_bottom_layer_seed_index = 19
  • seed_derivation = "seed_list_index_wrap"
  • input_encoding = "projectasap.input.v1"

Decoders validate both the metadata contents and that the metadata matches the
decoded kind_id.

Breaking change

Bytes produced before this PR (bare msgpack, or earlier draft ASAPv1 wrapper
without metadata) will be rejected. No backward-compatibility shim; callers must
re-serialize stored blobs.

Verification

  • cargo test msgpack
  • cargo test

Go mirror

sketchlib-go PR #68 is updated in lock-step.

@milindsrivastava1997

Copy link
Copy Markdown

One thought - a 2 digit hex magic ID restricts us to 256 IDs. While this seems reasonable for the short term, it's not out of the question that the number of sketches may go beyond that. We can make it 3 digits, or if we want to ensure complete future proofness (at the cost of some complexity), the magic ID can be prepended by a number that denotes the number of digits in the magic ID. Are there any references in other frameworks as to how this is handled?

@GordonYuanyc

Copy link
Copy Markdown
Collaborator Author

I agree that a single-byte sketch ID gives us an unnecessary 256-ID ceiling.

Prometheus and OTAP both use lightweight metadata before the raw binary payload so the receiver can interpret the following bytes from the bytes themselves. Prometheus stores a chunk encoding before each chunk payload; OTAP wraps raw Arrow IPC bytes with schema_id/type metadata.

For us, a thin wrapper around the existing MessagePack payload should be enough:

[ "ASK1" | version:u8 | kind_id_len:u8 | kind_id:bytes | msgpack_payload ]

"ASK1" identifies this as an ASAP sketch binary. version is for future wrapper-layout changes. kind_id_len + kind_id avoids the 256-ID limit while keeping small IDs compact. kind_id should be canonical unsigned big-endian with no leading zero bytes.

@GordonYuanyc

Copy link
Copy Markdown
Collaborator Author

With this wrapper, the serialized bytes are self-describing enough for the deserializer to determine which sketch decoder to use.

@milindsrivastava1997

Copy link
Copy Markdown

Sounds good. This encoding scheme makes sense.

@milindsrivastava1997

Copy link
Copy Markdown

@GordonYuanyc

Copy link
Copy Markdown
Collaborator Author

Protobuf is intentionally left untouched. protobuf's oneof already handles type discrimination the same way ASK1 does for msgpack, so there's nothing to add.
This PR pairs with ProjectASAP/sketchlib-go#68 which applies the same ASK1 change on the Go side.

GordonYuanyc and others added 7 commits June 29, 2026 23:24
…mpls

This is a proof-of-concept / starting point for making serialized sketch
binaries self-describing.  Every `to_msgpack()` output now starts with a
single type-discriminant byte (analogous to Prometheus magic bytes for
gauge/histogram), and `from_msgpack()` validates that byte before
deserialising the payload.

Magic-ID table (stable, never reuse a value):
  0x01  HllSketch
  0x02  CountMinSketch
  0x03  CountMinSketchWithHeap
  0x04  CountSketch
  0x05  DdSketch
  0x06  KllSketch
  0x07  HydraKllSketch
  0x08  SetAggregator
  0x09  DeltaResult

Wire format after this change:
  [ magic_id: u8 | <rmp_serde msgpack payload> ]

API is unchanged: callers still call `to_msgpack()` / `from_msgpack()`.
Round-trips still pass.  The `BadMagicId` error variant surfaces
mismatches at decode time instead of producing a confusing type error.

All 426 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…es on all sketch types

All native sketch serialization methods now also carry a magic-ID prefix
(range 0x81–0x89, separate from the portable 0x01–0x09 range):

  0x81  CountMin<_, _>        (native map-format)
  0x82  Count<_, _, _>        (native map-format)
  0x83  CountL2HH / CMSHeap   (native map-format)
  0x84  HyperLogLogImpl<_,_,_>
  0x85  HyperLogLogHIPImpl<_>
  0x86  DDSketch               (native map-format)
  0x87  KLL<T>
  0x88  KLLDynamic<T>
  0x89  KMV

The portable KllSketch and HydraKllSketch wire formats embed KLL bytes via
KLL::serialize_to_bytes / deserialize_from_bytes; those call sites pick up
the magic byte automatically and the portable round-trips continue to work.

The native MessagePackCodec shims in message_pack_format/native/ delegate to
these methods, so they too get the magic byte without any further changes.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- cargo fmt --all to fix formatting diffs caught by CI
- Add docs/msgpack-magic-ids.md documenting the full magic-ID table,
  the portable vs native distinction, the embedded-KLL relationship,
  and instructions for adding future sketch types

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Completes the magic-ID rollout across all serialize_to_bytes /
deserialize_from_bytes call sites in the codebase. The EH files
(eh.rs, eh_sketch_list.rs, eh_univ_optimized.rs) have no serialization
methods and need no changes.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The native header grows from 1 byte to 2 bytes:
  [ family+mode byte | hasher byte | <rmp_serde named payload> ]

Key changes:

serialize_to_bytes / deserialize_from_bytes are now in Mode-specialized
impl blocks instead of a single generic impl, so the first byte encodes
both the sketch family and the phantom-type parameter:

  CountMin<_, RegularPath, _> → 0x81
  CountMin<_, FastPath,    _> → 0x82
  Count<_,   RegularPath, _> → 0x83
  Count<_,   FastPath,    _> → 0x84
  HyperLogLogImpl<Classic,  _, _> → 0x86
  HyperLogLogImpl<ErtlMLE,  _, _> → 0x87

A new hasher_magic_id() method on SketchHasher (default = 0xff =
HASHER_UNKNOWN) fills the second byte.  DefaultXxHasher returns 0x01
(HASHER_DEFAULT_XX).  check_hasher_id<H>(stored) skips the check when
either side is 0xff, so custom hashers interoperate without registering.

Types without an H parameter (DDSketch, KLL, KLLDynamic, KMV, Hydra,
UnivMon, HyperLogLogHIPImpl) always write 0xff or HASHER_DEFAULT_XX as
the second byte for a consistent 2-byte header across all native blobs.

The native MessagePackCodec shims in message_pack_format/native/ are
updated to match the specialized impl split.

Docs updated: docs/msgpack-magic-ids.md now shows the 2-byte native
header layout and the full updated ID table.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Closes the coverage gap: the earlier split-impl work only had RegularPath
round-trip tests.  New tests:

  count_min_fast_path_round_trip_serialization  — verifies magic bytes
    0x82 / HASHER_DEFAULT_XX and data survives serialize→deserialize
  count_min_mode_mismatch_is_rejected           — FastPath bytes →
    RegularPath decoder must error
  count_sketch_fast_path_round_trip_serialization — same for Count
  count_sketch_mode_mismatch_is_rejected
  hll_magic_bytes_are_variant_specific          — Classic=0x86, ErtlMLE=0x87
  hll_variant_mismatch_is_rejected              — ErtlMLE bytes → Classic
    decoder must error

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Implements the wire format proposed in PR #63 review thread.
Every serialized sketch binary is now wrapped in the ASK1 envelope:

  [ b"ASK1" | version:u8 | kind_id_len:u8 | kind_id:bytes | msgpack_payload ]

The `kind_id` replaces the old bare magic-byte header:
- Portable (cross-language): 1-byte kind_id — same ID constants as before
- Native Rust: 2-byte kind_id — [type_discriminant, hasher_id]

The ASK1 framing lifts the 256-sketch-type ceiling while keeping the same
self-describing semantics. The 4-byte sentinel (b"ASK1") is unambiguous
since no msgpack value starts with 0x41.

All three original constraints hold:
- API unchanged (same serialize_to_bytes/from_msgpack signatures)
- Round trips verified (432 tests, all pass)
- Different sketch types/modes produce different kind_ids

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@GordonYuanyc
GordonYuanyc force-pushed the feat/msgpack-magic-id branch from 803bc22 to add3935 Compare June 30, 2026 03:24
@GordonYuanyc GordonYuanyc changed the title feat(msgpack): add magic-ID type-discriminant prefix to wire format [PoC] feat(msgpack): add ASK1 self-describing envelope to wire format Jun 30, 2026
@GordonYuanyc
GordonYuanyc marked this pull request as ready for review June 30, 2026 03:25
GordonYuanyc and others added 2 commits June 30, 2026 00:28
- kmv.rs: encode H::hasher_magic_id() instead of HASHER_UNKNOWN; verify
  stored hasher on deserialize via check_hasher_id::<H>
- hll.rs: validate stored hasher byte in HyperLogLogHIPImpl::deserialize_from_bytes
  (accept HASHER_DEFAULT_XX or HASHER_UNKNOWN, reject anything else)
- portable/*/from_msgpack: propagate decode_wrapper structural failures as
  Error::Decode rather than silently swallowing them into a misleading
  BadMagicId { got: bytes[0] } (bytes[0] is always 0x41 'A' for ASK1 input)
- docs/msgpack-magic-ids.md: fix copy-paste error — KLL inner bytes carry
  prefix 0x8a (NATIVE_KLL), not 0x87 (NATIVE_HLL_ERTL_MLE)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Auto-fixed uninlined format args (clippy::uninlined_format_args) and
applied cargo fmt across the tree.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@GordonYuanyc
GordonYuanyc marked this pull request as draft July 6, 2026 19:01
@GordonYuanyc GordonYuanyc changed the title feat(msgpack): add ASK1 self-describing envelope to wire format feat(msgpack): add ASAPv1 self-describing envelope to wire format Jul 7, 2026
@milindsrivastava1997

Copy link
Copy Markdown

Is there a description of what the sketch payload is, for each sketch family?

@GordonYuanyc

Copy link
Copy Markdown
Collaborator Author

Is there a description of what the sketch payload is, for each sketch family?

Not yet. The payload that can work for cross-language ser/de needs modification.

On the other hand, something that works currently is under the portable subdirectory.

@GordonYuanyc

Copy link
Copy Markdown
Collaborator Author

The PR is marked as draft, instead of close for one reason: it's easier to find reference for possible metadata and envelope design in PR description.

That means, for self-describing, cross-language ser/de, this PR will be broken into several small PRs, where each PR will contain one sketch algorithm.

Portable and payload will be redesigned and will gradually be unified into one place.

@milindsrivastava1997

Copy link
Copy Markdown

Sure. I would say though, it is better to put the envelope and metadata description in a design doc markdown file and add that to the repo. Similary, for the serialization formats for each sketch family (whether it supports cross-language serde or not)

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