Push 5 / P4: the decode conformance corpus, and the tag it could not read back

spec/vectors/decode_vectors.txt -- 37 committed byte strings across five
surfaces, each with its normative accept/reject verdict. The reference
implementation's fuzzers prove its own decoders self-consistent, which says
nothing about whether a foreign decoder agrees with the format. This is what one
is checked against. Gated in the conformance suite as [7d], and drift-locked:
the committed file must equal vectors::render(), so a wire-format change lands
in the diff.

It found a real defect on its first run. OperationKindTag::TransposeInterval
encoded to [30] and its own decoder REJECTED it -- Push 4a added the variant to
discriminant() and never to decode_canonical. OperationKindTag is what edit
barriers persist, so a barrier prohibiting TransposeInterval could be written and
never read back. Silent data loss on reopen.

Four things should have caught it. None did, and two made it worse:

  The round-trip test enumerated DISCRIMINANTS -- (0u8..30).map(decode_canonical)
  -- starting from bytes the decoder already knew, so it structurally could not
  notice a variant the decoder was missing. It now enumerates VARIANTS from one
  all_tags() list, with a completeness check in both directions.

  The distinctness test's hand-written variant list omitted it too. Same list now.

  operation_kind_tag_decode_rejects_malformed_bytes asserted that tag 30 is
  REJECTED, and layout-ir's decode_rejects_unknown_discriminants asserted the
  same at the barrier surface. Both were locking the bug in place and made it
  look deliberate. Both now name 31, and a new barrier test round-trips a barrier
  prohibiting every tag -- the persistence surface where this actually bites.

  The P2 decode fuzzer fed valid corpus bytes to the tag decoder and tallied the
  failure as a REJECTION, like any garbage input. It never asserted that an
  unmutated corpus entry decodes. Both fuzzers now do, as a pre-pass.

The harness had the same disease as the code. `check` collapsed "rejected" with
"accepted but does not re-encode", so a decoder that silently normalizes
non-canonical bytes PASSED the reject vectors it was written to catch. Verified:
removing the whole-state guard, and restoring the lenient compression codec, both
left the corpus green. `check` now returns Ok(injective) for accept and Err for
reject and never conflates them -- silently normalizing non-canonical bytes IS
accepting them. With that fixed, all four defect mutations fail the corpus, each
naming its class.

The corpus pins one vector per class this repo has shipped a bug in:
non-canonical-map-order (a guard catches it; no per-site check exists),
non-canonical-vec-order (only a per-site check catches it; a guard is blind),
lenient-sub-codec (a guard masked it in the manifest; the index had none), plus
trailing-bytes, truncated, unknown-discriminant, count-exceeds-remaining. A test
fails if one goes missing.

Gate: fmt clean, clippy 0, 30 targets / 1024 passed / 0 failed, docs 0 under
-D warnings, conformance 8/8 (now including [7d]), zero golden churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-09 20:16:15 -04:00
parent 09a7f62802
commit a41596d329
14 changed files with 1179 additions and 22 deletions

View File

@ -327,6 +327,22 @@ pub fn run_wire_decode_fuzz(iters: u64, seed: u64) -> WireFuzzCoverage {
.collect()
};
// The corpus must decode. An *unmutated* valid byte string that a decoder
// rejects would otherwise be tallied as a rejection, like any garbage input
// — which is exactly how a broken `OperationKindTag` decoder hid inside the
// ops fuzzer for two commits (Push 5 / P4).
for bytes in &manifests {
let m = Manifest::decode(bytes).expect("a valid manifest must decode");
assert_eq!(m.encode(), *bytes);
}
for bytes in &valid_indices {
let i = OperationIndex::decode(bytes).expect("a valid operation index must decode");
assert_eq!(i.encode(), *bytes);
}
for bytes in &valid_blocks {
block::decode_block(bytes).expect("a valid block payload must decode");
}
for _ in 0..iters {
// 1. Whole-image open. Must never panic; an Ok manifest must re-encode.
let pick = (rng.next_u64() as usize) % images.len();

View File

@ -60,6 +60,7 @@ mod store;
mod superblock;
pub mod fuzz;
pub mod vectors;
pub use block::{
decode_block, encode_block, envelope_offsets, pack_operation_blocks, BLOCK_SOFT_LIMIT,

View File

@ -0,0 +1,322 @@
//! Decode conformance vectors for the bundle wire (P4 of the decode-hardening
//! track). See `epiphany_ops::vectors` for the corpus's purpose and columns.
//!
//! These live inside the crate because several rejection classes can only be
//! *constructed* with internal access: a manifest whose vectors are out of order
//! but whose `manifest_id` is correctly derived over the reordered body isolates
//! the ordering rule from the id rule, and `ManifestId::derive` is crate-private.
use crate::chunk::{ChunkKind, ChunkRef, CompressionAlgorithm};
use crate::ids::{DocumentId, ManifestId, SchemaVersion};
use crate::manifest::Manifest;
use crate::opindex::OperationIndex;
use crate::{block, DecodeError};
use epiphany_determinism::{ChunkId, ContentHash};
/// `(surface, verdict, class, name, bytes)` — see `epiphany_ops::vectors`.
pub type DecodeVector = (
&'static str,
&'static str,
&'static str,
&'static str,
Vec<u8>,
);
/// A `ChunkRef` encodes as 95 bytes: id 32, kind 1, schema 4, offset 8,
/// compressed_length 8, uncompressed_length 8, compression 2, hash 32.
const CHUNK_REF_LEN: usize = 32 + 1 + 4 + 8 + 8 + 8 + 2 + 32;
/// Offset of the compression *parameter* byte within a `ChunkRef`.
const CHUNK_REF_COMPRESSION_PARAM: usize = 32 + 1 + 4 + 8 + 8 + 8 + 1;
fn block_ref(hash_byte: u8, offset: u64) -> ChunkRef {
ChunkRef {
id: ChunkId(ContentHash([hash_byte; 32])),
kind: ChunkKind::OperationEnvelopeBlock,
schema_version: SchemaVersion::V0,
offset,
compressed_length: 64,
uncompressed_length: 64,
compression: CompressionAlgorithm::None,
hash: ContentHash([hash_byte; 32]),
}
}
fn swap(bytes: &[u8], first: usize, width: usize) -> Vec<u8> {
let second = first + width;
let mut out = bytes.to_vec();
out[first..second].copy_from_slice(&bytes[second..second + width]);
out[second..second + width].copy_from_slice(&bytes[first..second]);
out
}
/// Every bundle-wire vector.
pub fn decode_vectors() -> Vec<DecodeVector> {
let mut v: Vec<DecodeVector> = Vec::new();
// --- Manifest ----------------------------------------------------------
const MAN: &str = "bundle.manifest";
let doc = DocumentId([5; 16]);
let empty = Manifest::empty(doc);
v.push((MAN, "accept", "-", "empty_manifest", empty.encode()));
let mut one = Manifest::empty(doc);
one.operation_roots.push(block_ref(0x11, 576));
let one_bytes = one.encode();
v.push((MAN, "accept", "-", "one_operation_root", one_bytes.clone()));
// Body edit: `manifest_id` is derived from the body, so it no longer matches.
let mut id_mismatch = one_bytes.clone();
let last = id_mismatch.len() - 1;
id_mismatch[last] ^= 0xFF;
v.push((
MAN,
"reject",
"manifest-id-mismatch",
"one_root_body_edited",
id_mismatch,
));
// Two roots, encoded in canonical (sorted) order. Swapping them and then
// *re-deriving the id over the reordered body* isolates the ordering rule:
// the id is right, the order is wrong, and only the re-encode guard rejects
// it — `encode_body` sorts, so the round trip cannot reproduce these bytes.
let mut two = Manifest::empty(doc);
two.operation_roots.push(block_ref(0x11, 576));
two.operation_roots.push(block_ref(0x22, 1024));
let two_bytes = two.encode();
v.push((MAN, "accept", "-", "two_operation_roots", two_bytes.clone()));
// Locate the first root: id(16) + document_id(16) + lineage option tag(1)
// + generation(8) + roots count(4).
const ROOTS_AT: usize = 16 + 16 + 1 + 8 + 4;
let mut reordered = swap(&two_bytes, ROOTS_AT, CHUNK_REF_LEN);
let body = &reordered[16..];
let redone = ManifestId::derive(doc, 0, body);
reordered[0..16].copy_from_slice(&redone.0.to_be_bytes());
v.push((
MAN,
"reject",
"non-canonical-vec-order",
"two_roots_out_of_order_valid_id",
reordered,
));
let mut trailing = one_bytes.clone();
trailing.push(0);
v.push((
MAN,
"reject",
"trailing-bytes",
"one_root_trailing",
trailing,
));
// --- OperationIndex ----------------------------------------------------
const IDX: &str = "bundle.operation_index";
let empty_index = OperationIndex::build(&[]).expect("empty").encode();
v.push((IDX, "accept", "-", "empty_index", empty_index.clone()));
let one_index = OperationIndex::build(&[(block_ref(0x11, 576), vec![([2; 16], 8)])])
.expect("one block")
.encode();
v.push((IDX, "accept", "-", "one_block_one_entry", one_index.clone()));
let two_index = OperationIndex::build(&[
(block_ref(0x11, 576), vec![([1; 16], 8), ([3; 16], 40)]),
(block_ref(0x22, 1024), vec![([2; 16], 8)]),
])
.expect("two blocks")
.encode();
v.push((IDX, "accept", "-", "two_blocks", two_index.clone()));
// The lenient-compression class. `CompressionAlgorithm::None` used to ignore
// this byte, so `[0, 0xFF]` and `[0, 0]` decoded alike: two byte strings, one
// value. The index has no whole-value re-encode guard, so it accepted both.
// (Push 5 / P3; `req:binfmt:compression-none-parameter`.)
let mut lenient = one_index.clone();
lenient[4 + CHUNK_REF_COMPRESSION_PARAM] = 0xFF;
v.push((
IDX,
"reject",
"lenient-sub-codec",
"compression_none_non_zero_parameter",
lenient,
));
// Strict ascent, checked per-site (there is no guard here to catch it).
v.push((
IDX,
"reject",
"non-canonical-vec-order",
"blocks_out_of_order",
swap(&two_index, 4, CHUNK_REF_LEN),
));
// Entries are 24 bytes: id(16) + block(4) + offset(4). They begin after the
// blocks and their count.
let entries_at = 4 + 2 * CHUNK_REF_LEN + 4;
v.push((
IDX,
"reject",
"non-canonical-vec-order",
"entries_out_of_order",
swap(&two_index, entries_at, 24),
));
// An entry naming a block ordinal that does not exist.
let mut bad_ordinal = one_index.clone();
let ordinal_at = 4 + CHUNK_REF_LEN + 4 + 16;
bad_ordinal[ordinal_at..ordinal_at + 4].copy_from_slice(&7u32.to_le_bytes());
v.push((
IDX,
"reject",
"block-ordinal-out-of-range",
"entry_names_missing_block",
bad_ordinal,
));
// A block reference whose kind is not an operation-envelope block.
let mut wrong_kind = one_index.clone();
wrong_kind[4 + 32] = ChunkKind::Snapshot.discriminant();
v.push((
IDX,
"reject",
"wrong-chunk-kind",
"block_is_not_an_envelope_block",
wrong_kind,
));
let mut idx_trailing = empty_index.clone();
idx_trailing.push(0);
v.push((
IDX,
"reject",
"trailing-bytes",
"empty_index_trailing",
idx_trailing,
));
// --- Block payload framing ---------------------------------------------
const BLK: &str = "bundle.block";
let payloads: Vec<Vec<u8>> = vec![vec![0xAA; 8], vec![0xBB; 13]];
let packed = block::pack_operation_blocks(&payloads);
let good = packed.first().expect("one block").clone();
v.push((BLK, "accept", "-", "two_envelopes", good.clone()));
let mut blk_trailing = good.clone();
blk_trailing.push(0);
v.push((
BLK,
"reject",
"trailing-bytes",
"two_envelopes_trailing",
blk_trailing,
));
let mut blk_truncated = good.clone();
blk_truncated.pop();
v.push((
BLK,
"reject",
"truncated",
"two_envelopes_truncated",
blk_truncated,
));
// A declared envelope count far past the bytes remaining: the decoder must
// reject on the count, not pre-allocate for it.
let mut huge = good.clone();
huge[0..4].copy_from_slice(&u32::MAX.to_le_bytes());
v.push((
BLK,
"reject",
"count-exceeds-remaining",
"envelope_count_u32_max",
huge,
));
v
}
/// Applies `surface`'s decoder to `bytes`.
///
/// `Ok(injective)` means the decoder **accepted**, and `injective` says whether
/// the value re-encodes to exactly these bytes; `Err` means it **rejected**. See
/// `epiphany_ops::vectors::check` for why those must not be collapsed.
///
/// `None` for surfaces this crate does not own.
pub fn check(surface: &str, bytes: &[u8]) -> Option<Result<bool, String>> {
fn report<T>(r: Result<T, DecodeError>) -> Result<T, String> {
r.map_err(|e| format!("{e:?}"))
}
match surface {
"bundle.manifest" => Some(report(Manifest::decode(bytes)).map(|m| m.encode() == bytes)),
"bundle.operation_index" => {
Some(report(OperationIndex::decode(bytes)).map(|i| i.encode() == bytes))
}
// A block payload is a framing, not a canonical value: `decode_block`
// returns the envelopes, and re-framing them reproduces the payload.
"bundle.block" => Some(report(block::decode_block(bytes)).map(|envelopes| {
block::pack_operation_blocks(&envelopes)
.first()
.is_some_and(|packed| packed == bytes)
})),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_vector_gets_its_declared_verdict() {
for (surface, verdict, class, name, bytes) in decode_vectors() {
let result = check(surface, &bytes).expect("a surface this crate owns");
match (verdict, &result) {
("accept", Ok(true)) => {}
("accept", Ok(false)) => {
panic!("{surface}/{name}: accepted but does not re-encode to its bytes")
}
("reject", Err(_)) => {}
_ => panic!("{surface}/{name} ({class}): declared {verdict}, got {result:?}"),
}
}
}
#[test]
fn every_surface_carries_both_verdicts() {
for surface in ["bundle.manifest", "bundle.operation_index", "bundle.block"] {
let rows: Vec<_> = decode_vectors()
.into_iter()
.filter(|(s, ..)| *s == surface)
.collect();
assert!(
rows.iter().any(|(_, v, ..)| *v == "accept"),
"{surface} has no accept vector"
);
assert!(
rows.iter().any(|(_, v, ..)| *v == "reject"),
"{surface} has no reject vector"
);
}
}
/// The reordered-manifest vector must isolate the *ordering* rule: its
/// `manifest_id` is correctly derived over the reordered body, so it is not
/// rejected merely for a stale id. If this stopped holding, the vector would
/// be pinning the wrong rejection.
#[test]
fn the_reordered_manifest_vector_carries_a_valid_id() {
let (.., bytes) = decode_vectors()
.into_iter()
.find(|(.., name, _)| *name == "two_roots_out_of_order_valid_id")
.expect("the vector exists");
let derived = ManifestId::derive(DocumentId([5; 16]), 0, &bytes[16..]);
let stored = ManifestId(u128::from_be_bytes(
bytes[0..16].try_into().expect("16 bytes"),
));
assert_eq!(stored, derived, "the id matches its (reordered) body");
assert!(Manifest::decode(&bytes).is_err(), "the order still rejects");
}
}

View File

@ -1100,22 +1100,61 @@ mod tests {
tag: 7
})
);
// Operation-kind tag 30 is one past the vocabulary (the Phase-3 ops
// tranche appended 24..=27, the repeat pair 28/29; encodings are
// append-only).
// Operation-kind tag 31 is one past the vocabulary (the Phase-3 ops
// tranche appended 24..=27, the repeat pair 28/29, `TransposeInterval`
// 30; encodings are append-only).
//
// This assertion named 30 until Push 5 / P4 — by which time 30 was
// `TransposeInterval`, so the test was pinning a bug: a barrier that
// prohibited the new operation encoded fine and would not read back.
let mut bytes = vec![0u8];
bytes.extend(set_blob(&[]));
bytes.extend(set_blob(&[vec![30u8]]));
bytes.extend(set_blob(&[vec![31u8]]));
bytes.push(0);
assert_eq!(
EditBarrier::decode_canonical_bytes(&bytes),
Err(BarrierDecodeError::InvalidTag {
kind: "OperationKindTag",
tag: 30
tag: 31
})
);
}
/// A barrier naming *every* operation tag must survive a round trip. Edit
/// barriers are where `OperationKindTag` is persisted, so a tag that encodes
/// and will not decode is silent data loss on reopen — which is exactly what
/// `TransposeInterval` did between Push 4a and Push 5 / P4.
#[test]
fn a_barrier_prohibiting_every_operation_tag_round_trips() {
use epiphany_ops::OperationKindRegistryId;
let mut tags: Vec<OperationKindTag> = (0u8..=30)
.filter(|d| *d != 16)
.map(|d| {
epiphany_determinism::CanonicalDecode::decode_canonical(&[d][..])
.unwrap_or_else(|e| panic!("tag {d} must decode: {e:?}"))
})
.collect();
tags.push(OperationKindTag::Registered(OperationKindRegistryId(7)));
let barrier = EditBarrier {
scope: BarrierScope::WholeScore,
affected_object_kinds: Vec::new(),
prohibited_operation_kinds: tags,
condition: BarrierCondition::Always,
};
let bytes = barrier.to_canonical_bytes();
let decoded = EditBarrier::decode_canonical_bytes(&bytes).expect("round-trips");
// `prohibited_operation_kinds` is encoded as a canonical *set*, so the
// decoded order is the canonical one, not the order it was built in.
// Compare as sets, and pin injectivity on the bytes.
let mut want = barrier.prohibited_operation_kinds.clone();
let mut got = decoded.prohibited_operation_kinds.clone();
want.sort_by_key(epiphany_determinism::CanonicalEncode::to_canonical_bytes);
got.sort_by_key(epiphany_determinism::CanonicalEncode::to_canonical_bytes);
assert_eq!(got, want, "every tag survives the round trip");
assert_eq!(decoded.to_canonical_bytes(), bytes);
}
#[test]
fn decode_rejects_non_nfc_pitch_space_text() {
// scope: PitchSpace(tag 5) carrying a decomposed "café" — canonically

View File

@ -1517,3 +1517,55 @@ Each was mutation-verified against the exact check it locks.
reduces to states with no conflicts, anomalies, pending, or spellings — the very
branches that hold every canonical-order check. Measured: 6 of 12 seeds failed to
produce all four. `build_decode_corpus` now draws until covered and asserts it.
## Push 5 / P4 — the cross-implementation decode corpus, and what it caught
`spec/vectors/decode_vectors.txt`: 37 committed byte strings across five
surfaces (`ops.materialized_state`, `ops.operation_kind_tag`, `bundle.manifest`,
`bundle.operation_index`, `bundle.block`), each with its normative accept/reject
verdict. The reference implementation's fuzzers prove its own decoders
self-consistent, which says nothing about whether a *foreign* decoder agrees
with the format. This is what one is checked against.
**It found a real defect on its first run.** `OperationKindTag::TransposeInterval`
encoded to `[30]` and **its own decoder rejected it**: Push 4a added the variant
to `discriminant()` and never to `decode_canonical`. `OperationKindTag` is what
edit barriers persist, so a barrier prohibiting `TransposeInterval` could be
written and never read back — silent data loss on reopen.
**Four things should have caught it and did not.**
1. `operation_kind_tag_decode_mirrors_encode_exactly` enumerated *discriminants*
(`(0u8..30).map(decode_canonical)`), so it started from bytes the decoder
already knew and structurally could not notice a variant the decoder was
missing. It now enumerates **variants**, from a single `all_tags()` list, with
a completeness check in both directions.
2. `every_normative_operation_tag_has_a_distinct_canonical_discriminant`'s
hand-written variant list also omitted it. Same list now.
3. `operation_kind_tag_decode_rejects_malformed_bytes` asserted **tag 30 is
rejected**, and `epiphany-layout-ir`'s `decode_rejects_unknown_discriminants`
asserted the same at the barrier surface. Both were *locking the bug in
place*, and made it look deliberate. Both now name 31.
4. The P2 decode fuzzer fed valid corpus bytes to the tag decoder and tallied the
failure as a **rejection**, like any garbage input. It never asserted that an
*unmutated* corpus entry decodes. Both fuzzers now do, as a pre-pass.
**The harness had the same disease as the code.** `check` originally collapsed
"rejected" with "accepted but does not re-encode", so a decoder that silently
normalizes non-canonical bytes *passed* the `reject` vectors it was written to
catch — verified: removing the whole-state guard and restoring the lenient
compression codec both left the corpus green. `check` now returns
`Ok(injective)` for accept and `Err` for reject, and the two are never conflated:
**silently normalizing non-canonical bytes IS accepting them.** With that fixed,
all four defect mutations fail the corpus, each naming its class.
The corpus pins one vector per class this repository has shipped a bug in:
`non-canonical-map-order` (a re-encode guard catches it; no per-site check
exists), `non-canonical-vec-order` (only a per-site check catches it; a guard is
blind), `lenient-sub-codec` (a guard *masked* it in the manifest; the index had
none), plus `trailing-bytes`, `truncated`, `unknown-discriminant`,
`count-exceeds-remaining`. `the_corpus_pins_every_class_we_have_shipped_a_bug_in`
fails if one goes missing.
Gated in the conformance suite as `[7d]`, and drift-locked: the committed file
must equal `vectors::render()`, so a wire-format change lands in the diff.

View File

@ -830,6 +830,22 @@ pub fn run_decode_fuzz(iters: u64, seed: u64) -> DecodeFuzzCoverage {
let corpus = build_decode_corpus(&mut rng);
let mut cov = DecodeFuzzCoverage::default();
// The corpus must decode. Obvious, and it was not checked: an *unmutated*
// valid byte string that a decoder rejects was silently tallied as a
// rejection, like any garbage input. `OperationKindTag::TransposeInterval`
// encoded to `[30]` and would not read back for two commits, right here in
// the corpus, and this fuzzer reported green (Push 5 / P4).
for bytes in &corpus.states {
let state = crate::MaterializedState::decode_canonical(bytes)
.expect("a valid materialized state must decode");
assert_eq!(state.canonical_bytes(), *bytes);
}
for bytes in &corpus.tags {
let tag = OperationKindTag::decode_canonical(bytes)
.expect("a valid operation-kind tag must decode");
assert_eq!(tag.to_canonical_bytes(), *bytes);
}
for _ in 0..iters {
let bytes = gen_state_input(&mut rng, &corpus);
match crate::MaterializedState::decode_canonical(&bytes) {

View File

@ -96,6 +96,7 @@ mod validate;
pub mod valuegen;
pub mod fuzz;
pub mod vectors;
pub use anomaly::{
AnomalousReplicaSegment, IntegrityAnomaly, IntegrityAnomalyKind, ReplicaAnomalyReason,

View File

@ -526,6 +526,11 @@ impl CanonicalDecode for OperationKindTag {
27 => OperationKindTag::SetStaffLayout,
28 => OperationKindTag::CreateRepeatStructure,
29 => OperationKindTag::DeleteRepeatStructure,
// Push 4a. Omitting this made `to_canonical_bytes` and
// `decode_canonical` asymmetric: the tag encoded to `[30]` and
// would not read back, so an edit barrier that named
// `TransposeInterval` could be persisted and never reopened.
30 => OperationKindTag::TransposeInterval,
_ => return Err(DecodeError::MalformedDomainTag),
})
}
@ -1897,9 +1902,18 @@ mod tests {
assert_eq!(prim.tag(), OperationKindTag::RespellPitch);
}
#[test]
fn every_normative_operation_tag_has_a_distinct_canonical_discriminant() {
let tags = [
/// **Every** `OperationKindTag` variant, listed by name.
///
/// Enumerating *variants* is the point. The round-trip test used to
/// enumerate *discriminants* — `(0u8..30).map(decode_canonical)` — which
/// starts from bytes the decoder already knows and therefore cannot notice a
/// variant the decoder is missing. Push 4a added `TransposeInterval` to
/// `discriminant()` and not to `decode_canonical`, and that test stayed
/// green while the tag encoded to `[30]` and would not read back.
///
/// Adding a variant without adding it here fails `the_tag_vocabulary_is_complete`.
fn all_tags() -> Vec<OperationKindTag> {
let mut tags = vec![
OperationKindTag::InsertEvent,
OperationKindTag::DeleteEvent,
OperationKindTag::ModifyEvent,
@ -1929,7 +1943,35 @@ mod tests {
OperationKindTag::SetStaffLayout,
OperationKindTag::CreateRepeatStructure,
OperationKindTag::DeleteRepeatStructure,
OperationKindTag::TransposeInterval,
];
tags.push(OperationKindTag::Registered(OperationKindRegistryId(
0x0102_0304_0506_0708_090A_0B0C_0D0E_0F10,
)));
tags
}
/// A compile-time-ish completeness check: `all_tags` must match the
/// vocabulary the decoder accepts, in both directions.
#[test]
fn the_tag_vocabulary_is_complete() {
let tags = all_tags();
assert_eq!(tags.len(), 31, "30 payload-less tags plus `Registered`");
// Every payload-less discriminant 0..=30 except 16 decodes to a variant
// that is in the list.
for d in (0u8..=30).filter(|d| *d != 16) {
let decoded = OperationKindTag::decode_canonical(&[d])
.unwrap_or_else(|e| panic!("discriminant {d} must decode: {e:?}"));
assert!(tags.contains(&decoded), "{decoded:?} missing from all_tags");
}
// And 31 is one past the vocabulary.
assert!(OperationKindTag::decode_canonical(&[31]).is_err());
}
#[test]
fn every_normative_operation_tag_has_a_distinct_canonical_discriminant() {
let tags = all_tags();
let encoded: std::collections::BTreeSet<_> = tags
.iter()
.map(CanonicalEncode::to_canonical_bytes)
@ -1939,17 +1981,13 @@ mod tests {
#[test]
fn operation_kind_tag_decode_mirrors_encode_exactly() {
// Every non-registered variant round-trips through its 1-byte form, and
// the registered variant through its 17-byte (tag + registry id) form.
let mut tags: Vec<OperationKindTag> = (0u8..30)
.filter(|d| *d != 16)
.map(|d| OperationKindTag::decode_canonical(&[d]).expect("known discriminant"))
.collect();
tags.push(OperationKindTag::Registered(OperationKindRegistryId(
0x0102_0304_0506_0708_090A_0B0C_0D0E_0F10,
)));
assert_eq!(tags.len(), 30, "the full tag vocabulary");
for tag in tags {
// Every variant round-trips: the payload-less ones through their 1-byte
// form, `Registered` through its 17-byte (tag + registry id) form.
//
// Driven from `all_tags()`, i.e. from the VARIANTS. Driving it from the
// discriminants the decoder already knows is what let a variant the
// decoder was missing pass unnoticed.
for tag in all_tags() {
let bytes = tag.to_canonical_bytes();
let decoded = OperationKindTag::decode_canonical(&bytes).expect("round-trips");
assert_eq!(decoded, tag);
@ -1964,10 +2002,12 @@ mod tests {
#[test]
fn operation_kind_tag_decode_rejects_malformed_bytes() {
use epiphany_determinism::DecodeError;
// Unknown discriminant (30 is one past the vocabulary): rejected,
// never normalized.
// Unknown discriminant (31 is one past the vocabulary): rejected,
// never normalized. This assertion named 30 until Push 5 / P4, by which
// time 30 was `TransposeInterval` — so the test was actively locking a
// bug in place rather than guarding against one.
assert_eq!(
OperationKindTag::decode_canonical(&[30]),
OperationKindTag::decode_canonical(&[31]),
Err(DecodeError::MalformedDomainTag)
);
// Empty input.

View File

@ -0,0 +1,304 @@
//! Decode conformance vectors for the operation layer (P4 of the
//! decode-hardening track).
//!
//! A curated, committed corpus of byte strings with their normative accept /
//! reject verdict. The reference implementation's own fuzzer proves *its*
//! decoders self-consistent; these vectors say what any decoder must do, so a
//! second implementation can be checked against the format rather than against
//! this code.
//!
//! Each rejection class here is one this repository actually shipped a bug in,
//! or one whose check is invisible to an injectivity fuzzer (see
//! `DECISIONS.md` §"Push 5 / P2"): the whole-state re-encode guard catches
//! fields the decoder *normalizes*, and is blind to order-preserving `Vec`
//! fields, which need per-site order checks. A conforming decoder needs both.
//!
//! The `class` string is informative, not normative: implementations need not
//! agree on error taxonomy, only on the accept/reject verdict.
use epiphany_core::{EventId, OperationId, ReplicaId, TypedObjectId};
use epiphany_determinism::CanonicalEncode;
use crate::{
IntegrityAnomaly, IntegrityAnomalyKind, MaterializedState, ObjectState,
OperationKindRegistryId, OperationKindTag, PendingReason,
};
/// One vector: `(surface, verdict, class, name, bytes)`.
///
/// `verdict` is `"accept"` or `"reject"`. An `accept` vector additionally
/// asserts **injectivity**: the decoded value must re-encode to exactly these
/// bytes.
pub type DecodeVector = (
&'static str,
&'static str,
&'static str,
&'static str,
Vec<u8>,
);
/// Swaps the two equal-length records of `entry` bytes that begin at `first`.
fn swap_records(bytes: &[u8], first: usize, entry: usize) -> Vec<u8> {
let second = first + entry;
let mut out = bytes.to_vec();
out[first..second].copy_from_slice(&bytes[second..second + entry]);
out[second..second + entry].copy_from_slice(&bytes[first..second]);
out
}
/// The offset of the count that first differs between an empty encoding and a
/// two-element one, and the per-record width. Both encodings agree up to the
/// count, and differ in total length by exactly the two records.
fn count_and_entry(empty: &[u8], two: &[u8]) -> (usize, usize) {
let count_at = empty
.iter()
.zip(two.iter())
.position(|(a, b)| a != b)
.expect("the counts differ");
(count_at, (two.len() - empty.len()) / 2)
}
fn object(counter: u64) -> TypedObjectId {
TypedObjectId::Event(EventId::new(ReplicaId(1), counter))
}
fn anomaly(counter: u64) -> IntegrityAnomaly {
IntegrityAnomaly::new(IntegrityAnomalyKind::OperationSlotEquivocated {
operation_id: OperationId::new(ReplicaId(1), counter),
})
}
/// Every operation-layer vector.
pub fn decode_vectors() -> Vec<DecodeVector> {
let mut v: Vec<DecodeVector> = Vec::new();
// --- MaterializedState -------------------------------------------------
const MS: &str = "ops.materialized_state";
let empty = MaterializedState::default().canonical_bytes();
v.push((MS, "accept", "-", "empty_state", empty.clone()));
// Two objects, canonically ordered. Swapping them is caught only by the
// whole-state re-encode guard: `objects` is a BTreeMap, so the decoder
// silently re-sorts it and no per-site check exists.
let two_objects = MaterializedState {
objects: [
(object(1), ObjectState::Live),
(object(2), ObjectState::Live),
]
.into_iter()
.collect(),
..Default::default()
}
.canonical_bytes();
let (at, entry) = count_and_entry(&empty, &two_objects);
v.push((MS, "accept", "-", "two_objects", two_objects.clone()));
v.push((
MS,
"reject",
"non-canonical-map-order",
"objects_out_of_order",
swap_records(&two_objects, at + 4, entry),
));
// Two anomalies, canonically ordered. `anomalies` is a Vec whose order the
// decoder PRESERVES, so a swap re-encodes to itself and the whole-state
// guard is blind: only a per-site order check rejects it.
let (lo, hi) = {
let (a, b) = (anomaly(1), anomaly(2));
if a.id < b.id {
(a, b)
} else {
(b, a)
}
};
let two_anomalies = MaterializedState {
anomalies: vec![lo, hi],
..Default::default()
}
.canonical_bytes();
let (at, entry) = count_and_entry(&empty, &two_anomalies);
v.push((MS, "accept", "-", "two_anomalies", two_anomalies.clone()));
v.push((
MS,
"reject",
"non-canonical-vec-order",
"anomalies_out_of_order",
swap_records(&two_anomalies, at + 4, entry),
));
// Same for `pending`, whose entries are (OperationId, PendingReason) pairs.
let (p1, p2) = (
OperationId::new(ReplicaId(1), 1),
OperationId::new(ReplicaId(1), 2),
);
let two_pending = MaterializedState {
pending: vec![
(p1, PendingReason::MissingCausalPredecessor { missing: p1 }),
(p2, PendingReason::MissingCausalPredecessor { missing: p1 }),
],
..Default::default()
}
.canonical_bytes();
let (at, entry) = count_and_entry(&empty, &two_pending);
v.push((MS, "accept", "-", "two_pending", two_pending.clone()));
v.push((
MS,
"reject",
"non-canonical-vec-order",
"pending_out_of_order",
swap_records(&two_pending, at + 4, entry),
));
let mut trailing = empty.clone();
trailing.push(0);
v.push((
MS,
"reject",
"trailing-bytes",
"empty_state_trailing",
trailing,
));
let mut truncated = empty.clone();
truncated.pop();
v.push((
MS,
"reject",
"truncated",
"empty_state_truncated",
truncated,
));
// A count prefix far past the bytes remaining. The decoder must not
// pre-allocate on it, and must not loop toward EOF for a measurable time.
let mut huge_count = empty.clone();
huge_count[0..4].copy_from_slice(&u32::MAX.to_le_bytes());
v.push((
MS,
"reject",
"count-exceeds-remaining",
"effects_count_u32_max",
huge_count,
));
// --- OperationKindTag --------------------------------------------------
const TAG: &str = "ops.operation_kind_tag";
for (name, tag) in [
("insert_event", OperationKindTag::InsertEvent),
("transpose_frozen", OperationKindTag::Transpose),
("transpose_interval", OperationKindTag::TransposeInterval),
(
"registered",
OperationKindTag::Registered(OperationKindRegistryId(0x0123_4567_89AB_CDEF)),
),
] {
v.push((TAG, "accept", "-", name, tag.to_canonical_bytes()));
}
v.push((TAG, "reject", "unknown-discriminant", "tag_200", vec![200]));
v.push((TAG, "reject", "truncated", "tag_empty", Vec::new()));
v.push((
TAG,
"reject",
"trailing-bytes",
"insert_event_trailing",
vec![0, 0],
));
// `Registered` is 1 + 16 bytes; one short must not read past the end.
let mut short_registered =
OperationKindTag::Registered(OperationKindRegistryId(1)).to_canonical_bytes();
short_registered.pop();
v.push((
TAG,
"reject",
"truncated",
"registered_one_byte_short",
short_registered,
));
v
}
/// Applies `surface`'s decoder to `bytes`.
///
/// `Ok(injective)` means the decoder **accepted**, and `injective` says whether
/// the value re-encodes to exactly these bytes. `Err` means it **rejected**.
///
/// The two are deliberately not collapsed. A decoder that accepts non-canonical
/// bytes and silently normalizes them is *not* rejecting them — that is the
/// whole defect class (`non-canonical-map-order`, `lenient-sub-codec`), and an
/// earlier version of this function reported it as a rejection, so the corpus
/// passed against decoders it was written to catch.
///
/// `None` when the surface is not owned by this crate.
pub fn check(surface: &str, bytes: &[u8]) -> Option<Result<bool, String>> {
match surface {
"ops.materialized_state" => Some(match MaterializedState::decode_canonical(bytes) {
Ok(state) => Ok(state.canonical_bytes() == bytes),
Err(e) => Err(format!("{e}")),
}),
"ops.operation_kind_tag" => Some(decode_tag(bytes)),
_ => None,
}
}
fn decode_tag(bytes: &[u8]) -> Result<bool, String> {
use epiphany_determinism::CanonicalDecode;
match OperationKindTag::decode_canonical(bytes) {
Ok(tag) => Ok(tag.to_canonical_bytes() == bytes),
Err(e) => Err(format!("{e:?}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Each vector must get the verdict it declares. This is the property a
/// second implementation is being asked to satisfy; if the reference cannot,
/// the corpus is wrong.
#[test]
fn every_vector_gets_its_declared_verdict() {
for (surface, verdict, class, name, bytes) in decode_vectors() {
let result = check(surface, &bytes).expect("a surface this crate owns");
match (verdict, &result) {
("accept", Ok(true)) => {}
("accept", Ok(false)) => {
panic!("{surface}/{name}: accepted but does not re-encode to its bytes")
}
("reject", Err(_)) => {}
_ => panic!("{surface}/{name} ({class}): declared {verdict}, got {result:?}"),
}
}
}
/// The corpus must actually contain both verdicts on every surface, or it is
/// pinning half a contract.
#[test]
fn every_surface_carries_both_verdicts() {
for surface in ["ops.materialized_state", "ops.operation_kind_tag"] {
let rows: Vec<_> = decode_vectors()
.into_iter()
.filter(|(s, ..)| *s == surface)
.collect();
assert!(
rows.iter().any(|(_, v, ..)| *v == "accept"),
"{surface} has no accept vector"
);
assert!(
rows.iter().any(|(_, v, ..)| *v == "reject"),
"{surface} has no reject vector"
);
}
}
/// The two rejection classes that need *different* machinery: a map order a
/// re-encode guard catches, and a `Vec` order only a per-site check catches.
/// If either vector went missing the corpus would stop pinning the lesson.
#[test]
fn the_corpus_pins_both_non_canonical_classes() {
let classes: Vec<&str> = decode_vectors().iter().map(|(_, _, c, ..)| *c).collect();
assert!(classes.contains(&"non-canonical-map-order"));
assert!(classes.contains(&"non-canonical-vec-order"));
}
}

View File

@ -153,5 +153,25 @@ fn main() {
}
}
eprintln!("[7d ] cross-implementation decode conformance vectors");
{
use epiphany_testkit::vectors;
assert_eq!(
vectors::COMMITTED,
vectors::render(),
"{} is stale; regenerate with `cargo run -q -p epiphany-testkit \
--example generate_vectors`",
vectors::PATH
);
match vectors::verify(vectors::COMMITTED) {
Ok(n) => eprintln!(" {n} vectors, every verdict agreed"),
Err(failures) => panic!(
"{} decode-vector disagreement(s):\n{}",
failures.len(),
failures.join("\n")
),
}
}
eprintln!("[8/8] ok: full conformance suite passed (scale {scale})");
}

View File

@ -0,0 +1,9 @@
//! Regenerates `spec/vectors/decode_vectors.txt`, the cross-implementation
//! decode conformance corpus. Run from the workspace root.
fn main() {
let text = epiphany_testkit::vectors::render();
let path = epiphany_testkit::vectors::PATH;
std::fs::write(path, &text).unwrap_or_else(|e| panic!("writing {path}: {e}"));
let rows = epiphany_testkit::vectors::parse(&text).expect("parses");
eprintln!("wrote {} vectors to {path}", rows.len());
}

View File

@ -91,6 +91,7 @@
//! materialization through a bundle snapshot.
pub mod rng;
pub mod vectors;
// Phase 2, Agent F (worklist F1): the Chapter 10 performance-budget gate the
// `benches/` targets assert through (Pass / Xfail rows with thresholds written

View File

@ -0,0 +1,268 @@
//! The cross-implementation decode conformance corpus (P4 of the
//! decode-hardening track).
//!
//! `spec/vectors/decode_vectors.txt` is a committed, human-diffable list of byte
//! strings and their normative accept/reject verdict. It is what a *second*
//! implementation is checked against — the reference implementation's fuzzers
//! prove its own decoders self-consistent, which says nothing about whether a
//! foreign decoder agrees with the format.
//!
//! Two properties are enforced here:
//!
//! 1. **The committed file is what the generator produces.** A wire-format
//! change that moves a vector's bytes must move the file too, deliberately,
//! in the diff.
//! 2. **Every vector gets its declared verdict** from the owning crate's
//! decoder. An `accept` vector additionally must re-encode to its own bytes.
//!
//! The `class` column is informative: implementations need not agree on error
//! taxonomy, only on accept versus reject.
/// The committed corpus.
pub const COMMITTED: &str = include_str!("../../../spec/vectors/decode_vectors.txt");
/// The path a regeneration writes to, relative to the workspace root.
pub const PATH: &str = "spec/vectors/decode_vectors.txt";
const HEADER: &str = "\
# Epiphany decode conformance vectors format version 1
#
# Generated. Regenerate with:
# cargo run -q -p epiphany-testkit --example generate_vectors
# `epiphany_testkit::vectors::the_committed_corpus_matches_the_generator` fails
# on drift, so a wire-format change must land here deliberately.
#
# One vector per line, space-separated:
#
# <surface> <verdict> <class> <name> <hex>
#
# `verdict` is `accept` or `reject`, and is the ONLY normative column besides
# the bytes. A conforming decoder must accept every `accept` vector and reject
# every `reject` vector. An accepted value must additionally re-encode to
# exactly the vector's bytes: canonical decode is injective, which is what
# content-addressing rests on.
#
# `class` names why a `reject` vector is rejected. It is informative only
# implementations need not agree on error taxonomy. `-` where not applicable.
#
# `<hex>` is lowercase, no separators; `-` denotes the empty byte string.
";
fn to_hex(bytes: &[u8]) -> String {
if bytes.is_empty() {
return "-".to_string();
}
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn from_hex(s: &str) -> Option<Vec<u8>> {
if s == "-" {
return Some(Vec::new());
}
if s.len() % 2 != 0 {
return None;
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect()
}
/// Every vector, in a stable order: the operation layer, then the bundle wire.
fn all() -> Vec<(String, String, String, String, Vec<u8>)> {
let ops = epiphany_ops::vectors::decode_vectors()
.into_iter()
.map(|(s, v, c, n, b)| {
(
s.to_string(),
v.to_string(),
c.to_string(),
n.to_string(),
b,
)
});
let bundle = epiphany_bundle::vectors::decode_vectors()
.into_iter()
.map(|(s, v, c, n, b)| {
(
s.to_string(),
v.to_string(),
c.to_string(),
n.to_string(),
b,
)
});
ops.chain(bundle).collect()
}
/// Renders the corpus file.
pub fn render() -> String {
let mut out = String::from(HEADER);
let mut surface = String::new();
for (s, v, c, n, b) in all() {
if s != surface {
out.push_str("\n# ");
out.push_str(&s);
out.push('\n');
surface = s.clone();
}
out.push_str(&format!("{s} {v} {c} {n} {}\n", to_hex(&b)));
}
out
}
/// One parsed row.
pub struct Row {
pub surface: String,
pub verdict: String,
pub class: String,
pub name: String,
pub bytes: Vec<u8>,
}
/// Parses the corpus file, skipping comments and blank lines.
pub fn parse(text: &str) -> Result<Vec<Row>, String> {
let mut rows = Vec::new();
for (i, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let f: Vec<&str> = line.split_whitespace().collect();
if f.len() != 5 {
return Err(format!(
"line {}: expected 5 columns, got {}",
i + 1,
f.len()
));
}
rows.push(Row {
surface: f[0].to_string(),
verdict: f[1].to_string(),
class: f[2].to_string(),
name: f[3].to_string(),
bytes: from_hex(f[4]).ok_or_else(|| format!("line {}: bad hex", i + 1))?,
});
}
Ok(rows)
}
/// Runs every vector against the owning crate's decoder, returning the number
/// checked or the disagreements.
pub fn verify(text: &str) -> Result<usize, Vec<String>> {
let rows = match parse(text) {
Ok(r) => r,
Err(e) => return Err(vec![e]),
};
let mut failures = Vec::new();
for row in &rows {
let result = epiphany_ops::vectors::check(&row.surface, &row.bytes)
.or_else(|| epiphany_bundle::vectors::check(&row.surface, &row.bytes));
let Some(result) = result else {
failures.push(format!("{}: no decoder owns this surface", row.surface));
continue;
};
// `Ok(injective)` = accepted; `Err` = rejected. A decoder that accepts
// a `reject` vector fails even if it then re-encodes it faithfully, and
// one that accepts an `accept` vector non-injectively fails too. The two
// must not be collapsed: silently normalizing non-canonical bytes IS
// accepting them, and is the defect the corpus exists to catch.
match (row.verdict.as_str(), &result) {
("accept", Ok(true)) | ("reject", Err(_)) => {}
("accept", Ok(false)) => failures.push(format!(
"{}/{}: accepted, but the value does not re-encode to its bytes",
row.surface, row.name
)),
("reject", Ok(injective)) => failures.push(format!(
"{}/{} ({}): declared reject, but was ACCEPTED (injective={injective})",
row.surface, row.name, row.class
)),
_ => failures.push(format!(
"{}/{} ({}): declared {}, got {:?}",
row.surface, row.name, row.class, row.verdict, result
)),
}
}
if failures.is_empty() {
Ok(rows.len())
} else {
Err(failures)
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The file in the tree is exactly what the generator emits. A wire-format
/// change must land in this diff.
#[test]
fn the_committed_corpus_matches_the_generator() {
assert_eq!(
COMMITTED,
render(),
"\n{PATH} is stale. Regenerate:\n \
cargo run -q -p epiphany-testkit --example generate_vectors\n"
);
}
/// The reference implementation satisfies the contract it is publishing. If
/// it cannot, the corpus is wrong, not the decoder.
#[test]
fn the_reference_implementation_agrees_with_every_vector() {
match verify(COMMITTED) {
Ok(n) => assert!(n >= 25, "only {n} vectors — the corpus has thinned"),
Err(failures) => panic!(
"{} disagreement(s):\n{}",
failures.len(),
failures.join("\n")
),
}
}
/// The corpus must pin the rejection classes this repository learned the
/// hard way. Losing one would quietly stop testing it.
#[test]
fn the_corpus_pins_every_class_we_have_shipped_a_bug_in() {
let rows = parse(COMMITTED).expect("parses");
let classes: Vec<&str> = rows.iter().map(|r| r.class.as_str()).collect();
for required in [
// A guard catches this; no per-site check exists (P2).
"non-canonical-map-order",
// Only a per-site check catches this; a guard is blind (P2).
"non-canonical-vec-order",
// A guard *masked* this in the manifest; the index had none (P3).
"lenient-sub-codec",
"trailing-bytes",
"truncated",
"unknown-discriminant",
"count-exceeds-remaining",
] {
assert!(
classes.contains(&required),
"the corpus no longer pins `{required}`"
);
}
}
/// Both verdicts on every surface, or the corpus pins half a contract.
#[test]
fn every_surface_carries_both_verdicts() {
use std::collections::BTreeMap;
let rows = parse(COMMITTED).expect("parses");
let mut seen: BTreeMap<&str, (bool, bool)> = BTreeMap::new();
for r in &rows {
let e = seen.entry(r.surface.as_str()).or_default();
match r.verdict.as_str() {
"accept" => e.0 = true,
"reject" => e.1 = true,
other => panic!("unknown verdict {other}"),
}
}
assert!(seen.len() >= 5, "surfaces: {:?}", seen.keys());
for (surface, (accept, reject)) in seen {
assert!(accept, "{surface} has no accept vector");
assert!(reject, "{surface} has no reject vector");
}
}
}

View File

@ -0,0 +1,68 @@
# Epiphany decode conformance vectors — format version 1
#
# Generated. Regenerate with:
# cargo run -q -p epiphany-testkit --example generate_vectors
# `epiphany_testkit::vectors::the_committed_corpus_matches_the_generator` fails
# on drift, so a wire-format change must land here deliberately.
#
# One vector per line, space-separated:
#
# <surface> <verdict> <class> <name> <hex>
#
# `verdict` is `accept` or `reject`, and is the ONLY normative column besides
# the bytes. A conforming decoder must accept every `accept` vector and reject
# every `reject` vector. An accepted value must additionally re-encode to
# exactly the vector's bytes: canonical decode is injective, which is what
# content-addressing rests on.
#
# `class` names why a `reject` vector is rejected. It is informative only —
# implementations need not agree on error taxonomy. `-` where not applicable.
#
# `<hex>` is lowercase, no separators; `-` denotes the empty byte string.
# ops.materialized_state
ops.materialized_state accept - empty_state 0000000000000000000000000000000000000000000000000000000000000000
ops.materialized_state accept - two_objects 00000000000000000000000002000000000000000000000000010000000000000001000000000000000000000100000000000000020000000000000000000000000000000000
ops.materialized_state reject non-canonical-map-order objects_out_of_order 00000000000000000000000002000000000000000000000000010000000000000002000000000000000000000100000000000000010000000000000000000000000000000000
ops.materialized_state accept - two_anomalies 00000000000000000200000021000000ffffffffffffffff68eefa3a8f8dfa49010000000000000001000000000000000121000000ffffffffffffffffbf0e1228971141c501000000000000000100000000000000020000000000000000000000000000000000000000
ops.materialized_state reject non-canonical-vec-order anomalies_out_of_order 00000000000000000200000021000000ffffffffffffffffbf0e1228971141c5010000000000000001000000000000000221000000ffffffffffffffff68eefa3a8f8dfa4901000000000000000100000000000000010000000000000000000000000000000000000000
ops.materialized_state accept - two_pending 0000000000000000000000000000000000000000000000000000000002000000000000000000000100000000000000010000000000000000010000000000000001000000000000000100000000000000020000000000000000010000000000000001
ops.materialized_state reject non-canonical-vec-order pending_out_of_order 0000000000000000000000000000000000000000000000000000000002000000000000000000000100000000000000020000000000000000010000000000000001000000000000000100000000000000010000000000000000010000000000000001
ops.materialized_state reject trailing-bytes empty_state_trailing 000000000000000000000000000000000000000000000000000000000000000000
ops.materialized_state reject truncated empty_state_truncated 00000000000000000000000000000000000000000000000000000000000000
ops.materialized_state reject count-exceeds-remaining effects_count_u32_max ffffffff00000000000000000000000000000000000000000000000000000000
# ops.operation_kind_tag
ops.operation_kind_tag accept - insert_event 00
ops.operation_kind_tag accept - transpose_frozen 04
ops.operation_kind_tag accept - transpose_interval 1e
ops.operation_kind_tag accept - registered 1000000000000000000123456789abcdef
ops.operation_kind_tag reject unknown-discriminant tag_200 c8
ops.operation_kind_tag reject truncated tag_empty -
ops.operation_kind_tag reject trailing-bytes insert_event_trailing 0000
ops.operation_kind_tag reject truncated registered_one_byte_short 10000000000000000000000000000000
# bundle.manifest
bundle.manifest accept - empty_manifest 6f9e7d11689ab113c4a1f05faf60fe60050505050505050505050505050505050000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000400000000010000000001000000000000
bundle.manifest accept - one_operation_root ae38d9cd408df9b59917c43a82c9d1880505050505050505050505050505050500000000000000000001000000111111111111111111111111111111111111111111111111111111111111111100000001004002000000000000400000000000000040000000000000000000111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000400000000010000000001000000000000
bundle.manifest reject manifest-id-mismatch one_root_body_edited ae38d9cd408df9b59917c43a82c9d18805050505050505050505050505050505000000000000000000010000001111111111111111111111111111111111111111111111111111111111111111000000010040020000000000004000000000000000400000000000000000001111111111111111111111111111111111111111111111111111111111111111000000000000000000000000000001000000000000000000000000000000000000000000000000000000010000000000000000000004000000000100000000010000000000ff
bundle.manifest accept - two_operation_roots 423da9a59202adc10cdd0985146bb1d805050505050505050505050505050505000000000000000000020000001111111111111111111111111111111111111111111111111111111111111111000000010040020000000000004000000000000000400000000000000000001111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222200000001000004000000000000400000000000000040000000000000000000222222222222222222222222222222222222222222222222222222222222222200000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000400000000010000000001000000000000
bundle.manifest reject non-canonical-vec-order two_roots_out_of_order_valid_id c643c490d7705661b435a5e3efe0c5fa05050505050505050505050505050505000000000000000000020000002222222222222222222222222222222222222222222222222222222222222222000000010000040000000000004000000000000000400000000000000000002222222222222222222222222222222222222222222222222222222222222222111111111111111111111111111111111111111111111111111111111111111100000001004002000000000000400000000000000040000000000000000000111111111111111111111111111111111111111111111111111111111111111100000000000000000000000000000100000000000000000000000000000000000000000000000000000001000000000000000000000400000000010000000001000000000000
bundle.manifest reject trailing-bytes one_root_trailing ae38d9cd408df9b59917c43a82c9d188050505050505050505050505050505050000000000000000000100000011111111111111111111111111111111111111111111111111111111111111110000000100400200000000000040000000000000004000000000000000000011111111111111111111111111111111111111111111111111111111111111110000000000000000000000000000010000000000000000000000000000000000000000000000000000000100000000000000000000040000000001000000000100000000000000
# bundle.operation_index
bundle.operation_index accept - empty_index 0000000000000000
bundle.operation_index accept - one_block_one_entry 01000000111111111111111111111111111111111111111111111111111111111111111100000001004002000000000000400000000000000040000000000000000000111111111111111111111111111111111111111111111111111111111111111101000000020202020202020202020202020202020000000008000000
bundle.operation_index accept - two_blocks 020000001111111111111111111111111111111111111111111111111111111111111111000000010040020000000000004000000000000000400000000000000000001111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222200000001000004000000000000400000000000000040000000000000000000222222222222222222222222222222222222222222222222222222222222222203000000010101010101010101010101010101010000000008000000020202020202020202020202020202020100000008000000030303030303030303030303030303030000000028000000
bundle.operation_index reject lenient-sub-codec compression_none_non_zero_parameter 010000001111111111111111111111111111111111111111111111111111111111111111000000010040020000000000004000000000000000400000000000000000ff111111111111111111111111111111111111111111111111111111111111111101000000020202020202020202020202020202020000000008000000
bundle.operation_index reject non-canonical-vec-order blocks_out_of_order 020000002222222222222222222222222222222222222222222222222222222222222222000000010000040000000000004000000000000000400000000000000000002222222222222222222222222222222222222222222222222222222222222222111111111111111111111111111111111111111111111111111111111111111100000001004002000000000000400000000000000040000000000000000000111111111111111111111111111111111111111111111111111111111111111103000000010101010101010101010101010101010000000008000000020202020202020202020202020202020100000008000000030303030303030303030303030303030000000028000000
bundle.operation_index reject non-canonical-vec-order entries_out_of_order 020000001111111111111111111111111111111111111111111111111111111111111111000000010040020000000000004000000000000000400000000000000000001111111111111111111111111111111111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222200000001000004000000000000400000000000000040000000000000000000222222222222222222222222222222222222222222222222222222222222222203000000020202020202020202020202020202020100000008000000010101010101010101010101010101010000000008000000030303030303030303030303030303030000000028000000
bundle.operation_index reject block-ordinal-out-of-range entry_names_missing_block 01000000111111111111111111111111111111111111111111111111111111111111111100000001004002000000000000400000000000000040000000000000000000111111111111111111111111111111111111111111111111111111111111111101000000020202020202020202020202020202020700000008000000
bundle.operation_index reject wrong-chunk-kind block_is_not_an_envelope_block 01000000111111111111111111111111111111111111111111111111111111111111111102000001004002000000000000400000000000000040000000000000000000111111111111111111111111111111111111111111111111111111111111111101000000020202020202020202020202020202020000000008000000
bundle.operation_index reject trailing-bytes empty_index_trailing 000000000000000000
# bundle.block
bundle.block accept - two_envelopes 0200000008000000aaaaaaaaaaaaaaaa0d000000bbbbbbbbbbbbbbbbbbbbbbbbbb
bundle.block reject trailing-bytes two_envelopes_trailing 0200000008000000aaaaaaaaaaaaaaaa0d000000bbbbbbbbbbbbbbbbbbbbbbbbbb00
bundle.block reject truncated two_envelopes_truncated 0200000008000000aaaaaaaaaaaaaaaa0d000000bbbbbbbbbbbbbbbbbbbbbbbb
bundle.block reject count-exceeds-remaining envelope_count_u32_max ffffffff08000000aaaaaaaaaaaaaaaa0d000000bbbbbbbbbbbbbbbbbbbbbbbbbb