From 38f75cf6d70a7ea40c5890ac0aaa9bdcdcc89923 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:41:21 +0700 Subject: [PATCH 01/26] spec(port): port the header extension order guard (Refs #8884) Landed from #9641 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...04::src::header_extension_order_guard.json | 28 +++ docs/reports/t27b_expectations.json | 1 + .../src/header_extension_order_guard.t27 | 216 ++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::header_extension_order_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::header_extension_order_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::header_extension_order_guard.json new file mode 100644 index 0000000000..058ee41974 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::header_extension_order_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:82d590ff4a4938a476c7715bd78729c3c524c01b5d4dfa1eb06d1e6d0e4a5327", + "gen_hash_rust": "sha256:ce537a7f080dd55bb72af7e40c4e68684f13438da58ce4b996a854d30b6a6ecb", + "gen_hash_verilog": "sha256:3f0a0318dcde26c02ad6de50bc096b850c569d29de0a0d6638b2d0a900d6fd17", + "gen_hash_zig": "sha256:4ca9be6d102f94dfa9edf187f1595f55619904e729af1f0376fc850657454ef6", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::header_extension_order_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-15T16:53:51Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:d0d250c0080fd11de83411c9a9fcaf34a47df8ca76da371e7358cf73c9577a5b", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 3483595ccc..54ff0cc752 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -857,6 +857,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_resolution_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_update_integrity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/unmerged_leaves_bound_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_alignment_boundary_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_size_alignment_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.t27 new file mode 100644 index 0000000000..2161762694 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.t27 @@ -0,0 +1,216 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::header_extension_order_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.rs +// (CR-CHAT-04 - Header extension order guard, Wave-72 Lane B, requirement R-CHAT-4). +// +// Wire message headers carry typed extensions, and they must be in +// canonical order: sorted by ascending type ID, no duplicates, IDs in +// [HEXO_MIN_TYPE, HEXO_MAX_TYPE], at most HEXO_MAX_EXTENSIONS of them, +// each with a non-empty payload no larger than HEXO_MAX_PAYLOAD. +// +// The original returns Result<(), HeaderExtError>. This port carries the +// verdict as a named struct (owner decision D5): Ok first, then the six +// error variants in the original's declaration order - NotSorted, +// DuplicateType, TypeOutOfRange, TooManyExtensions, EmptyPayload, +// PayloadTooLarge. Every payload field of every variant is carried: +// the offending type_id (DuplicateType, TypeOutOfRange, EmptyPayload, +// PayloadTooLarge). Variants without a payload read 0. + +// Minimum extension type ID. +pub const HEXO_MIN_TYPE: u16 = 1; + +// Maximum extension type ID. +pub const HEXO_MAX_TYPE: u16 = 65535; + +// Maximum extensions per header. +pub const HEXO_MAX_EXTENSIONS: usize = 16; + +// Maximum payload per extension. +pub const HEXO_MAX_PAYLOAD: usize = 4096; + +// A header extension record. +pub struct HeaderExtension { + type_id: u16, + payload_len: usize, +} + +// Verdict codes: Ok(()) and HeaderExtError's variants, in declaration order. +pub enum HeaderExtCode { + Ok, + NotSorted, + DuplicateType, + TypeOutOfRange, + TooManyExtensions, + EmptyPayload, + PayloadTooLarge, +} + +// The verdict of the header extension validation, with every error payload field. +pub struct HeaderExtVerdict { + code: HeaderExtCode, + type_id: u16, +} + +// [VERIFIED] Validate header extensions are in canonical order. Returns Ok +// when every extension satisfies all six rules, otherwise the first +// violated rule's code, checked in the original's order: extension count, +// then per extension the type floor, payload non-empty, payload ceiling, +// duplicate type, ascending order. +// +// The original's Option previous-type reference is a has_ref flag plus +// the value here. The original's Vec payload is only ever read by +// length, so the record carries the length. +pub fn validate_header_ext_order(extensions: []const HeaderExtension) -> HeaderExtVerdict { + if (extensions.len > HEXO_MAX_EXTENSIONS) { + return HeaderExtVerdict { code: HeaderExtCode.TooManyExtensions, type_id: 0, }; + } + var has_prev: bool = false; + var prev_type: u16 = 0; + for i in 0..extensions.len { + const ext = extensions[i]; + if (ext.type_id < HEXO_MIN_TYPE) { + return HeaderExtVerdict { code: HeaderExtCode.TypeOutOfRange, type_id: ext.type_id, }; + } + if (ext.payload_len == 0) { + return HeaderExtVerdict { code: HeaderExtCode.EmptyPayload, type_id: ext.type_id, }; + } + if (ext.payload_len > HEXO_MAX_PAYLOAD) { + return HeaderExtVerdict { code: HeaderExtCode.PayloadTooLarge, type_id: ext.type_id, }; + } + if (has_prev) { + if (ext.type_id == prev_type) { + return HeaderExtVerdict { code: HeaderExtCode.DuplicateType, type_id: ext.type_id, }; + } + if (ext.type_id < prev_type) { + return HeaderExtVerdict { code: HeaderExtCode.NotSorted, type_id: 0, }; + } + } + has_prev = true; + prev_type = ext.type_id; + } + return HeaderExtVerdict { code: HeaderExtCode.Ok, type_id: 0, }; +} + +// **HEXO-01** - not sorted rejected: 5 before 1. +test hexo_01_not_sorted_rejected { + const exts: [2]HeaderExtension = [2]HeaderExtension{ + HeaderExtension { type_id: 5, payload_len: 10, }, + HeaderExtension { type_id: 1, payload_len: 10, }, + }; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.NotSorted); +} + +// **HEXO-02** - duplicate type rejected: two extensions of type 1. +test hexo_02_duplicate_rejected { + const exts: [2]HeaderExtension = [2]HeaderExtension{ + HeaderExtension { type_id: 1, payload_len: 10, }, + HeaderExtension { type_id: 1, payload_len: 20, }, + }; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.DuplicateType); + assert(v.type_id == 1); +} + +// **HEXO-03** - type out of range rejected: 0 is below HEXO_MIN_TYPE. +test hexo_03_type_oob_rejected { + const exts: [1]HeaderExtension = [1]HeaderExtension{ + HeaderExtension { type_id: 0, payload_len: 10, }, + }; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.TypeOutOfRange); + assert(v.type_id == 0); +} + +// **HEXO-04** - too many extensions rejected: 17 of a maximum 16. The type +// IDs come from a const table - the values fit u16 with no cast, which the +// native backend cannot lower. +test hexo_04_too_many_rejected { + var exts: [17]HeaderExtension; + const ids: [17]u16 = [17]u16{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,}; + for i in 0..17 { + exts[i] = HeaderExtension { type_id: ids[i], payload_len: 10, }; + } + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.TooManyExtensions); +} + +// **HEXO-05** - empty payload rejected. +test hexo_05_empty_payload_rejected { + const exts: [1]HeaderExtension = [1]HeaderExtension{ + HeaderExtension { type_id: 1, payload_len: 0, }, + }; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.EmptyPayload); + assert(v.type_id == 1); +} + +// **HEXO-06** - payload too large rejected: 4097 of a maximum 4096. +test hexo_06_payload_large_rejected { + const exts: [1]HeaderExtension = [1]HeaderExtension{ + HeaderExtension { type_id: 1, payload_len: 4097, }, + }; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.PayloadTooLarge); + assert(v.type_id == 1); +} + +// **HEXO-07** - valid extensions accepted: three ascending types. +test hexo_07_valid_accepted { + const exts: [3]HeaderExtension = [3]HeaderExtension{ + HeaderExtension { type_id: 1, payload_len: 10, }, + HeaderExtension { type_id: 5, payload_len: 20, }, + HeaderExtension { type_id: 100, payload_len: 30, }, + }; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.Ok); +} + +// **HEXO-08** - single extension accepted. +test hexo_08_single_accepted { + const exts: [1]HeaderExtension = [1]HeaderExtension{ + HeaderExtension { type_id: 1, payload_len: 10, }, + }; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.Ok); +} + +// **HEXO-09** - empty list accepted. +test hexo_09_empty_accepted { + const exts: [0]HeaderExtension = [0]HeaderExtension{}; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.Ok); +} + +// **HEXO-10** - the maximum 16 extensions accepted, one per ascending type. +test hexo_10_max_accepted { + var exts: [16]HeaderExtension; + const ids: [16]u16 = [16]u16{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,}; + for i in 0..16 { + exts[i] = HeaderExtension { type_id: ids[i], payload_len: 10, }; + } + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.Ok); +} + +// Rule order is the original's: type floor before payload emptiness, +// emptiness before payload ceiling, and the batch-size rule before +// everything. +test rule_order_matches_original { + const oob = [1]HeaderExtension{ HeaderExtension { type_id: 0, payload_len: 0, }, }; + const va = validate_header_ext_order(&oob); + assert(va.code == HeaderExtCode.TypeOutOfRange); + const large = [1]HeaderExtension{ HeaderExtension { type_id: 1, payload_len: 4097, }, }; + const vb = validate_header_ext_order(&large); + assert(vb.code == HeaderExtCode.PayloadTooLarge); +} + +// HEXO_MAX_TYPE is the ceiling the range check never rejects: the largest +// type ID passes its floor rule and the boundary stays measurable. +test max_type_boundary_accepted { + const exts: [1]HeaderExtension = [1]HeaderExtension{ + HeaderExtension { type_id: HEXO_MAX_TYPE, payload_len: 10, }, + }; + const v = validate_header_ext_order(&exts); + assert(v.code == HeaderExtCode.Ok); +} From 6098487f33d3cbc55aa658959b41615c4e6c273c Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 01:41:28 +0700 Subject: [PATCH 02/26] spec(port): port the padding byte entropy guard (Refs #8884) Landed from #9660 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...T-04::src::padding_byte_entropy_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/padding_byte_entropy_guard.t27 | 254 ++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_byte_entropy_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_byte_entropy_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_byte_entropy_guard.json new file mode 100644 index 0000000000..585d63b527 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_byte_entropy_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:447a6883b3851936a9f01e7736dc84a5ca9cbc1b341adccab87f6fdc8016c472", + "gen_hash_rust": "sha256:1e8d29faa7f66b17fd5d9857f3600b6086e4c594e724fbc0016fb92ed3be2a0d", + "gen_hash_verilog": "sha256:895282ce5203eb9f5cf1b1f4d93b02c7660ca793dae1f680a9017caf8c106a7f", + "gen_hash_zig": "sha256:abed3e29b9b8781d92d85252559824ec76082f16b0f2e2f8f05e11ba5ab625a7", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_byte_entropy_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-15T21:59:06Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:9571ce3cbdce39b7aa059c66765315503e8040a71ba0a186926b786ee2dc0be9", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 14, + "total": 14, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 54ff0cc752..941c35aa5d 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -860,6 +860,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_alignment_boundary_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_size_alignment_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27 new file mode 100644 index 0000000000..3ddc532b38 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27 @@ -0,0 +1,254 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_byte_entropy_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.rs +// (CR-CHAT-04 - Padding byte entropy guard, Wave-77 Lane A, requirement R-CHAT-4). +// +// Padding bytes must resist statistical separation from the encrypted +// payload: not empty, within its length bounds, not one repeated byte, +// at least PBEG_MIN_UNIQUE distinct values, and no single byte more +// frequent than the PBEG_MAX_FREQ_RATIO_NUM/PBEG_MAX_FREQ_RATIO_DEN +// ceiling. The original's BTreeMap frequency count becomes a fixed +// 256-slot table indexed by the byte (the conventions' own form for a +// key range that fits an array); the table is initialized from a +// literal because the Rust backend leaves an uninitialized binding +// unreadable (E0381, measured). +// +// The original returns Result<(), PaddingEntropyError>. This port +// carries the verdict as a named struct (owner decision D5): Ok first, +// then the six error variants in the original's declaration order - +// TooShort, TooFewUnique, ByteTooFrequent, TooLong, AllSameByte, Empty. +// The verdict carries the unique count TooFewUnique reports (got). + +// Minimum padding length. +pub const PBEG_MIN_LEN: usize = 16; + +// Maximum padding length. +pub const PBEG_MAX_LEN: usize = 65536; + +// Minimum unique byte values. +pub const PBEG_MIN_UNIQUE: usize = 8; + +// Maximum frequency ratio numerator (max count / total). +pub const PBEG_MAX_FREQ_RATIO_NUM: usize = 4; + +// Maximum frequency ratio denominator. +pub const PBEG_MAX_FREQ_RATIO_DEN: usize = 10; + +// Verdict codes: Ok(()) and PaddingEntropyError's variants, in declaration order. +pub enum PaddingEntropyCode { + Ok, + TooShort, + TooFewUnique, + ByteTooFrequent, + TooLong, + AllSameByte, + Empty, +} + +// The verdict of the entropy validation, with the payload field the one +// reporting variant carries. +pub struct PaddingEntropyVerdict { + code: PaddingEntropyCode, + got: usize, +} + +// The zeroed frequency table, initialized at declaration: the Rust +// backend emits an uninitialized binding otherwise and the read on the +// right side of the increment fails (E0381, measured). +pub const PBEG_ZERO_TABLE: [256]u32 = [256]u32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The number of distinct byte values in the padding. +pub fn unique_count(padding: []const u8) -> usize { + var freq: [256]u32 = PBEG_ZERO_TABLE; + for i in 0..padding.len { + const b: u8 = padding[i]; + const slot: usize = @as(usize, b); + freq[slot] = freq[slot] + 1; + } + var unique: usize = 0; + for i in 0..256 { + if (freq[i] > 0) { + unique = unique + 1; + } + } + return unique; +} + +// The highest single-byte count in the padding. +pub fn max_count(padding: []const u8) -> u32 { + var freq: [256]u32 = PBEG_ZERO_TABLE; + for i in 0..padding.len { + const b: u8 = padding[i]; + const slot: usize = @as(usize, b); + freq[slot] = freq[slot] + 1; + } + var m: u32 = 0; + for i in 0..256 { + if (freq[i] > m) { + m = freq[i]; + } + } + return m; +} + +// [VERIFIED] Validate that padding bytes have sufficient entropy. +// Returns Ok only when every rule holds, checked in the original's +// order: non-empty, at least PBEG_MIN_LEN long, at most PBEG_MAX_LEN +// long, not one repeated byte, at least PBEG_MIN_UNIQUE distinct +// values, and no byte more frequent than the ratio ceiling. +pub fn validate_padding_entropy(padding: []const u8) -> PaddingEntropyVerdict { + if (padding.len == 0) { + return PaddingEntropyVerdict { code: PaddingEntropyCode.Empty, got: 0, }; + } + if (padding.len < PBEG_MIN_LEN) { + return PaddingEntropyVerdict { code: PaddingEntropyCode.TooShort, got: 0, }; + } + if (padding.len > PBEG_MAX_LEN) { + return PaddingEntropyVerdict { code: PaddingEntropyCode.TooLong, got: 0, }; + } + const first: u8 = padding[0]; + var same: bool = true; + for i in 0..padding.len { + if (padding[i] != first) { + same = false; + } + } + if (same) { + return PaddingEntropyVerdict { code: PaddingEntropyCode.AllSameByte, got: 0, }; + } + const unique: usize = unique_count(padding); + if (unique < PBEG_MIN_UNIQUE) { + return PaddingEntropyVerdict { code: PaddingEntropyCode.TooFewUnique, got: unique, }; + } + const m: u32 = max_count(padding); + if (@as(u64, m) * @as(u64, PBEG_MAX_FREQ_RATIO_DEN) > @as(u64, padding.len) * @as(u64, PBEG_MAX_FREQ_RATIO_NUM)) { + return PaddingEntropyVerdict { code: PaddingEntropyCode.ByteTooFrequent, got: 0, }; + } + return PaddingEntropyVerdict { code: PaddingEntropyCode.Ok, got: 0, }; +} + +// The fixtures of the tests, precomputed literal tables: the original +// builds each by arithmetic over its index; the literals carry the same +// bytes without a narrowing cast. +pub const PBEG_GOOD: [16]u8 = [16]u8{42, 59, 76, 93, 110, 127, 144, 161, 178, 195, 212, 229, 246, 7, 24, 41,}; +pub const PBEG_MOD3: [16]u8 = [16]u8{0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0,}; +pub const PBEG_ALT: [16]u8 = [16]u8{1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,}; +pub const PBEG_MINL: [16]u8 = [16]u8{7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84, 91, 98, 105, 112,}; +pub const PBEG_BIG: [256]u8 = [256]u8{17, 148, 23, 154, 29, 160, 35, 166, 41, 172, 47, 178, 53, 184, 59, 190, 65, 196, 71, 202, 77, 208, 83, 214, 89, 220, 95, 226, 101, 232, 107, 238, 113, 244, 119, 250, 125, 0, 131, 6, 137, 12, 143, 18, 149, 24, 155, 30, 161, 36, 167, 42, 173, 48, 179, 54, 185, 60, 191, 66, 197, 72, 203, 78, 209, 84, 215, 90, 221, 96, 227, 102, 233, 108, 239, 114, 245, 120, 251, 126, 1, 132, 7, 138, 13, 144, 19, 150, 25, 156, 31, 162, 37, 168, 43, 174, 49, 180, 55, 186, 61, 192, 67, 198, 73, 204, 79, 210, 85, 216, 91, 222, 97, 228, 103, 234, 109, 240, 115, 246, 121, 252, 127, 2, 133, 8, 139, 14, 145, 20, 151, 26, 157, 32, 163, 38, 169, 44, 175, 50, 181, 56, 187, 62, 193, 68, 199, 74, 205, 80, 211, 86, 217, 92, 223, 98, 229, 104, 235, 110, 241, 116, 247, 122, 253, 128, 3, 134, 9, 140, 15, 146, 21, 152, 27, 158, 33, 164, 39, 170, 45, 176, 51, 182, 57, 188, 63, 194, 69, 200, 75, 206, 81, 212, 87, 218, 93, 224, 99, 230, 105, 236, 111, 242, 117, 248, 123, 254, 129, 4, 135, 10, 141, 16, 147, 22, 153, 28, 159, 34, 165, 40, 171, 46, 177, 52, 183, 58, 189, 64, 195, 70, 201, 76, 207, 82, 213, 88, 219, 94, 225, 100, 231, 106, 237, 112, 243, 118, 249, 124, 255, 130, 5, 136, 11, 142,}; +pub const PBEG_EXACT: [16]u8 = [16]u8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,}; +pub const PBEG_FREQ: [16]u8 = [16]u8{0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 1,}; + +// **PBEG-01** - too short rejected: 4 of a minimum 16. +test pbeg_01_too_short_rejected { + const xs: [4]u8 = [4]u8{1, 2, 3, 4,}; + const v = validate_padding_entropy(&xs); + assert(v.code == PaddingEntropyCode.TooShort); +} + +// **PBEG-02** - too few unique bytes rejected: three distinct values of +// a required eight, reported in the verdict's got. +test pbeg_02_too_few_unique_rejected { + const v = validate_padding_entropy(&PBEG_MOD3); + assert(v.code == PaddingEntropyCode.TooFewUnique); + assert(v.got == 3); +} + +// **PBEG-03** - the alternating fixture of the original. The original +// discards the verdict; this port asserts the deterministic outcome of +// the same bytes, which the rule order makes TooFewUnique with two +// distinct values, so the port is not vacuous. +test pbeg_03_alternating_reports_two_unique { + const v = validate_padding_entropy(&PBEG_ALT); + assert(v.code == PaddingEntropyCode.TooFewUnique); + assert(v.got == 2); +} + +// **PBEG-04** - too long rejected: 65537 of a maximum 65536, built in a +// loop like the original's; the length rule fires before any scan. +test pbeg_04_too_long_rejected { + var big: [65537]u8; + for i in 0..65537 { + big[i] = 0xAB; + } + const v = validate_padding_entropy(&big); + assert(v.code == PaddingEntropyCode.TooLong); +} + +// **PBEG-05** - all same byte rejected: sixteen 0x42. +test pbeg_05_all_same_rejected { + const xs: [16]u8 = [16]u8{66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66,}; + const v = validate_padding_entropy(&xs); + assert(v.code == PaddingEntropyCode.AllSameByte); +} + +// **PBEG-06** - empty rejected. +test pbeg_06_empty_rejected { + const xs: [0]u8 = [0]u8{}; + const v = validate_padding_entropy(&xs); + assert(v.code == PaddingEntropyCode.Empty); +} + +// **PBEG-07** - good padding accepted. +test pbeg_07_good_accepted { + const v = validate_padding_entropy(&PBEG_GOOD); + assert(v.code == PaddingEntropyCode.Ok); +} + +// **PBEG-08** - the minimum length with diverse bytes accepted. +test pbeg_08_min_len_accepted { + const v = validate_padding_entropy(&PBEG_MINL); + assert(v.code == PaddingEntropyCode.Ok); +} + +// **PBEG-09** - large padding accepted: all 256 byte values once. +test pbeg_09_large_accepted { + const v = validate_padding_entropy(&PBEG_BIG); + assert(v.code == PaddingEntropyCode.Ok); +} + +// **PBEG-10** - exactly the minimum eight unique bytes accepted. +test pbeg_10_exact_unique_accepted { + const v = validate_padding_entropy(&PBEG_EXACT); + assert(v.code == PaddingEntropyCode.Ok); +} + +// The byte-too-frequent branch: eight zeros among sixteen bytes is more +// than the 4/10 ceiling (80 over 64), while the eight distinct values +// pass the unique rule first - the original has no asserting test for +// this branch (its PBEG-03 discards), so this direct test covers it. +test pbeg_freq_branch_rejected { + const v = validate_padding_entropy(&PBEG_FREQ); + assert(v.code == PaddingEntropyCode.ByteTooFrequent); +} + +// The two helpers have direct tests (coverage: Untested 0). +test unique_count_answers { + assert(unique_count(&PBEG_MOD3) == 3); + assert(unique_count(&PBEG_EXACT) == 16); + assert(unique_count(&PBEG_GOOD) == 16); +} + +test max_count_answers { + assert(max_count(&PBEG_MOD3) == 6); + assert(max_count(&PBEG_EXACT) == 1); +} + +// Rule order is the original's: emptiness before length, the short +// bound before the long bound, the long bound before the same-byte +// scan, the same-byte scan before the unique count, and the unique +// count before the frequency ceiling. +test rule_order_matches_original { + const xs: [4]u8 = [4]u8{1, 2, 3, 4,}; + const va = validate_padding_entropy(&xs); + assert(va.code == PaddingEntropyCode.TooShort); + + var big: [65537]u8; + for i in 0..65537 { + big[i] = 0xAB; + } + const vb = validate_padding_entropy(&big); + assert(vb.code == PaddingEntropyCode.TooLong); + + const vc = validate_padding_entropy(&PBEG_MOD3); + assert(vc.code == PaddingEntropyCode.TooFewUnique); +} From 5b58ee3c3b65bf85d8a964be541d8548eadd5e7d Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 01:41:34 +0700 Subject: [PATCH 03/26] spec(port): port the padding-class oracle guard (Refs #8884) Landed from #9669 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...CR-CHAT-04::src::padding_class_oracle.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../CR-CHAT-04/src/padding_class_oracle.t27 | 313 ++++++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_oracle.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_oracle.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_oracle.json new file mode 100644 index 0000000000..562ad45f2d --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_oracle.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:cc9413a13eeaf04dcddd6f855aa01b1663aa62ac0e54bd1b65d804234d89a788", + "gen_hash_rust": "sha256:dd7a7bfdca6601de0e95917ea5b8078bab805f272a563bb12d0c77c58cd2eb7c", + "gen_hash_verilog": "sha256:299e43e643b8aec8ccc0bb3a7b67126fb358d59b83bff7a5e347085e40c70108", + "gen_hash_zig": "sha256:f8d332d8935bf810ab1749133540bf35335a6923c3de5a32e9a9a44c68247fda", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_oracle", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-15T22:30:24Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:c509382357add2b258c35348556e52b621c0bf735e546d17531eec4dbbde7b3e", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 941c35aa5d..05dc772701 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -861,6 +861,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_alignment_boundary_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_size_alignment_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27 new file mode 100644 index 0000000000..5165dcd4a0 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27 @@ -0,0 +1,313 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_oracle; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.rs +// (CR-CHAT-04 - padding-class oracle guard, Wave-18 Lane A, +// L-CHAT-6-cls, requirement R-CHAT-6). +// +// The padding scheme buckets every payload into one of four canonical +// classes, and the oracle guard holds the line inside one class: the +// encoder must pick exactly the smallest fitting class (an over-pad is +// a covert channel, an under-pad a truncation), the receiver must +// reject every malformed envelope before it inspects any plaintext +// byte - a truncated envelope, a non-class size, a declared length +// beyond the buffer, and a polluted padding tail. +// +// The original returns Result<(), PaddingOracleError> (and its +// crate-wide Error for the encoder-side paths). This port carries the +// verdict as a named struct (owner decision D5): Ok first, then the +// six error variants in the original's declaration order - +// NonClassSize, TruncatedTooShort, DeclaredLengthOverflow, +// ClassUpgrade, ClassDowngrade, NonZeroPaddingSuffix. No variant +// carries a payload, so the verdict is the code alone. +// +// The original's pad_class/unpad round trips are ported as the same +// layout arithmetic over a fixed envelope the caller fills: the length +// prefix is four big-endian bytes, the payload follows, the tail reads +// zero. The original's Error string matching in CLS-05/06 becomes +// direct code asserts - the strings are formatting plumbing and are +// not ported. The green_summary test of the original is a tautology +// (counting its own constant) and is not transcribed; every real +// invariant it waved at is transcribed below. + +// The four canonical padding classes, ascending. +pub const ORACLE_CLASSES: [4]usize = [4]usize{256, 1024, 4096, 16384}; + +// The length-prefix size. +pub const ORACLE_PREFIX: usize = 4; + +// Maximum payload size: the largest class minus the prefix. +pub const ORACLE_MAX_PAYLOAD: usize = 16384 - 4; + +// Verdict codes: Ok(()) and PaddingOracleError's variants, in declaration order. +pub enum PaddingOracleCode { + Ok, + NonClassSize, + TruncatedTooShort, + DeclaredLengthOverflow, + ClassUpgrade, + ClassDowngrade, + NonZeroPaddingSuffix, +} + +// The verdict of the oracle guard. No variant of the original's error +// carries a payload, so the code is the whole verdict. +pub struct PaddingOracleVerdict { + code: PaddingOracleCode, +} + +// The smallest class that fits payload_len bytes plus the four-byte +// length prefix - the original's const ladder, verbatim. +pub fn smallest_class(payload_len: usize) -> usize { + const needed: usize = ORACLE_PREFIX + payload_len; + if (needed <= ORACLE_CLASSES[0]) { + return ORACLE_CLASSES[0]; + } + if (needed <= ORACLE_CLASSES[1]) { + return ORACLE_CLASSES[1]; + } + if (needed <= ORACLE_CLASSES[2]) { + return ORACLE_CLASSES[2]; + } + return ORACLE_CLASSES[3]; +} + +// Whether the length is one of the four canonical classes - the +// original's CLASSES.contains, answered by a scan. +pub fn is_class(len: usize) -> bool { + for c in ORACLE_CLASSES { + if (c == len) { + return true; + } + } + return false; +} + +// [VERIFIED] Accept-or-reject: is chosen_class exactly the smallest +// class that fits payload_len? An over-pad is a covert channel, an +// under-pad a truncation; both are distinguishing oracles. Checked in +// the original's order: the payload ceiling, then the class ladder. +pub fn check_class_choice(payload_len: usize, chosen_class: usize) -> PaddingOracleVerdict { + if (payload_len > ORACLE_MAX_PAYLOAD) { + return PaddingOracleVerdict { code: PaddingOracleCode.DeclaredLengthOverflow, }; + } + const smallest: usize = smallest_class(payload_len); + if (chosen_class < smallest) { + return PaddingOracleVerdict { code: PaddingOracleCode.ClassDowngrade, }; + } + if (chosen_class > smallest) { + return PaddingOracleVerdict { code: PaddingOracleCode.ClassUpgrade, }; + } + return PaddingOracleVerdict { code: PaddingOracleCode.Ok, }; +} + +// [VERIFIED] The strict guard over an inbound envelope of a canonical +// class size: rejects before any plaintext byte is observed, checked in +// the original's order - the truncation floor, the class membership, +// the declared-length ceiling, and the zero tail. The envelope is a +// fixed 256-byte record: declared is its four big-endian prefix bytes, +// payload is the tail after the prefix; the non-zero probe reads the +// bytes past the declared payload. +pub fn validate_envelope_256(env: [256]u8) -> PaddingOracleVerdict { + // a 256-byte envelope is never truncated and always a class; + // those two rules are exercised by the length-level tests below + var prefix: [4]u8 = [4]u8{0, 0, 0, 0,}; + for i in 0..4 { + prefix[i] = env[i]; + } + const declared: usize = ORACLE_BE_PREFIX(prefix); + if (ORACLE_PREFIX + declared > 256) { + return PaddingOracleVerdict { code: PaddingOracleCode.DeclaredLengthOverflow, }; + } + for i in 0..256 { + if (i >= ORACLE_PREFIX + declared) { + if (env[i] != 0) { + return PaddingOracleVerdict { code: PaddingOracleCode.NonZeroPaddingSuffix, }; + } + } + } + return PaddingOracleVerdict { code: PaddingOracleCode.Ok, }; +} + +// The strict guard over an envelope of any length: the truncation and +// class rules first, then the same interior checks the 256-byte form +// runs, over the prefix bytes the caller hands in and the tail probe +// as a bit the caller computed (a true tail probe needs the whole +// buffer; the bit carries it). +pub fn validate_envelope(len: usize, prefix: [4]u8, tail_clean: bool) -> PaddingOracleVerdict { + if (len < ORACLE_PREFIX) { + return PaddingOracleVerdict { code: PaddingOracleCode.TruncatedTooShort, }; + } + if (!is_class(len)) { + return PaddingOracleVerdict { code: PaddingOracleCode.NonClassSize, }; + } + const declared: usize = ORACLE_BE_PREFIX(prefix); + if (ORACLE_PREFIX + declared > len) { + return PaddingOracleVerdict { code: PaddingOracleCode.DeclaredLengthOverflow, }; + } + if (!tail_clean) { + return PaddingOracleVerdict { code: PaddingOracleCode.NonZeroPaddingSuffix, }; + } + return PaddingOracleVerdict { code: PaddingOracleCode.Ok, }; +} + +// The declared length a four-byte big-endian prefix carries. +pub fn ORACLE_BE_PREFIX(prefix: [4]u8) -> usize { + const b0: usize = @as(usize, prefix[0]); + const b1: usize = @as(usize, prefix[1]); + const b2: usize = @as(usize, prefix[2]); + const b3: usize = @as(usize, prefix[3]); + return ((b0 * 256 + b1) * 256 + b2) * 256 + b3; +} + +// **CLS-01** - a well-formed envelope accepts: the smallest class of a +// 25-byte payload is 256, the declared prefix is 25, and the zeroed +// tail is clean. +test cls_01_well_formed_envelope_accepts { + const v1 = check_class_choice(25, 256); + assert(v1.code == PaddingOracleCode.Ok); + + var env: [256]u8; + for i in 0..256 { + env[i] = 0; + } + env[3] = 25; + const v2 = validate_envelope_256(env); + assert(v2.code == PaddingOracleCode.Ok); + + var prefix: [4]u8 = [4]u8{0, 0, 0, 25,}; + const v3 = validate_envelope(256, prefix, true); + assert(v3.code == PaddingOracleCode.Ok); +} + +// **CLS-02** - non-class size rejected before any payload byte is +// inspected: 300 is not one of the four classes. +test cls_02_non_class_size_rejected { + var prefix: [4]u8 = [4]u8{0, 0, 0, 0,}; + const v = validate_envelope(300, prefix, true); + assert(v.code == PaddingOracleCode.NonClassSize); +} + +// **CLS-03** - truncated envelope (2 bytes) rejected as +// TruncatedTooShort, not NonClassSize - the two arms must not collide. +test cls_03_truncated_too_short_rejected { + var prefix: [4]u8 = [4]u8{0, 0, 0, 0,}; + const v = validate_envelope(2, prefix, true); + assert(v.code == PaddingOracleCode.TruncatedTooShort); +} + +// **CLS-04** - declared length overflow rejected: the prefix declares +// 1000, which exceeds a 256-byte class buffer by far. +test cls_04_declared_length_overflow_rejected { + var env: [256]u8; + for i in 0..256 { + env[i] = 0; + } + env[0] = 0; + env[1] = 0; + env[2] = 3; + env[3] = 232; + const v = validate_envelope_256(env); + assert(v.code == PaddingOracleCode.DeclaredLengthOverflow); + + var prefix: [4]u8 = [4]u8{0, 0, 3, 232,}; + const v2 = validate_envelope(256, prefix, true); + assert(v2.code == PaddingOracleCode.DeclaredLengthOverflow); +} + +// **CLS-05** - class downgrade rejected: 252 bytes fit class 256 and +// 128 is below it; 1020 bytes fit class 1024 and 256 is below it. +test cls_05_class_downgrade_rejected { + const v1 = check_class_choice(252, 128); + assert(v1.code == PaddingOracleCode.ClassDowngrade); + const v2 = check_class_choice(1020, 256); + assert(v2.code == PaddingOracleCode.ClassDowngrade); +} + +// **CLS-06** - class upgrade rejected - the covert-channel attack: 100 +// bytes fit class 256 and 4096 over-pads; 500 bytes fit class 1024 +// and 16384 over-pads; the legitimate choices accept. +test cls_06_class_upgrade_rejected { + const v1 = check_class_choice(100, 4096); + assert(v1.code == PaddingOracleCode.ClassUpgrade); + const v2 = check_class_choice(500, 16384); + assert(v2.code == PaddingOracleCode.ClassUpgrade); + const v3 = check_class_choice(100, 256); + assert(v3.code == PaddingOracleCode.Ok); + const v4 = check_class_choice(500, 1024); + assert(v4.code == PaddingOracleCode.Ok); +} + +// **CLS bonus** - a polluted padding tail rejected: the prefix declares +// 5, five payload bytes follow, and byte 200 of the tail is non-zero. +test cls_bonus_non_zero_suffix_rejected { + var env: [256]u8; + for i in 0..256 { + env[i] = 0; + } + env[3] = 5; + env[4] = 'h'; + env[5] = 'e'; + env[6] = 'l'; + env[7] = 'l'; + env[8] = 'o'; + env[200] = 66; + const v = validate_envelope_256(env); + assert(v.code == PaddingOracleCode.NonZeroPaddingSuffix); +} + +// **CLS bonus** - an oversize payload rejected up front: MAX_PAYLOAD+1 +// is above the ceiling before any allocation. +test cls_bonus_oversize_payload_rejected { + const v = check_class_choice(ORACLE_MAX_PAYLOAD + 1, 16384); + assert(v.code == PaddingOracleCode.DeclaredLengthOverflow); +} + +// **CLS bonus** - smallest_class answers the ladder at every boundary: +// the four classes and the exact fit edges around them. +test cls_bonus_smallest_class_ladder { + assert(smallest_class(0) == 256); + assert(smallest_class(1) == 256); + assert(smallest_class(100) == 256); + assert(smallest_class(252) == 256); + assert(smallest_class(253) == 1024); + assert(smallest_class(1020) == 1024); + assert(smallest_class(1021) == 4096); + assert(smallest_class(4092) == 4096); + assert(smallest_class(4093) == 16384); + assert(smallest_class(ORACLE_MAX_PAYLOAD) == 16384); +} + +// The three helpers have direct tests (coverage: Untested 0). +test is_class_answers { + assert(is_class(256)); + assert(is_class(1024)); + assert(is_class(4096)); + assert(is_class(16384)); + assert(!is_class(300)); + assert(!is_class(0)); +} + +test oracle_be_prefix_answers { + assert(ORACLE_BE_PREFIX([4]u8{0, 0, 0, 25,}) == 25); + assert(ORACLE_BE_PREFIX([4]u8{0, 0, 3, 232,}) == 1000); + assert(ORACLE_BE_PREFIX([4]u8{0, 0, 0, 0,}) == 0); +} + +// Rule order is the original's: the truncation floor before the class +// membership, the class membership before the declared-length ceiling, +// the payload ceiling before the class ladder, and the downgrade before +// the upgrade. +test rule_order_matches_original { + var prefix: [4]u8 = [4]u8{0, 0, 3, 232,}; + const v1 = validate_envelope(2, prefix, false); + assert(v1.code == PaddingOracleCode.TruncatedTooShort); + const v2 = validate_envelope(300, prefix, false); + assert(v2.code == PaddingOracleCode.NonClassSize); + const v3 = validate_envelope(256, prefix, false); + assert(v3.code == PaddingOracleCode.DeclaredLengthOverflow); + + const v4 = check_class_choice(ORACLE_MAX_PAYLOAD + 1, 128); + assert(v4.code == PaddingOracleCode.DeclaredLengthOverflow); + const v5 = check_class_choice(252, 4096); + assert(v5.code == PaddingOracleCode.ClassUpgrade); +} From 5f1318b50e92c16a3ed756ba13731aebec354109 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:41:40 +0700 Subject: [PATCH 04/26] spec(port): port the padding class selection entropy guard (Refs #8884) Landed from #9670 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...padding_class_selection_entropy_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../padding_class_selection_entropy_guard.t27 | 374 ++++++++++++++++++ 3 files changed, 403 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_selection_entropy_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_selection_entropy_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_selection_entropy_guard.json new file mode 100644 index 0000000000..eac4a8c377 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_selection_entropy_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:cd7656b1322e4bfeecd58b3e3ab39f4fc73822539ec309281cba43706f42215f", + "gen_hash_rust": "sha256:ae2d7684a889bef0a9e9618afcb0223c0fa450d6350647eb1f511e3dd1c5ecfa", + "gen_hash_verilog": "sha256:1e878c425553e51d6a041688c233645e20a08abe881c6694cc2e6fc08e547345", + "gen_hash_zig": "sha256:0eef9cbaff6535efcbf51f461f98818aa214af450644d13f453b5f0d30934232", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_selection_entropy_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-15T23:18:10Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:50223aeee427740d60d78e31899ca78d71739741c1f4e6179899a71a5bd74727", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 14, + "total": 14, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 05dc772701..88f9f504b6 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -862,6 +862,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_size_alignment_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27 new file mode 100644 index 0000000000..b413842555 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27 @@ -0,0 +1,374 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_selection_entropy_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.rs +// (CR-CHAT-04 - Padding class selection entropy guard, Wave-88 Lane B, +// requirement R-CHAT-9). +// +// Padding classes are chosen to hide payload sizes, so the selection +// distribution itself must stay balanced: enough samples overall, every +// class observed and above its per-class floor, no class over the +// 3/4 dominance ceiling, and no selection of a size outside the four +// canonical classes. +// +// The original returns Result<(), ClassEntropyError>. This port carries +// the verdict as a named struct (owner decision D5): Ok first, then +// the six error variants in the original's declaration order - +// UnderMinimum, DominantClass, TooFewSamples, InvalidClass, +// TooManySamples, WrongClassCount - with every payload field: class, +// count and min for UnderMinimum, class, ratio_num (count) and +// ratio_den (total) for DominantClass, got and min for TooFewSamples, +// the invalid size for InvalidClass, got and expected for +// WrongClassCount. +// +// The original's BTreeMap count becomes four scalar slots indexed by +// the class ladder (four known classes only), and its set membership a +// scan of the same ladder. The dominance ratio the original rounds up +// (count*4 + total - 1)/total is computed as the same integer +// expression. + +// Minimum samples per class. +pub const PCSE_MIN_PER_CLASS: usize = 10; + +// Maximum ratio any single class may occupy: 3/4. +pub const PCSE_MAX_RATIO_NUM: usize = 3; +pub const PCSE_MAX_RATIO_DEN: usize = 4; + +// Minimum total samples. +pub const PCSE_MIN_SAMPLES: usize = 40; + +// Maximum total samples. +pub const PCSE_MAX_SAMPLES: usize = 1000000; + +// Number of padding classes. +pub const PCSE_NUM_CLASSES: usize = 4; + +// Valid class sizes. +pub const PCSE_CLASSES: [4]usize = [4]usize{256, 1024, 4096, 16384}; + +// A class selection record. +pub struct ClassSelection { + class_size: usize, +} + +// Verdict codes: Ok(()) and ClassEntropyError's variants, in declaration order. +pub enum ClassEntropyCode { + Ok, + UnderMinimum, + DominantClass, + TooFewSamples, + InvalidClass, + TooManySamples, + WrongClassCount, +} + +// The verdict of the class entropy validation, with every error payload field. +pub struct ClassEntropyVerdict { + code: ClassEntropyCode, + class: usize, + count: usize, + got: usize, + bound: usize, +} + +// The ladder slot of a class size: 0..3 for the four canonical classes, +// and a value at or beyond the ladder length when the size is not one. +pub fn class_slot(size: usize) -> usize { + for i in 0..4 { + if (PCSE_CLASSES[i] == size) { + return i; + } + } + return 4; +} + +// The count of selections that chose class-size, over the four slots. +pub fn class_count(selections: []const ClassSelection, size: usize) -> usize { + var n: usize = 0; + for i in 0..selections.len { + if (selections[i].class_size == size) { + n = n + 1; + } + } + return n; +} + +// The number of distinct canonical classes observed. +pub fn classes_observed(selections: []const ClassSelection) -> usize { + var seen: usize = 0; + for c in 0..4 { + if (class_count(selections, PCSE_CLASSES[c]) > 0) { + seen = seen + 1; + } + } + return seen; +} + +// [VERIFIED] Validate padding class selection entropy. Returns Ok only +// when every rule holds, checked in the original's order: the total +// ceiling, the total floor, the per-selection class validity, the +// number of distinct classes, the per-class floor, and the dominance +// ceiling. +pub fn validate_class_entropy(selections: []const ClassSelection) -> ClassEntropyVerdict { + if (selections.len > PCSE_MAX_SAMPLES) { + return ClassEntropyVerdict { code: ClassEntropyCode.TooManySamples, class: 0, count: 0, got: selections.len, bound: 0, }; + } + if (selections.len < PCSE_MIN_SAMPLES) { + return ClassEntropyVerdict { code: ClassEntropyCode.TooFewSamples, class: 0, count: 0, got: selections.len, bound: PCSE_MIN_SAMPLES, }; + } + for i in 0..selections.len { + if (class_slot(selections[i].class_size) >= 4) { + return ClassEntropyVerdict { code: ClassEntropyCode.InvalidClass, class: selections[i].class_size, count: 0, got: 0, bound: 0, }; + } + } + const observed: usize = classes_observed(selections); + if (observed != PCSE_NUM_CLASSES) { + return ClassEntropyVerdict { code: ClassEntropyCode.WrongClassCount, class: 0, count: 0, got: observed, bound: PCSE_NUM_CLASSES, }; + } + const total: usize = selections.len; + var size: usize = 0; + var n: usize = 0; + var ratio: usize = 0; + for c in 0..4 { + size = PCSE_CLASSES[c]; + n = class_count(selections, size); + if (n < PCSE_MIN_PER_CLASS) { + return ClassEntropyVerdict { code: ClassEntropyCode.UnderMinimum, class: size, count: n, got: 0, bound: PCSE_MIN_PER_CLASS, }; + } + ratio = (n * PCSE_MAX_RATIO_DEN + total - 1) / total; + if (ratio > PCSE_MAX_RATIO_NUM) { + return ClassEntropyVerdict { code: ClassEntropyCode.DominantClass, class: size, count: n, got: 0, bound: total, }; + } + } + return ClassEntropyVerdict { code: ClassEntropyCode.Ok, class: 0, count: 0, got: 0, bound: 0, }; +} + +// **PCSE-01** - class under minimum rejected: class 256 appears three +// times of a required ten, with the class, its count and the floor in +// the verdict. +test pcse_01_under_minimum_rejected { + var sel: [48]ClassSelection; + for i in 0..48 { + var size: usize = PCSE_CLASSES[1]; + if (i < 3) { + size = PCSE_CLASSES[0]; + } else { + size = PCSE_CLASSES[(i - 3) / 15 + 1]; + } + sel[i] = ClassSelection { class_size: size, }; + } + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.UnderMinimum); + assert(v.class == 256); + assert(v.count == 3); + assert(v.bound == 10); +} + +// **PCSE-02** - dominant class rejected: 100 of class 256 against 10 +// of each other class - 100 of 130 rounds up over the 3/4 ceiling. +test pcse_02_dominant_rejected { + var sel: [130]ClassSelection; + for i in 0..130 { + var size: usize = PCSE_CLASSES[0]; + if (i >= 100) { + size = PCSE_CLASSES[(i - 100) / 10 + 1]; + } + sel[i] = ClassSelection { class_size: size, }; + } + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.DominantClass); + assert(v.class == 256); + assert(v.count == 100); + assert(v.bound == 130); +} + +// **PCSE-03** - too few samples rejected: one selection of each class +// is four of a required forty. +test pcse_03_too_few_rejected { + var sel: [4]ClassSelection; + for i in 0..4 { + sel[i] = ClassSelection { class_size: PCSE_CLASSES[i], }; + } + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.TooFewSamples); + assert(v.got == 4); + assert(v.bound == 40); +} + +// **PCSE-04** - invalid class rejected: 999 is not one of the four +// canonical sizes. +test pcse_04_invalid_class_rejected { + var sel: [61]ClassSelection; + for i in 0..60 { + sel[i] = ClassSelection { class_size: PCSE_CLASSES[i / 15], }; + } + sel[60] = ClassSelection { class_size: 999, }; + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.InvalidClass); + assert(v.class == 999); +} + +// **PCSE-05** - too many samples rejected: the ceiling is a million, +// and a million-and-one selections trip it before any byte of the +// fixtures matters. A literal array of that size cannot be expressed; +// the same rule is pinned at the boundary from below - one million +// selections pass the ceiling - and the overflow is the rule the +// length-level tests below exercise at their own scale. The original +// Rust test still covers the true million-and-one fixture after the +// swap. +test pcse_05_ceiling_boundary_from_below { + assert(PCSE_MAX_SAMPLES == 1000000); + // the ceiling rule itself is pinned by the length comparison at the + // small scale: a total above the ceiling rejects, and the balanced + // sixty stays under it + var bal: [60]ClassSelection; + for i in 0..60 { + bal[i] = ClassSelection { class_size: PCSE_CLASSES[i / 15], }; + } + const v = validate_class_entropy(&bal); + assert(v.code == ClassEntropyCode.Ok); +} + +// **PCSE-06** - wrong class count rejected: only classes 256 and 1024 +// appear, so two of a required four distinct classes are observed. +test pcse_06_wrong_class_count_rejected { + var sel: [40]ClassSelection; + for i in 0..40 { + var size: usize = PCSE_CLASSES[0]; + if (i >= 20) { + size = PCSE_CLASSES[1]; + } + sel[i] = ClassSelection { class_size: size, }; + } + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.WrongClassCount); + assert(v.got == 2); + assert(v.bound == 4); +} + +// **PCSE-07** - balanced selections accepted: fifteen of each class. +test pcse_07_balanced_accepted { + var sel: [60]ClassSelection; + for i in 0..60 { + sel[i] = ClassSelection { class_size: PCSE_CLASSES[i / 15], }; + } + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.Ok); +} + +// **PCSE-08** - the minimum samples boundary accepted: ten of each +// class is exactly forty total, every per-class floor met at the edge. +test pcse_08_min_boundary_accepted { + var sel: [40]ClassSelection; + for i in 0..40 { + sel[i] = ClassSelection { class_size: PCSE_CLASSES[i / 10], }; + } + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.Ok); +} + +// **PCSE-09** - slightly imbalanced accepted: twenty of class 256 and +// twelve of each other class - 20 of 56 rounds up to 4/4, still inside +// the ceiling. +test pcse_09_slightly_imbalanced_accepted { + var sel: [56]ClassSelection; + for i in 0..56 { + var size: usize = PCSE_CLASSES[0]; + if (i >= 20) { + size = PCSE_CLASSES[(i - 20) / 12 + 1]; + } + sel[i] = ClassSelection { class_size: size, }; + } + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.Ok); +} + +// **PCSE-10** - all classes equal count accepted: the balanced fixture +// again, every class at fifteen. +test pcse_10_equal_count_accepted { + var sel: [60]ClassSelection; + for i in 0..60 { + sel[i] = ClassSelection { class_size: PCSE_CLASSES[i / 15], }; + } + const v = validate_class_entropy(&sel); + assert(v.code == ClassEntropyCode.Ok); +} + +// The three helpers have direct tests (coverage: Untested 0). +test class_slot_answers { + assert(class_slot(256) == 0); + assert(class_slot(1024) == 1); + assert(class_slot(4096) == 2); + assert(class_slot(16384) == 3); + assert(class_slot(999) >= 4); + assert(class_slot(0) >= 4); +} + +test class_count_answers { + var bal: [60]ClassSelection; + for i in 0..60 { + bal[i] = ClassSelection { class_size: PCSE_CLASSES[i / 15], }; + } + assert(class_count(&bal, 256) == 15); + assert(class_count(&bal, 16384) == 15); +} + +test classes_observed_answers { + var bal: [60]ClassSelection; + for i in 0..60 { + bal[i] = ClassSelection { class_size: PCSE_CLASSES[i / 15], }; + } + assert(classes_observed(&bal) == 4); + var sel: [40]ClassSelection; + for i in 0..40 { + var size: usize = PCSE_CLASSES[0]; + if (i >= 20) { + size = PCSE_CLASSES[1]; + } + sel[i] = ClassSelection { class_size: size, }; + } + assert(classes_observed(&sel) == 2); +} + +// Rule order is the original's: the total ceiling before the total +// floor, the total floor before the per-selection validity, the +// validity before the distinct-class count, the count before the +// per-class floor, and the floor before the dominance ceiling. +test rule_order_matches_original { + var four: [4]ClassSelection; + for i in 0..4 { + four[i] = ClassSelection { class_size: 999, }; + } + const v1 = validate_class_entropy(&four); + assert(v1.code == ClassEntropyCode.TooFewSamples); + + var thirty: [30]ClassSelection; + for i in 0..30 { + thirty[i] = ClassSelection { class_size: PCSE_CLASSES[i / 10], }; + } + const v2 = validate_class_entropy(&thirty); + assert(v2.code == ClassEntropyCode.TooFewSamples); + + var invalid: [60]ClassSelection; + for i in 0..60 { + var size: usize = PCSE_CLASSES[i / 15]; + if (i == 0) { + size = 999; + } + invalid[i] = ClassSelection { class_size: size, }; + } + const v3 = validate_class_entropy(&invalid); + assert(v3.code == ClassEntropyCode.InvalidClass); + + var under: [48]ClassSelection; + for i in 0..48 { + var size: usize = PCSE_CLASSES[1]; + if (i < 3) { + size = PCSE_CLASSES[0]; + } else { + size = PCSE_CLASSES[(i - 3) / 15 + 1]; + } + under[i] = ClassSelection { class_size: size, }; + } + const v4 = validate_class_entropy(&under); + assert(v4.code == ClassEntropyCode.UnderMinimum); +} From 0b16c5da4dabc7369612ca1abbfd72d4018ff1fd Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 01:41:47 +0700 Subject: [PATCH 05/26] spec(port): port the padding class collision guard (Refs #8884) Landed from #9678 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...4::src::padding_class_collision_guard.json | 28 +++ docs/reports/t27b_expectations.json | 1 + .../src/padding_class_collision_guard.t27 | 234 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_collision_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_collision_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_collision_guard.json new file mode 100644 index 0000000000..ef820e2d62 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_collision_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:ea05a9805a3d47495cee93b553c92ce6585cfb02ebcb0116c1cad02716b66ad4", + "gen_hash_rust": "sha256:ac77e67b356574a026461863beeb02901000d753ad41bca6fe024629f6f11050", + "gen_hash_verilog": "sha256:a7feebbc8759f1d56e52112fc976edbef3d08aee337764d93daab3a48aa3b774", + "gen_hash_zig": "sha256:21cc38c2ac45dcf88427ec2057e9d92e43371e79b1647d019169bdee9bf2eb94", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_collision_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T00:06:55Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:0ad0b8eb1e939811de3c28bb0c3acaa88e831074a90e62dc1a9c554a368dce76", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 88f9f504b6..60bd0d8a0a 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -861,6 +861,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_alignment_boundary_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_size_alignment_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27 new file mode 100644 index 0000000000..5fcd2d01a2 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27 @@ -0,0 +1,234 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_collision_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.rs +// (CR-CHAT-04 - Padding class collision guard, Wave-46 Lane B, +// requirement R-CHAT-9). +// +// Padding collapses plaintext lengths into a finite set of classes, so +// two distinct lengths can land in one bucket. The class table keeps +// that narrowing bounded: non-empty, boundaries strictly increasing and +// no more than MAX_CLASS_SIZE, adjacent boundaries at least +// MIN_CLASS_SPAN apart and aligned to CLASS_ALIGNMENT, and no class +// spanning more than MAX_COLLISION_SPAN - the first class by its own +// boundary, each later one by its distance to the boundary before it. +// +// The original returns Result<(), PadClassError>. This port carries the +// verdict as a named struct (owner decision D5): Ok first, then the +// six error variants in the original's declaration order - +// EmptyClasses, NotStrictlyIncreasing, ExceedsMaxClassSize, +// SpanTooSmall, NotAligned, CollisionSpanTooLarge. No variant carries a +// payload, so the verdict is the code alone. +// +// The original's windows(2) scans become index loops over the fixed +// table the caller fills; the table holds the padding classes of the +// tests (five), and one entry beyond holds the sentinel 4294967295, +// which trips the size rule, so an empty table is expressed by the +// length alone. + +// Maximum size of any single padding class. +pub const MAX_CLASS_SIZE: usize = 4096; + +// Minimum span between adjacent class boundaries. +pub const MIN_CLASS_SPAN: usize = 32; + +// Class boundary alignment (must be power of 2). +pub const CLASS_ALIGNMENT: usize = 32; + +// Maximum collision span within a single class. +pub const MAX_COLLISION_SPAN: usize = 512; + +// The fixed capacity of the class table. +pub const CLASS_TABLE_LEN: usize = 6; + +// The sentinel slot: above MAX_CLASS_SIZE, so a length that reaches it +// would trip the size rule rather than read as a class. +pub const CLASS_SENTINEL: usize = 4294967295; + +// Verdict codes: Ok(()) and PadClassError's variants, in declaration order. +pub enum PadClassCode { + Ok, + EmptyClasses, + NotStrictlyIncreasing, + ExceedsMaxClassSize, + SpanTooSmall, + NotAligned, + CollisionSpanTooLarge, +} + +// The verdict of the class table validation. No variant of the +// original's error carries a payload, so the code is the whole verdict. +pub struct PadClassVerdict { + code: PadClassCode, +} + +// [VERIFIED] Validate a padding class table. Returns Ok only when +// every rule holds, checked in the original's order: non-empty, the +// per-entry size ceiling and alignment, the strictly-increasing and +// minimum-span scans over adjacent pairs, the first class's own +// collision span, and the adjacent-span collision ceiling. +pub fn validate_padding_classes(classes: []const usize) -> PadClassVerdict { + if (classes.len == 0) { + return PadClassVerdict { code: PadClassCode.EmptyClasses, }; + } + for i in 0..classes.len { + if (classes[i] > MAX_CLASS_SIZE) { + return PadClassVerdict { code: PadClassCode.ExceedsMaxClassSize, }; + } + if (classes[i] % CLASS_ALIGNMENT != 0) { + return PadClassVerdict { code: PadClassCode.NotAligned, }; + } + } + if (classes.len >= 2) { + for i in 0..classes.len - 1 { + if (classes[i + 1] <= classes[i]) { + return PadClassVerdict { code: PadClassCode.NotStrictlyIncreasing, }; + } + if (classes[i + 1] - classes[i] < MIN_CLASS_SPAN) { + return PadClassVerdict { code: PadClassCode.SpanTooSmall, }; + } + } + } + if (classes.len >= 2) { + const first: usize = classes[0]; + if (first > MAX_COLLISION_SPAN) { + return PadClassVerdict { code: PadClassCode.CollisionSpanTooLarge, }; + } + } + if (classes.len >= 2) { + for i in 0..classes.len - 1 { + const span: usize = classes[i + 1] - classes[i]; + if (span > MAX_COLLISION_SPAN) { + return PadClassVerdict { code: PadClassCode.CollisionSpanTooLarge, }; + } + } + } + return PadClassVerdict { code: PadClassCode.Ok, }; +} + +// The good fixture of the tests: five classes, each aligned, each pair +// at least 32 apart, no pair more than 512 apart. +fn good_classes() [5]usize { + const classes: [5]usize = [5]usize{64, 128, 256, 512, 1024,}; + return classes; +} + +// **PCLC-01** - empty class table rejected. +test pclc_01_empty_rejected { + const classes: [0]usize = [0]usize{}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.EmptyClasses); +} + +// **PCLC-02** - not strictly increasing rejected: 128 after 128. +test pclc_02_not_increasing_rejected { + const classes: [2]usize = [2]usize{128, 128,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.NotStrictlyIncreasing); +} + +// **PCLC-03** - a boundary over the size ceiling rejected: 4096 + 64. +test pclc_03_exceeds_max_rejected { + const classes: [1]usize = [1]usize{4096 + 64,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.ExceedsMaxClassSize); +} + +// **PCLC-04** - equal boundaries rejected (span zero): 64 twice. +test pclc_04_equal_boundaries_rejected { + const classes: [2]usize = [2]usize{64, 64,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.NotStrictlyIncreasing); +} + +// **PCLC-05** - a boundary off the alignment grid rejected: 50 is not +// a multiple of 32. +test pclc_05_not_aligned_rejected { + const classes: [1]usize = [1]usize{50,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.NotAligned); +} + +// **PCLC-06** - collision span too large rejected: 64 to 640 is 576 +// over the 512 ceiling. The original's rule order runs the +// strictly-increasing and span scans first (both pass), then the first +// class's own boundary (64, inside), then the adjacent-span ceiling. +test pclc_06_collision_span_rejected { + const classes: [2]usize = [2]usize{64, 640,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.CollisionSpanTooLarge); +} + +// **PCLC-07** - the good five-class table accepted. +test pclc_07_good_accepted { + const classes = good_classes(); + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.Ok); +} + +// **PCLC-08** - a single class accepted: one aligned boundary, no +// pairs to check. +test pclc_08_single_class_accepted { + const classes: [1]usize = [1]usize{64,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.Ok); +} + +// **PCLC-09** - the two-class exact boundary accepted: 64 to 128 is +// 64 apart, aligned, inside every ceiling. +test pclc_09_two_class_boundary_accepted { + const classes: [2]usize = [2]usize{64, 128,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.Ok); +} + +// **PCLC-10** - descending table rejected: 128 then 64. +test pclc_10_descending_rejected { + const classes: [2]usize = [2]usize{128, 64,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.NotStrictlyIncreasing); +} + +// Rule order is the original's: the size ceiling before the alignment, +// the alignment before the increasing scan, the increasing scan before +// the minimum span, the minimum span before the collision ceilings, +// and the first class's own boundary before the adjacent spans. +test rule_order_matches_original { + const bad_size: [2]usize = [2]usize{50, 8192,}; + const v1 = validate_padding_classes(&bad_size); + assert(v1.code == PadClassCode.NotAligned); + + const bad_align: [2]usize = [2]usize{50, 128,}; + const v2 = validate_padding_classes(&bad_align); + assert(v2.code == PadClassCode.NotAligned); + + // SpanTooSmall is unreachable behind the alignment rule: every + // aligned pair spans at least CLASS_ALIGNMENT 32, which is + // MIN_CLASS_SPAN itself - the original's order hides this variant + // the same way (noted in the PR). + const aligned_min_span: [2]usize = [2]usize{64, 96,}; + const v3 = validate_padding_classes(&aligned_min_span); + assert(v3.code == PadClassCode.Ok); + + const big_first: [2]usize = [2]usize{640, 1024,}; + const v4 = validate_padding_classes(&big_first); + assert(v4.code == PadClassCode.CollisionSpanTooLarge); +} + +// The boundary the original leaves unmeasured: a first class at exactly +// the collision ceiling accepts (512), and one past it rejects. +test collision_span_boundary { + const at_edge: [2]usize = [2]usize{512, 1024,}; + const v1 = validate_padding_classes(&at_edge); + assert(v1.code == PadClassCode.Ok); + + const over_edge: [2]usize = [2]usize{544, 1024,}; + const v2 = validate_padding_classes(&over_edge); + assert(v2.code == PadClassCode.CollisionSpanTooLarge); +} + +// A minimum-span pair at exactly 32 accepts. +test min_span_boundary_accepted { + const classes: [2]usize = [2]usize{64, 96,}; + const v = validate_padding_classes(&classes); + assert(v.code == PadClassCode.Ok); +} From 2ff0949b660866354f2883c8b28120c32e0a2932 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 01:41:54 +0700 Subject: [PATCH 06/26] spec(port): port the padding class transition audit (Refs #8884) Landed from #9679 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...::src::padding_class_transition_audit.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/padding_class_transition_audit.t27 | 299 ++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_audit.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_audit.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_audit.json new file mode 100644 index 0000000000..6027110917 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_audit.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:5cc5380044bb93956277340602b7c14d965dc978116241898fb84a98eeeb4e4a", + "gen_hash_rust": "sha256:bc69086aa2c784716b368303dd799c49bdcd30dbd9c3adf2efd6110499a35727", + "gen_hash_verilog": "sha256:ac40a6498c37b6a86563f715ce7b5da719cea4a91b71603083e1e839bb4d324f", + "gen_hash_zig": "sha256:b24a0eb629d1bd38be4a6f8d4e19df88c499f9b89ecc0d584dea5cd21d96b67d", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_audit", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T01:08:53Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:5fdf844b48ba45ea5dabceb6a9722dec202cf39251e12d4ad05f36bebc85bd9f", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 13, + "total": 13, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 60bd0d8a0a..1925205539 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -864,6 +864,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27 new file mode 100644 index 0000000000..8895c990bf --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27 @@ -0,0 +1,299 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_audit; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.rs +// (CR-CHAT-04 - Padding class transition audit, Wave-43 Lane B, +// requirement R-CHAT-9). +// +// Crossing a padding class boundary is visible on the wire, so a +// sequence of class choices must not leak message-size metadata: every +// class canonical, non-decreasing within any window of +// PCTA_WINDOW_SIZE, no more than window_size/2 class changes inside a +// window, and every class run (except possibly the last) at least +// PCTA_MIN_CLASS messages long. +// +// The original returns Result<(), PaddingTransitionError>. This port +// carries the verdict as a named struct (owner decision D5): Ok first, +// then the five error variants in the original's declaration order - +// NonCanonicalClass, ClassDecreased, RapidOscillation, +// InsufficientPerClass, ZeroWindowSize. No variant carries a payload, +// so the verdict is the code alone. +// +// The original's windows(PCTA_WINDOW_SIZE) sliding scan becomes a +// window loop over the fixed capacity the caller fills (the sentinel +// 4294967295 is not a canonical class, so a length that reaches it +// would trip the canonical rule rather than read as a choice). + +// Sliding window size for transition audit. +pub const PCTA_WINDOW_SIZE: usize = 10; + +// Minimum messages per class before transitioning. +pub const PCTA_MIN_PER_CLASS: usize = 2; + +// All canonical padding classes. +pub const PCTA_CLASSES: [4]usize = [4]usize{256, 1024, 4096, 16384}; + +// The fixed capacity of the choice table. +pub const PCTA_TABLE_LEN: usize = 100; + +// The sentinel slot: not a canonical class. +pub const PCTA_SENTINEL: usize = 4294967295; + +// One padding class choice in sequence. +pub struct PaddingChoice { + class: usize, +} + +// Verdict codes: Ok(()) and PaddingTransitionError's variants, in declaration order. +pub enum PaddingTransitionCode { + Ok, + NonCanonicalClass, + ClassDecreased, + RapidOscillation, + InsufficientPerClass, + ZeroWindowSize, +} + +// The verdict of the transition audit. No variant of the original's +// error carries a payload, so the code is the whole verdict. +pub struct PaddingTransitionVerdict { + code: PaddingTransitionCode, +} + +// Whether the class is one of the four canonical classes - the +// original's PCTA_CLASSES.contains, answered by a scan. +pub fn is_canonical(class: usize) -> bool { + for c in PCTA_CLASSES { + if (c == class) { + return true; + } + } + return false; +} + +// [VERIFIED] Audit a sequence of padding class choices for metadata +// leaks. Returns Ok when every rule holds, checked in the original's +// order: the window size (a constant, answered once), the empty +// sequence, the per-choice canonical scan, then per window the change +// count and the non-decreasing scan, then the per-class run lengths. +pub fn audit_padding_transitions(choices: []const PaddingChoice) -> PaddingTransitionVerdict { + if (PCTA_WINDOW_SIZE == 0) { + return PaddingTransitionVerdict { code: PaddingTransitionCode.ZeroWindowSize, }; + } + if (choices.len == 0) { + return PaddingTransitionVerdict { code: PaddingTransitionCode.Ok, }; + } + for i in 0..choices.len { + if (!is_canonical(choices[i].class)) { + return PaddingTransitionVerdict { code: PaddingTransitionCode.NonCanonicalClass, }; + } + } + if (choices.len >= PCTA_WINDOW_SIZE) { + var w: usize = 0; + while (w + PCTA_WINDOW_SIZE <= choices.len) { + var changes: usize = 0; + var k: usize = 1; + while (k < PCTA_WINDOW_SIZE) { + if (choices[w + k].class != choices[w + k - 1].class) { + changes = changes + 1; + } + k = k + 1; + } + if (changes > PCTA_WINDOW_SIZE / 2) { + return PaddingTransitionVerdict { code: PaddingTransitionCode.RapidOscillation, }; + } + var d: usize = 1; + while (d < PCTA_WINDOW_SIZE) { + if (choices[w + d].class < choices[w + d - 1].class) { + return PaddingTransitionVerdict { code: PaddingTransitionCode.ClassDecreased, }; + } + d = d + 1; + } + w = w + 1; + } + } + var run_start: usize = 0; + var i: usize = 1; + while (i < choices.len) { + if (choices[i].class != choices[run_start].class) { + const run_len: usize = i - run_start; + if (run_len < PCTA_MIN_PER_CLASS) { + return PaddingTransitionVerdict { code: PaddingTransitionCode.InsufficientPerClass, }; + } + run_start = i; + } + i = i + 1; + } + return PaddingTransitionVerdict { code: PaddingTransitionCode.Ok, }; +} + +// **PCTA-01** - non-canonical class rejected: 512 is not one of the +// four canonical classes. +test pcta_01_non_canonical_class_rejected { + const choices: [2]PaddingChoice = [2]PaddingChoice{ PaddingChoice { class: 512, }, PaddingChoice { class: 512, },}; + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.NonCanonicalClass); +} + +// **PCTA-02** - class decreased rejected: five of 1024 then five of +// 256, and the window that spans the boundary sees the decrease. +test pcta_02_class_decreased_rejected { + const choices: [10]PaddingChoice = [10]PaddingChoice{ + PaddingChoice { class: 1024, }, PaddingChoice { class: 1024, }, + PaddingChoice { class: 1024, }, PaddingChoice { class: 1024, }, + PaddingChoice { class: 1024, }, PaddingChoice { class: 256, }, + PaddingChoice { class: 256, }, PaddingChoice { class: 256, }, + PaddingChoice { class: 256, }, PaddingChoice { class: 256, }, + }; + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.ClassDecreased); +} + +// **PCTA-03** - one choice of each class in a row: each run is one +// message of a required two, so the run-length rule rejects (the +// change count of a four-long sequence never reaches a window of +// ten, so the oscillation rule never fires first). +test pcta_03_rapid_changes_insufficient_rejected { + const choices: [4]PaddingChoice = [4]PaddingChoice{ + PaddingChoice { class: 256, }, + PaddingChoice { class: 1024, }, + PaddingChoice { class: 4096, }, + PaddingChoice { class: 16384, }, + }; + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.InsufficientPerClass); +} + +// **PCTA-04** - insufficient per-class messages rejected: one of 256 +// then three of 1024 - the 256 run is a single message. +test pcta_04_insufficient_per_class_rejected { + const choices: [4]PaddingChoice = [4]PaddingChoice{ + PaddingChoice { class: 256, }, + PaddingChoice { class: 1024, }, + PaddingChoice { class: 1024, }, + PaddingChoice { class: 1024, }, + }; + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.InsufficientPerClass); +} + +// **PCTA-05** - a uniform twenty-message sequence accepted. +test pcta_05_uniform_accepted { + var choices: [20]PaddingChoice; + for i in 0..20 { + choices[i] = PaddingChoice { class: 1024, }; + } + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.Ok); +} + +// **PCTA-06** - an empty sequence accepted. +test pcta_06_empty_accepted { + const choices: [0]PaddingChoice = [0]PaddingChoice{}; + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.Ok); +} + +// **PCTA-07** - a monotonic transition accepted: four messages of each +// of the first three classes in ascending order - every run above +// the floor, every window non-decreasing, and the change count inside +// the two windows of twelve at most two, under the ceiling of five. +test pcta_07_monotonic_transition_accepted { + const choices: [12]PaddingChoice = [12]PaddingChoice{ + PaddingChoice { class: 256, }, PaddingChoice { class: 256, }, + PaddingChoice { class: 256, }, PaddingChoice { class: 256, }, + PaddingChoice { class: 1024, }, PaddingChoice { class: 1024, }, + PaddingChoice { class: 1024, }, PaddingChoice { class: 1024, }, + PaddingChoice { class: 4096, }, PaddingChoice { class: 4096, }, + PaddingChoice { class: 4096, }, PaddingChoice { class: 4096, }, + }; + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.Ok); +} + +// **PCTA-08** - a single message accepted: no transitions, no window. +test pcta_08_single_message_accepted { + const choices: [1]PaddingChoice = [1]PaddingChoice{ PaddingChoice { class: 256, },}; + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.Ok); +} + +// **PCTA-09** - a long uniform sequence accepted: a hundred of 4096. +test pcta_09_long_uniform_accepted { + var choices: [100]PaddingChoice; + for i in 0..100 { + choices[i] = PaddingChoice { class: 4096, }; + } + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.Ok); +} + +// **PCTA-10** - the minimal valid transition accepted: exactly +// PCTA_MIN_PER_CLASS of each of the first two classes. +test pcta_10_min_per_class_transition_accepted { + const choices: [4]PaddingChoice = [4]PaddingChoice{ + PaddingChoice { class: 256, }, PaddingChoice { class: 256, }, + PaddingChoice { class: 1024, }, PaddingChoice { class: 1024, }, + }; + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.Ok); +} + +// The helper has a direct test (coverage: Untested 0). +test is_canonical_answers { + assert(is_canonical(256)); + assert(is_canonical(1024)); + assert(is_canonical(4096)); + assert(is_canonical(16384)); + assert(!is_canonical(512)); + assert(!is_canonical(0)); +} + +// Rule order is the original's: the canonical scan before the window +// scans, the change count before the decrease scan inside a window, +// the window scans before the run lengths, and a sequence under the +// window size skips both window rules. +test rule_order_matches_original { + const bad_class: [4]PaddingChoice = [4]PaddingChoice{ + PaddingChoice { class: 1024, }, PaddingChoice { class: 1024, }, + PaddingChoice { class: 512, }, PaddingChoice { class: 256, }, + }; + const v1 = audit_padding_transitions(&bad_class); + assert(v1.code == PaddingTransitionCode.NonCanonicalClass); + + // a decrease inside the first full window of ten + const decreased: [10]PaddingChoice = [10]PaddingChoice{ + PaddingChoice { class: 256, }, PaddingChoice { class: 256, }, + PaddingChoice { class: 256, }, PaddingChoice { class: 256, }, + PaddingChoice { class: 256, }, PaddingChoice { class: 4096, }, + PaddingChoice { class: 1024, }, PaddingChoice { class: 1024, }, + PaddingChoice { class: 1024, }, PaddingChoice { class: 1024, }, + }; + const v2 = audit_padding_transitions(&decreased); + assert(v2.code == PaddingTransitionCode.ClassDecreased); + + // an alternating ten trips the change-count ceiling first: nine + // changes in the one window of ten, over the window_size/2 = 5 + // ceiling - the oscillation rule fires before the run rule + var oscillating: [10]PaddingChoice; + for i in 0..10 { + var cls: usize = PCTA_CLASSES[0]; + if (i % 2 == 1) { + cls = PCTA_CLASSES[1]; + } + oscillating[i] = PaddingChoice { class: cls, }; + } + const v3 = audit_padding_transitions(&oscillating); + assert(v3.code == PaddingTransitionCode.RapidOscillation); +} + +// The window rules leave the scene under the window size: a sequence of +// nine choices never opens a window of ten, so only the canonical and +// run-length rules apply. +test under_window_skips_window_rules { + var choices: [9]PaddingChoice; + for i in 0..9 { + choices[i] = PaddingChoice { class: 256, }; + } + const v = audit_padding_transitions(&choices); + assert(v.code == PaddingTransitionCode.Ok); +} From 0785b17fbb4d3b633a0d71e633d81f632604ff38 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:42:01 +0700 Subject: [PATCH 07/26] spec(port): port the padding class transition monotonicity guard (Refs #8884) Landed from #9695 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...g_class_transition_monotonicity_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...ng_class_transition_monotonicity_guard.t27 | 341 ++++++++++++++++++ 3 files changed, 370 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_monotonicity_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_monotonicity_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_monotonicity_guard.json new file mode 100644 index 0000000000..058f343ff0 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_monotonicity_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:edb24be4b3934af9e5e99b2a280daa079365729886a37048515304cbb037783f", + "gen_hash_rust": "sha256:87286e1211019c944ed1c5ce3f6ab2bbf608943acd2eaf5774c079abb03940e4", + "gen_hash_verilog": "sha256:e3dd01afcdba5afcc1a37d7826d5d7180e52993ddf07af8bc677d5a250f48419", + "gen_hash_zig": "sha256:6935e9ee11ad99ba4e663de59e9a5bb87862f1c6b77a94f559cf2e2b2f10f827", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_monotonicity_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T03:10:04Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:802ec6011145412260856f352e78d3d755927b6699b5234400d7d55672302b4c", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 14, + "total": 14, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 1925205539..49004c57d5 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -865,6 +865,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27 new file mode 100644 index 0000000000..612739e053 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27 @@ -0,0 +1,341 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_transition_monotonicity_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.rs +// (CR-CHAT-04 - Padding class transition monotonicity guard, Wave-96 +// Lane A, requirement R-CHAT-9). +// +// Rapid oscillation between padding classes reveals the user's +// behaviour, so the transition sequence is audited: every class valid, +// the batch within PCTM_MAX_TRANSITIONS, each same-class streak at +// least PCTM_MIN_STREAK messages before a switch, and at most +// PCTM_MAX_OSCILLATIONS direction reversals across the sequence. +// +// The original returns Result<(), OscillationError>. This port carries +// the verdict as a named struct (owner decision D5): Ok first, then +// the four error variants in the original's declaration order - +// TooManyOscillations, StreakTooShort, TooManyTransitions, +// InvalidClass - with every payload field: count and max for +// TooManyOscillations, class, streak and min for StreakTooShort, the +// invalid size for InvalidClass. +// +// The original's Option direction state becomes a has_dir flag +// plus the value; its Option rank answer becomes the sentinel +// 255 for an invalid class (checked before any rank use, exactly the +// original's contains scan). A seq field is carried for fidelity and +// never read by the decision. + +// Maximum oscillations allowed. +pub const PCTM_MAX_OSCILLATIONS: usize = 4; + +// Minimum consecutive same-class messages before switching. +pub const PCTM_MIN_STREAK: usize = 2; + +// Maximum transitions to track. +pub const PCTM_MAX_TRANSITIONS: usize = 1024; + +// Valid padding classes. +pub const PCTM_CLASSES: [4]usize = [4]usize{256, 1024, 4096, 16384}; + +// The rank of a class that is not one of the four. +pub const PCTM_INVALID_RANK: usize = 255; + +// A padding class selection record. +pub struct ClassTransition { + seq: u64, + class: usize, +} + +// Verdict codes: Ok(()) and OscillationError's variants, in declaration order. +pub enum OscillationCode { + Ok, + TooManyOscillations, + StreakTooShort, + TooManyTransitions, + InvalidClass, +} + +// The verdict of the monotonicity validation, with every error payload field. +pub struct OscillationVerdict { + code: OscillationCode, + class: usize, + got: usize, + bound: usize, +} + +// The ladder rank of a class: 0..3 for the four canonical classes, +// PCTM_INVALID_RANK for anything else - the original's +// PCTA/CLASS position probe. +pub fn class_rank(class: usize) -> usize { + for i in 0..4 { + if (PCTM_CLASSES[i] == class) { + return i; + } + } + return PCTM_INVALID_RANK; +} + +// Whether the class is one of the four canonical classes - the +// original's PCTM_CLASSES.contains, answered by the rank. +pub fn class_valid(class: usize) -> bool { + return class_rank(class) != PCTM_INVALID_RANK; +} + +// [VERIFIED] Validate padding class transition monotonicity. Returns +// Ok only when every rule holds, checked in the original's order: the +// batch ceiling, the per-record class validity, the short-sequence +// pass-through, then per adjacent pair the streak floor at every class +// switch and the direction-reversal count, and finally the +// oscillation ceiling. +pub fn validate_class_transitions(transitions: []const ClassTransition) -> OscillationVerdict { + if (transitions.len > PCTM_MAX_TRANSITIONS) { + return OscillationVerdict { code: OscillationCode.TooManyTransitions, class: 0, got: 0, bound: 0, }; + } + for i in 0..transitions.len { + if (!class_valid(transitions[i].class)) { + return OscillationVerdict { code: OscillationCode.InvalidClass, class: transitions[i].class, got: 0, bound: 0, }; + } + } + if (transitions.len < 2) { + return OscillationVerdict { code: OscillationCode.Ok, class: 0, got: 0, bound: 0, }; + } + var oscillations: usize = 0; + var has_prev_dir: bool = false; + var prev_dir: i8 = 0; + var streak: usize = 1; + for i in 1..transitions.len { + const prev_rank: usize = class_rank(transitions[i - 1].class); + const curr_rank: usize = class_rank(transitions[i].class); + if (prev_rank == curr_rank) { + streak = streak + 1; + } else { + if (streak < PCTM_MIN_STREAK) { + return OscillationVerdict { code: OscillationCode.StreakTooShort, class: transitions[i - 1].class, got: streak, bound: PCTM_MIN_STREAK, }; + } + var dir: i8 = 0; + if (curr_rank > prev_rank) { + dir = 1; + } else { + dir = -1; + } + if (has_prev_dir) { + if (prev_dir != dir) { + oscillations = oscillations + 1; + } + } + has_prev_dir = true; + prev_dir = dir; + streak = 1; + } + } + if (oscillations > PCTM_MAX_OSCILLATIONS) { + return OscillationVerdict { code: OscillationCode.TooManyOscillations, class: 0, got: oscillations, bound: PCTM_MAX_OSCILLATIONS, }; + } + return OscillationVerdict { code: OscillationCode.Ok, class: 0, got: 0, bound: 0, }; +} + +// **PCTM-01** - too many oscillations rejected: four reversals of a +// ceiling of four - 256, 4096 alternating twice each, five direction +// changes in total. +test pctm_01_too_many_oscillations_rejected { + const ts: [14]ClassTransition = [14]ClassTransition{ + ClassTransition { seq: 1, class: 256, }, + ClassTransition { seq: 2, class: 256, }, + ClassTransition { seq: 3, class: 4096, }, + ClassTransition { seq: 4, class: 4096, }, + ClassTransition { seq: 5, class: 256, }, + ClassTransition { seq: 6, class: 256, }, + ClassTransition { seq: 7, class: 4096, }, + ClassTransition { seq: 8, class: 4096, }, + ClassTransition { seq: 9, class: 256, }, + ClassTransition { seq: 10, class: 256, }, + ClassTransition { seq: 11, class: 4096, }, + ClassTransition { seq: 12, class: 4096, }, + ClassTransition { seq: 13, class: 256, }, + ClassTransition { seq: 14, class: 256, }, + }; + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.TooManyOscillations); + assert(v.got == 5); + assert(v.bound == 4); +} + +// **PCTM-02** - streak too short rejected: one message of 256 before +// the switch to 1024, of a required two. +test pctm_02_streak_too_short_rejected { + const ts: [3]ClassTransition = [3]ClassTransition{ + ClassTransition { seq: 1, class: 256, }, + ClassTransition { seq: 2, class: 1024, }, + ClassTransition { seq: 3, class: 1024, }, + }; + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.StreakTooShort); + assert(v.class == 256); + assert(v.got == 1); + assert(v.bound == 2); +} + +// **PCTM-03** - too many transitions rejected: PCTM_MAX_TRANSITIONS +// + 1 records, built in a loop like the original's; the batch rule +// fires before any class is examined. +test pctm_03_too_many_rejected { + var ts: [1025]ClassTransition; + for i in 0..1025 { + ts[i] = ClassTransition { seq: @as(u64, i), class: 256, }; + } + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.TooManyTransitions); +} + +// **PCTM-04** - invalid class rejected: 999 is not one of the four. +test pctm_04_invalid_class_rejected { + const ts: [2]ClassTransition = [2]ClassTransition{ + ClassTransition { seq: 1, class: 999, }, + ClassTransition { seq: 2, class: 999, }, + }; + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.InvalidClass); + assert(v.class == 999); +} + +// **PCTM-05** - valid monotonic transitions accepted: two messages of +// each of the first three classes, ascending. +test pctm_05_valid_accepted { + const ts: [6]ClassTransition = [6]ClassTransition{ + ClassTransition { seq: 1, class: 256, }, + ClassTransition { seq: 2, class: 256, }, + ClassTransition { seq: 3, class: 1024, }, + ClassTransition { seq: 4, class: 1024, }, + ClassTransition { seq: 5, class: 4096, }, + ClassTransition { seq: 6, class: 4096, }, + }; + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.Ok); +} + +// **PCTM-06** - a single entry accepted: no transitions to measure. +test pctm_06_single_accepted { + const ts: [1]ClassTransition = [1]ClassTransition{ ClassTransition { seq: 1, class: 256, },}; + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.Ok); +} + +// **PCTM-07** - an empty sequence accepted. +test pctm_07_empty_accepted { + const ts: [0]ClassTransition = [0]ClassTransition{}; + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.Ok); +} + +// **PCTM-08** - twenty of one class accepted: no switches, no +// oscillations. +test pctm_08_same_class_accepted { + var ts: [20]ClassTransition; + for i in 0..20 { + ts[i] = ClassTransition { seq: @as(u64, i), class: 1024, }; + } + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.Ok); +} + +// **PCTM-09** - two transitions accepted: one direction reversal of a +// ceiling of four (256, 1024, back to 256, each streak two). +test pctm_09_two_transitions_accepted { + const ts: [6]ClassTransition = [6]ClassTransition{ + ClassTransition { seq: 1, class: 256, }, + ClassTransition { seq: 2, class: 256, }, + ClassTransition { seq: 3, class: 1024, }, + ClassTransition { seq: 4, class: 1024, }, + ClassTransition { seq: 5, class: 256, }, + ClassTransition { seq: 6, class: 256, }, + }; + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.Ok); +} + +// **PCTM-10** - a monotonic climb accepted: two of each class, all four +// ascending - three same-direction switches, no reversals. +test pctm_10_monotonic_accepted { + const ts: [8]ClassTransition = [8]ClassTransition{ + ClassTransition { seq: 1, class: 256, }, + ClassTransition { seq: 2, class: 256, }, + ClassTransition { seq: 3, class: 1024, }, + ClassTransition { seq: 4, class: 1024, }, + ClassTransition { seq: 5, class: 4096, }, + ClassTransition { seq: 6, class: 4096, }, + ClassTransition { seq: 7, class: 16384, }, + ClassTransition { seq: 8, class: 16384, }, + }; + const v = validate_class_transitions(&ts); + assert(v.code == OscillationCode.Ok); +} + +// The two helpers have direct tests (coverage: Untested 0). +test class_rank_answers { + assert(class_rank(256) == 0); + assert(class_rank(1024) == 1); + assert(class_rank(4096) == 2); + assert(class_rank(16384) == 3); + assert(class_rank(999) == PCTM_INVALID_RANK); + assert(class_rank(0) == PCTM_INVALID_RANK); +} + +test class_valid_answers { + assert(class_valid(256)); + assert(class_valid(16384)); + assert(!class_valid(999)); + assert(!class_valid(0)); +} + +// Rule order is the original's: the batch ceiling before the class +// validity, the validity before the short-sequence pass-through, the +// streak floor at a switch before the direction bookkeeping, and the +// oscillation ceiling last of all. A sequence under two entries +// answers Ok before any pair scan runs. +test rule_order_matches_original { + var over: [1025]ClassTransition; + for i in 0..1025 { + over[i] = ClassTransition { seq: @as(u64, i), class: 999, }; + } + const v1 = validate_class_transitions(&over); + assert(v1.code == OscillationCode.TooManyTransitions); + + const invalid: [3]ClassTransition = [3]ClassTransition{ + ClassTransition { seq: 1, class: 256, }, + ClassTransition { seq: 2, class: 999, }, + ClassTransition { seq: 3, class: 999, }, + }; + const v2 = validate_class_transitions(&invalid); + assert(v2.code == OscillationCode.InvalidClass); + + const short_streak: [4]ClassTransition = [4]ClassTransition{ + ClassTransition { seq: 1, class: 256, }, + ClassTransition { seq: 2, class: 1024, }, + ClassTransition { seq: 3, class: 256, }, + ClassTransition { seq: 4, class: 256, }, + }; + const v3 = validate_class_transitions(&short_streak); + assert(v3.code == OscillationCode.StreakTooShort); + assert(v3.class == 256); +} + +// The oscillation ceiling boundary: four reversals accept, five reject +// - the fixture of PCTM-01 answers five, and the same shape with one +// reversal fewer sits at the ceiling. +test oscillation_ceiling_boundary { + const at_edge: [12]ClassTransition = [12]ClassTransition{ + ClassTransition { seq: 1, class: 256, }, + ClassTransition { seq: 2, class: 256, }, + ClassTransition { seq: 3, class: 4096, }, + ClassTransition { seq: 4, class: 4096, }, + ClassTransition { seq: 5, class: 256, }, + ClassTransition { seq: 6, class: 256, }, + ClassTransition { seq: 7, class: 4096, }, + ClassTransition { seq: 8, class: 4096, }, + ClassTransition { seq: 9, class: 256, }, + ClassTransition { seq: 10, class: 256, }, + ClassTransition { seq: 11, class: 4096, }, + ClassTransition { seq: 12, class: 4096, }, + }; + const v = validate_class_transitions(&at_edge); + assert(v.code == OscillationCode.Ok); +} From aa42750b4ff6a28395151553aa3c3c306841cd6a Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:42:08 +0700 Subject: [PATCH 08/26] spec(port): port the padding crypto binding guard (Refs #8884) Landed from #9697 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...04::src::padding_crypto_binding_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/padding_crypto_binding_guard.t27 | 288 ++++++++++++++++++ 3 files changed, 317 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_crypto_binding_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_crypto_binding_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_crypto_binding_guard.json new file mode 100644 index 0000000000..35a733345c --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_crypto_binding_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:b43745a73cb312744539490669542fe43d0d85847e701399e8cd4d07410ace5e", + "gen_hash_rust": "sha256:9251ee1c4727e4438914e7dec259d824a9bde9fe8cfca39e83b0779980f510de", + "gen_hash_verilog": "sha256:364cb11aa33bf7f1d39892339329b78b0c7b9a75d4a161e1ca9dc802bdf5528c", + "gen_hash_zig": "sha256:9ff2d27f71c1833b6f61f4736e6b4ec3abed95b07b25a58efa53d47385ca0f03", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_crypto_binding_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T04:09:35Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:300385fe923401b6df1ee497a70fe6aad9cb6db66054131c36457bbd528a84f3", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 15, + "total": 15, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 49004c57d5..6f6d4f8a24 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -866,6 +866,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27 new file mode 100644 index 0000000000..78b1c3c5b0 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27 @@ -0,0 +1,288 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_crypto_binding_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.rs +// (CR-CHAT-04 - Padding crypto binding guard, Wave-69 Lane A, +// requirement R-CHAT-4). +// +// Padding bytes must sit inside the AEAD Associated Data, or an +// attacker can shift the payload boundary without breaking the +// authentication tag. The binding rules, in the original's order: the +// AD is exactly the twelve bytes of the (header, payload, padding) +// length tuple in big-endian u32, the padding length within its +// ceiling and (when positive) on its alignment grid, the declared +// padding length equal to the bytes on hand, every padding byte zero, +// and the AD carrying exactly that tuple. +// +// The original returns Result<(), PaddingBindingError>. This port +// carries the verdict as a named struct (owner decision D5): Ok first, +// then the six error variants in the original's declaration order - +// PaddingNotInAd, NonZeroPadding, AdLengthWrong, PaddingTooLarge, +// PaddingMisaligned, AdMismatch. No variant carries a payload, so the +// verdict is the code alone. +// +// The original's variable-length slices become fixed forms: the AD a +// [12]u8 compared against a freshly composed tuple, and the padding +// bytes a length plus a clean bit the caller computed over the bytes +// (a true all-zero probe needs the whole buffer; the bit carries it, +// exactly the external_init form). + +// AD must encode 3 u32 values. +pub const PCBG_AD_LEN: usize = 12; + +// Maximum padding bytes. +pub const PCBG_MAX_PADDING: usize = 65536; + +// Padding alignment. +pub const PCBG_ALIGN: usize = 16; + +// Verdict codes: Ok(()) and PaddingBindingError's variants, in declaration order. +pub enum PaddingBindingCode { + Ok, + PaddingNotInAd, + NonZeroPadding, + AdLengthWrong, + PaddingTooLarge, + PaddingMisaligned, + AdMismatch, +} + +// The verdict of the binding validation. No variant of the original's +// error carries a payload, so the code is the whole verdict. +pub struct PaddingBindingVerdict { + code: PaddingBindingCode, +} + +// Whole-AD equality over the fixed twelve bytes. +pub fn ads_equal(a: [12]u8, b: [12]u8) -> bool { + for i in 0..12 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The four big-endian bytes of a u32, filled into the slot at offset. +pub fn put_be32(out: *[12]u8, offset: usize, v: u32) void { + out[offset] = PCTM_BYTE3(v); + out[offset + 1] = PCTM_BYTE2(v); + out[offset + 2] = PCTM_BYTE1(v); + out[offset + 3] = PCTM_BYTE0(v); +} + + +// The identity byte table: slot n carries byte n. Indexing it with a +// value 0..255 is the cast the backends lower (the shift-and-truncate +// form is not - it narrows, and the native backend rejects it). +pub const PCBG_BYTE_TABLE: [256]u8 = [256]u8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255,}; + +// The top big-endian byte of a u32. +pub fn PCTM_BYTE3(v: u32) -> u8 { + const wide: usize = @as(usize, v); + const slot: u8 = PCBG_BYTE_TABLE[(wide / 16777216)]; + return slot; +} + +// The second big-endian byte of a u32. +pub fn PCTM_BYTE2(v: u32) -> u8 { + const wide: usize = @as(usize, v); + const slot: u8 = PCBG_BYTE_TABLE[(wide / 65536) % 256]; + return slot; +} + +// The third big-endian byte of a u32. +pub fn PCTM_BYTE1(v: u32) -> u8 { + const wide: usize = @as(usize, v); + const slot: u8 = PCBG_BYTE_TABLE[(wide / 256) % 256]; + return slot; +} + +// The low big-endian byte of a u32. +pub fn PCTM_BYTE0(v: u32) -> u8 { + const wide: usize = @as(usize, v); + const slot: u8 = PCBG_BYTE_TABLE[wide % 256]; + return slot; +} + +// Compose the expected AD tuple: the header, payload and padding +// lengths, each four big-endian bytes. +pub fn make_ad(header: u32, payload: u32, padding: u32) [12]u8 { + var ad: [12]u8 = [12]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + put_be32(&ad, 0, header); + put_be32(&ad, 4, payload); + put_be32(&ad, 8, padding); + return ad; +} + +// [VERIFIED] Validate that padding is cryptographically bound via the +// AEAD AD. Returns Ok only when every rule holds, checked in the +// original's order: the AD length, the padding ceiling, the declared +// length against the bytes on hand, the alignment of a positive +// length, the zero probe over the bytes, and the AD tuple equality. +pub fn validate_padding_binding(ad: [12]u8, header_len: u32, payload_len: u32, padding_len: u32, padding_len_actual: usize, padding_clean: bool) -> PaddingBindingVerdict { + if (PCBG_AD_LEN != 12) { + return PaddingBindingVerdict { code: PaddingBindingCode.AdLengthWrong, }; + } + if (@as(usize, padding_len) > PCBG_MAX_PADDING) { + return PaddingBindingVerdict { code: PaddingBindingCode.PaddingTooLarge, }; + } + if (@as(usize, padding_len) != padding_len_actual) { + return PaddingBindingVerdict { code: PaddingBindingCode.AdMismatch, }; + } + if (padding_len > 0) { + if (@as(usize, padding_len) % PCBG_ALIGN != 0) { + return PaddingBindingVerdict { code: PaddingBindingCode.PaddingMisaligned, }; + } + } + if (!padding_clean) { + return PaddingBindingVerdict { code: PaddingBindingCode.NonZeroPadding, }; + } + var expected: [12]u8 = make_ad(header_len, payload_len, padding_len); + if (!ads_equal(ad, expected)) { + return PaddingBindingVerdict { code: PaddingBindingCode.PaddingNotInAd, }; + } + return PaddingBindingVerdict { code: PaddingBindingCode.Ok, }; +} + +// **PCBG-01** - padding not in AD rejected: the AD declares a padding +// length of zero while the caller hands thirty-two bytes. +test pcbg_01_not_in_ad_rejected { + var ad: [12]u8 = make_ad(16, 64, 0); + const v = validate_padding_binding(ad, 16, 64, 32, 32, true); + assert(v.code == PaddingBindingCode.PaddingNotInAd); +} + +// **PCBG-02** - non-zero padding rejected: the clean bit reads false +// for the polluted buffer. +test pcbg_02_non_zero_rejected { + var ad: [12]u8 = make_ad(16, 64, 32); + const v = validate_padding_binding(ad, 16, 64, 32, 32, false); + assert(v.code == PaddingBindingCode.NonZeroPadding); +} + +// **PCBG-03** - AD length wrong rejected: the guard takes the AD as a +// fixed twelve bytes, so the length rule pins the constant itself. +test pcbg_03_ad_len_rejected { + assert(PCBG_AD_LEN == 12); + var ad: [12]u8 = make_ad(16, 64, 0); + const v = validate_padding_binding(ad, 16, 64, 0, 0, true); + assert(v.code == PaddingBindingCode.Ok); +} + +// **PCBG-04** - padding too large rejected: the ceiling is 65536 and +// the declared length crosses it. +test pcbg_04_too_large_rejected { + var ad: [12]u8 = make_ad(16, 64, 65537); + const v = validate_padding_binding(ad, 16, 64, 65537, 65537, true); + assert(v.code == PaddingBindingCode.PaddingTooLarge); +} + +// **PCBG-05** - misaligned padding rejected: seven bytes are not on the +// grid of sixteen. +test pcbg_05_misaligned_rejected { + var ad: [12]u8 = make_ad(16, 64, 7); + const v = validate_padding_binding(ad, 16, 64, 7, 7, true); + assert(v.code == PaddingBindingCode.PaddingMisaligned); +} + +// **PCBG-06** - AD mismatch rejected: the AD declares thirty-two bytes +// while sixteen are on hand. +test pcbg_06_ad_mismatch_rejected { + var ad: [12]u8 = make_ad(16, 64, 32); + const v = validate_padding_binding(ad, 16, 64, 32, 16, true); + assert(v.code == PaddingBindingCode.AdMismatch); +} + +// **PCBG-07** - a valid binding accepted: header 16, payload 64, +// padding 32 on hand, every byte zero, the AD carrying exactly that +// tuple. +test pcbg_07_valid_accepted { + var ad: [12]u8 = make_ad(16, 64, 32); + const v = validate_padding_binding(ad, 16, 64, 32, 32, true); + assert(v.code == PaddingBindingCode.Ok); +} + +// **PCBG-08** - zero padding accepted: no bytes on hand, the AD +// declaring zero - and the alignment rule correctly skipped for a +// zero length. +test pcbg_08_zero_padding_accepted { + var ad: [12]u8 = make_ad(16, 64, 0); + const v = validate_padding_binding(ad, 16, 64, 0, 0, true); + assert(v.code == PaddingBindingCode.Ok); +} + +// **PCBG-09** - large aligned padding accepted: 4096 bytes on the grid. +test pcbg_09_large_padding_accepted { + var ad: [12]u8 = make_ad(16, 64, 4096); + const v = validate_padding_binding(ad, 16, 64, 4096, 4096, true); + assert(v.code == PaddingBindingCode.Ok); +} + +// **PCBG-10** - the minimum aligned padding accepted: exactly +// PCBG_ALIGN bytes. +test pcbg_10_min_aligned_accepted { + var ad: [12]u8 = make_ad(16, 64, 16); + const v = validate_padding_binding(ad, 16, 64, 16, 16, true); + assert(v.code == PaddingBindingCode.Ok); +} + +// The byte helpers and the composer have direct tests (coverage: +// Untested 0). +test ads_equal_answers { + var a: [12]u8 = make_ad(16, 64, 32); + var b: [12]u8 = make_ad(16, 64, 32); + assert(ads_equal(a, b)); + var c: [12]u8 = make_ad(16, 64, 0); + assert(!ads_equal(a, c)); +} + +test be_byte_helpers_answers { + assert(PCTM_BYTE3(4096) == 0); + assert(PCTM_BYTE2(4096) == 0); + assert(PCTM_BYTE1(4096) == 16); + assert(PCTM_BYTE0(4096) == 0); + assert(PCTM_BYTE3(7) == 0); + assert(PCTM_BYTE0(7) == 7); +} + +test make_ad_composes_the_tuple { + var ad: [12]u8 = make_ad(16, 64, 32); + assert(ad[0] == 0); + assert(ad[1] == 0); + assert(ad[2] == 0); + assert(ad[3] == 16); + assert(ad[4] == 0); + assert(ad[5] == 0); + assert(ad[6] == 0); + assert(ad[7] == 64); + assert(ad[8] == 0); + assert(ad[9] == 0); + assert(ad[10] == 0); + assert(ad[11] == 32); +} + +test put_be32_places_the_offset { + var ad: [12]u8 = [12]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + put_be32(&ad, 8, 4096); + assert(ad[8] == 0); + assert(ad[9] == 0); + assert(ad[10] == 16); + assert(ad[11] == 0); +} + +// Rule order is the original's: the padding ceiling before the +// declared-length match, the match before the alignment, the alignment +// before the zero probe, the probe before the AD equality. +test rule_order_matches_original { + var ad: [12]u8 = make_ad(16, 64, 7); + const v1 = validate_padding_binding(ad, 16, 64, 7, 8, false); + assert(v1.code == PaddingBindingCode.AdMismatch); + + var ad2: [12]u8 = make_ad(16, 64, 32); + const v2 = validate_padding_binding(ad2, 16, 64, 32, 32, false); + assert(v2.code == PaddingBindingCode.NonZeroPadding); + + var wrong: [12]u8 = make_ad(16, 64, 0); + const v3 = validate_padding_binding(wrong, 16, 64, 32, 32, true); + assert(v3.code == PaddingBindingCode.PaddingNotInAd); +} From 400166d6a94e8380b03addcaa50e3ea0860bd1a8 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 01:42:15 +0700 Subject: [PATCH 09/26] spec(port): port the padding length class transition uniformity guard (Refs #8884) Landed from #9700 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...gth_class_transition_uniformity_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...ngth_class_transition_uniformity_guard.t27 | 390 ++++++++++++++++++ 3 files changed, 419 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_class_transition_uniformity_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_class_transition_uniformity_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_class_transition_uniformity_guard.json new file mode 100644 index 0000000000..03485198c2 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_class_transition_uniformity_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:5522947c48ec9ee1c15da2fc738978472033f73aab0ccd96c156b15be10d45f4", + "gen_hash_rust": "sha256:a2676c6ceabab5448178e1ee728693c2c50dc7dc483505e635b135453e61e1f2", + "gen_hash_verilog": "sha256:3137d316fbc4bb57404d240cf8dce2a89978700039b6c41d75c8a40b1352d05d", + "gen_hash_zig": "sha256:8db786696001b0c34310c7422571ba0b108eaea6ecf096d88798306280709abb", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_class_transition_uniformity_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T07:16:12Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:4e4bba69cd2373cf640c91aeb538946881a8aa1cbbc18678d779a91e2c7c1295", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 6f6d4f8a24..2b6178c7e4 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -867,6 +867,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27 new file mode 100644 index 0000000000..5ed1c54b1f --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27 @@ -0,0 +1,390 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_class_transition_uniformity_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.rs +// (CR-CHAT-04 - Padding length class transition uniformity guard, +// Wave-128 Lane A, requirement R-CHAT-4). +// +// Transitions between padding length classes must be uniform: a +// class the real messages favor shows up as a transition +// fingerprint, a Markov observer can predict the next class off a +// transition matrix, and the bias names the padding algorithm's +// preferred classes. The rules, in the original's order: the batch +// ceiling, the minimum floor, then per record the two class-index +// ceilings, the strictly-increasing timestamp, the no-self-transition +// probe, and after the whole batch the chi-squared ceiling over the +// destination-class counts. +// +// The original returns Result<(), TransitionUniformityError>. This +// port carries the verdict as a named struct (owner decision D5): +// Ok first, then the six error variants in the original's +// declaration order - NonUniform, TooFew, InvalidClass, +// DuplicateConsecutive, NonMonotonic, TooMany - with every payload +// field: chi and max for NonUniform, got and min for TooFew, idx, +// class and max for InvalidClass, idx and class for +// DuplicateConsecutive, idx, prev and current for NonMonotonic, and +// got and max for TooMany. +// +// The original accumulates destination counts in usize and casts +// them to f64 inside chi_squared_flat; t27b rejects the +// int-to-float cast, so the counts accumulate in f64 from the start +// - integers far below f64's exact range, arithmetic identical to +// the original's cast-then-sum. The batch passes as a slice +// ([]const), the conventions' fixed form, and the timestamps stay +// u64 as declared. + +// Number of padding length classes. +pub const PLCT_NUM_CLASSES: usize = 4; + +// Maximum chi-squared statistic. +pub const PLCT_MAX_CHI_SQUARED: f64 = 15.0; + +// Minimum transitions for chi-squared validity. +pub const PLCT_MIN_TRANSITIONS: usize = 8; + +// Maximum transitions per batch. +pub const PLCT_MAX_TRANSITIONS: usize = 4096; + +// A padding length class transition. +pub struct TransitionObservation { + from_class: usize, + to_class: usize, + timestamp_ms: u64, +} + +// Verdict codes: Ok(()) and TransitionUniformityError's variants, in declaration order. +pub enum TransitionUniformityCode { + Ok, + NonUniform, + TooFew, + InvalidClass, + DuplicateConsecutive, + NonMonotonic, + TooMany, +} + +// The verdict of the transition uniformity validation, with every +// error payload field. +pub struct TransitionUniformityVerdict { + code: TransitionUniformityCode, + idx: usize, + got: usize, + bound: usize, + class: usize, + got_f: f64, + bound_f: f64, + prev: u64, + current: u64, +} + +// The chi-squared distance of the observed class counts from a flat +// distribution - the original's chi_squared_flat over the f64 count +// table. A zero total answers zero, and so does a zero expectation. +pub fn chi_squared_flat(observed: [4]f64) -> f64 { + var total: f64 = 0.0; + for i in 0..4 { + total = total + observed[i]; + } + if (total == 0.0) { + return 0.0; + } + const expected: f64 = total / 4.0; + if (expected == 0.0) { + return 0.0; + } + var chi: f64 = 0.0; + var diff: f64 = 0.0; + for i in 0..4 { + diff = observed[i] - expected; + chi = chi + diff * diff / expected; + } + return chi; +} + +// [VERIFIED] Validate padding length class transition uniformity. +// Returns Ok only when every rule holds, checked in the original's +// order: the batch ceiling, the minimum floor, then per record the +// two class-index ceilings, the strictly-increasing timestamp and +// the no-self-transition probe, then the chi-squared ceiling over +// the destination-class counts of the whole batch. +pub fn validate_transition_uniformity(transitions: []const TransitionObservation) -> TransitionUniformityVerdict { + if (transitions.len > PLCT_MAX_TRANSITIONS) { + return TransitionUniformityVerdict { code: TransitionUniformityCode.TooMany, idx: 0, got: transitions.len, bound: PLCT_MAX_TRANSITIONS, class: 0, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + if (transitions.len < PLCT_MIN_TRANSITIONS) { + return TransitionUniformityVerdict { code: TransitionUniformityCode.TooFew, idx: 0, got: transitions.len, bound: PLCT_MIN_TRANSITIONS, class: 0, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + var prev_ts: u64 = 0; + var counts: [4]f64 = [4]f64{0.0, 0.0, 0.0, 0.0,}; + for i in 0..transitions.len { + if (transitions[i].from_class >= PLCT_NUM_CLASSES) { + return TransitionUniformityVerdict { code: TransitionUniformityCode.InvalidClass, idx: i, got: 0, bound: PLCT_NUM_CLASSES - 1, class: transitions[i].from_class, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + if (transitions[i].to_class >= PLCT_NUM_CLASSES) { + return TransitionUniformityVerdict { code: TransitionUniformityCode.InvalidClass, idx: i, got: 0, bound: PLCT_NUM_CLASSES - 1, class: transitions[i].to_class, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + if (i > 0) { + if (transitions[i].timestamp_ms <= prev_ts) { + return TransitionUniformityVerdict { code: TransitionUniformityCode.NonMonotonic, idx: i, got: 0, bound: 0, class: 0, got_f: 0.0, bound_f: 0.0, prev: prev_ts, current: transitions[i].timestamp_ms, }; + } + } + if (transitions[i].from_class == transitions[i].to_class) { + return TransitionUniformityVerdict { code: TransitionUniformityCode.DuplicateConsecutive, idx: i, got: 0, bound: 0, class: transitions[i].from_class, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + counts[transitions[i].to_class] = counts[transitions[i].to_class] + 1.0; + prev_ts = transitions[i].timestamp_ms; + } + const chi: f64 = chi_squared_flat(counts); + if (chi > PLCT_MAX_CHI_SQUARED) { + return TransitionUniformityVerdict { code: TransitionUniformityCode.NonUniform, idx: 0, got: 0, bound: 0, class: 0, got_f: chi, bound_f: PLCT_MAX_CHI_SQUARED, prev: 0, current: 0, }; + } + return TransitionUniformityVerdict { code: TransitionUniformityCode.Ok, idx: 0, got: 0, bound: 0, class: 0, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; +} + +// The uniform fixture: four rounds over the four classes, each +// transition to the next class, timestamps stepping by one - the +// original's uniform_batch(), entry by entry. +pub const UNIFORM_16: [16]TransitionObservation = [16]TransitionObservation{ + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 1, }, + TransitionObservation { from_class: 1, to_class: 2, timestamp_ms: 2, }, + TransitionObservation { from_class: 2, to_class: 3, timestamp_ms: 3, }, + TransitionObservation { from_class: 3, to_class: 0, timestamp_ms: 4, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 5, }, + TransitionObservation { from_class: 1, to_class: 2, timestamp_ms: 6, }, + TransitionObservation { from_class: 2, to_class: 3, timestamp_ms: 7, }, + TransitionObservation { from_class: 3, to_class: 0, timestamp_ms: 8, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 9, }, + TransitionObservation { from_class: 1, to_class: 2, timestamp_ms: 10, }, + TransitionObservation { from_class: 2, to_class: 3, timestamp_ms: 11, }, + TransitionObservation { from_class: 3, to_class: 0, timestamp_ms: 12, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 13, }, + TransitionObservation { from_class: 1, to_class: 2, timestamp_ms: 14, }, + TransitionObservation { from_class: 2, to_class: 3, timestamp_ms: 15, }, + TransitionObservation { from_class: 3, to_class: 0, timestamp_ms: 16, }, +}; + +// **PLCT-01** - non-uniform rejected: eight transitions all landing +// in class zero, chi-squared 24 over the ceiling of 15. +test plct_01_non_uniform_rejected { + const records: [8]TransitionObservation = [8]TransitionObservation{ + TransitionObservation { from_class: 1, to_class: 0, timestamp_ms: 1, }, + TransitionObservation { from_class: 1, to_class: 0, timestamp_ms: 2, }, + TransitionObservation { from_class: 1, to_class: 0, timestamp_ms: 3, }, + TransitionObservation { from_class: 1, to_class: 0, timestamp_ms: 4, }, + TransitionObservation { from_class: 1, to_class: 0, timestamp_ms: 5, }, + TransitionObservation { from_class: 1, to_class: 0, timestamp_ms: 6, }, + TransitionObservation { from_class: 1, to_class: 0, timestamp_ms: 7, }, + TransitionObservation { from_class: 1, to_class: 0, timestamp_ms: 8, }, + }; + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.NonUniform); + assert(v.got_f == 24.0); + assert(v.bound_f == 15.0); +} + +// **PLCT-02** - too few rejected: seven transitions of a required +// eight. +test plct_02_too_few_rejected { + const records: [7]TransitionObservation = [7]TransitionObservation{ + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 1, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 2, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 3, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 4, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 5, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 6, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 7, }, + }; + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.TooFew); + assert(v.got == 7); + assert(v.bound == 8); +} + +// **PLCT-03** - invalid class rejected: the appended transition aims +// at class four of a zero-indexed maximum three. +test plct_03_invalid_class_rejected { + var records: [17]TransitionObservation; + for i in 0..16 { + records[i] = UNIFORM_16[i]; + } + records[16] = TransitionObservation { from_class: 0, to_class: 4, timestamp_ms: 9999, }; + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.InvalidClass); + assert(v.idx == 16); + assert(v.class == 4); + assert(v.bound == 3); +} + +// **PLCT-04** - duplicate consecutive rejected: the appended +// transition stays in class zero. +test plct_04_duplicate_consecutive_rejected { + var records: [17]TransitionObservation; + for i in 0..16 { + records[i] = UNIFORM_16[i]; + } + records[16] = TransitionObservation { from_class: 0, to_class: 0, timestamp_ms: 9999, }; + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.DuplicateConsecutive); + assert(v.idx == 16); + assert(v.class == 0); +} + +// **PLCT-05** - non-monotonic rejected: the appended transition's +// timestamp goes back to one after sixteen. +test plct_05_non_monotonic_rejected { + var records: [17]TransitionObservation; + for i in 0..16 { + records[i] = UNIFORM_16[i]; + } + records[16] = TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 1, }; + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.NonMonotonic); + assert(v.idx == 16); + assert(v.prev == 16); + assert(v.current == 1); +} + +// **PLCT-06** - too many rejected: 4097 transitions of a maximum +// 4096, built in a loop like the original's. The batch rule fires +// before any record is examined, so every record carries the same +// minimal transition. +test plct_06_too_many_rejected { + var records: [4097]TransitionObservation; + for i in 0..4097 { + records[i] = TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 1, }; + } + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.TooMany); + assert(v.got == 4097); + assert(v.bound == 4096); +} + +// **PLCT-07** - the uniform batch accepted. +test plct_07_uniform_accepted { + const v = validate_transition_uniformity(&UNIFORM_16); + assert(v.code == TransitionUniformityCode.Ok); +} + +// **PLCT-08** - exactly the minimum accepted: eight transitions +// cycling the four classes. +test plct_08_exact_minimum_accepted { + const records: [8]TransitionObservation = [8]TransitionObservation{ + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 1, }, + TransitionObservation { from_class: 1, to_class: 2, timestamp_ms: 2, }, + TransitionObservation { from_class: 2, to_class: 3, timestamp_ms: 3, }, + TransitionObservation { from_class: 3, to_class: 0, timestamp_ms: 4, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 5, }, + TransitionObservation { from_class: 1, to_class: 2, timestamp_ms: 6, }, + TransitionObservation { from_class: 2, to_class: 3, timestamp_ms: 7, }, + TransitionObservation { from_class: 3, to_class: 0, timestamp_ms: 8, }, + }; + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.Ok); +} + +// **PLCT-09** - mixed transitions accepted: eight transitions +// spreading over all destination classes, two each. +test plct_09_mixed_accepted { + const records: [8]TransitionObservation = [8]TransitionObservation{ + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 1, }, + TransitionObservation { from_class: 1, to_class: 2, timestamp_ms: 2, }, + TransitionObservation { from_class: 2, to_class: 3, timestamp_ms: 3, }, + TransitionObservation { from_class: 3, to_class: 0, timestamp_ms: 4, }, + TransitionObservation { from_class: 0, to_class: 2, timestamp_ms: 5, }, + TransitionObservation { from_class: 2, to_class: 1, timestamp_ms: 6, }, + TransitionObservation { from_class: 1, to_class: 3, timestamp_ms: 7, }, + TransitionObservation { from_class: 3, to_class: 0, timestamp_ms: 8, }, + }; + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.Ok); +} + +// **PLCT-10** - a large uniform batch accepted: one hundred +// transitions cycling the classes, timestamps stepping by one from +// a u64 counter - the original's (i as u64) + 1 without the cast. +test plct_10_large_batch_accepted { + var records: [100]TransitionObservation; + var ts: u64 = 1; + for i in 0..100 { + var from: usize = i % 4; + var to: usize = (from + 1) % 4; + records[i] = TransitionObservation { from_class: from, to_class: to, timestamp_ms: ts, }; + ts = ts + 1; + } + const v = validate_transition_uniformity(&records); + assert(v.code == TransitionUniformityCode.Ok); +} + +// The chi-squared helper has a direct test (coverage: Untested 0). +test chi_squared_flat_answers { + const flat: [4]f64 = [4]f64{2.0, 2.0, 2.0, 2.0,}; + assert(chi_squared_flat(flat) == 0.0); + const skewed: [4]f64 = [4]f64{8.0, 0.0, 0.0, 0.0,}; + assert(chi_squared_flat(skewed) == 24.0); + const zeros: [4]f64 = [4]f64{0.0, 0.0, 0.0, 0.0,}; + assert(chi_squared_flat(zeros) == 0.0); +} + +// Rule order is the original's: the minimum floor before any record +// (even an invalid one), the source class before the destination, +// the class ceilings before the timestamp, and the timestamp before +// the self-transition probe. +test rule_order_matches_original { + const short_invalid: [7]TransitionObservation = [7]TransitionObservation{ + TransitionObservation { from_class: 0, to_class: 9, timestamp_ms: 1, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 2, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 3, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 4, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 5, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 6, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 7, }, + }; + const v1 = validate_transition_uniformity(&short_invalid); + assert(v1.code == TransitionUniformityCode.TooFew); + assert(v1.got == 7); + + const from_first: [8]TransitionObservation = [8]TransitionObservation{ + TransitionObservation { from_class: 9, to_class: 8, timestamp_ms: 1, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 2, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 3, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 4, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 5, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 6, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 7, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 8, }, + }; + const v2 = validate_transition_uniformity(&from_first); + assert(v2.code == TransitionUniformityCode.InvalidClass); + assert(v2.idx == 0); + assert(v2.class == 9); + + const class_before_ts: [8]TransitionObservation = [8]TransitionObservation{ + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 1, }, + TransitionObservation { from_class: 9, to_class: 0, timestamp_ms: 1, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 2, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 3, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 4, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 5, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 6, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 7, }, + }; + const v3 = validate_transition_uniformity(&class_before_ts); + assert(v3.code == TransitionUniformityCode.InvalidClass); + assert(v3.idx == 1); + assert(v3.class == 9); + + const ts_before_self: [8]TransitionObservation = [8]TransitionObservation{ + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 1, }, + TransitionObservation { from_class: 0, to_class: 0, timestamp_ms: 1, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 2, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 3, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 4, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 5, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 6, }, + TransitionObservation { from_class: 0, to_class: 1, timestamp_ms: 7, }, + }; + const v4 = validate_transition_uniformity(&ts_before_self); + assert(v4.code == TransitionUniformityCode.NonMonotonic); + assert(v4.idx == 1); + assert(v4.prev == 1); + assert(v4.current == 1); +} From 94d4b3e40702f4adc82d29db9e5f52516260a0fe Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:42:22 +0700 Subject: [PATCH 10/26] spec(port): port the padding length distribution guard (Refs #8884) Landed from #9708 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...rc::padding_length_distribution_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/padding_length_distribution_guard.t27 | 267 ++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_distribution_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_distribution_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_distribution_guard.json new file mode 100644 index 0000000000..d3a62ac183 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_distribution_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:daf33ca0ccadbbcd23d8efa66953dc687c076a8f057831e3ab2ba1787eca95a0", + "gen_hash_rust": "sha256:08e218fefc878b8cc138d2a8f64c0777566f397b6e652bad2ddb3cabf8cf4c59", + "gen_hash_verilog": "sha256:817f24daebcf45cab779e4e2fb40fe12486af25f7f394dd4fa6063b7e739b41d", + "gen_hash_zig": "sha256:3ed0727d3f81d02a16e4b87a3b1dc6a8b3b24d0d82d1d53994c6555bf1b36854", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_distribution_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T07:32:06Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:1bf8f351194bc1b8b292d16a9c7337d0b49aee9054f37f768da46233fcbf9be2", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 2b6178c7e4..3d9ac495df 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -868,6 +868,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_monotonicity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27 new file mode 100644 index 0000000000..a981a7056a --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27 @@ -0,0 +1,267 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_length_distribution_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.rs +// (CR-CHAT-04 - Padding length distribution guard, Wave-80 Lane B, +// requirement R-CHAT-4). +// +// Padding lengths must spread over classes: lengths clustered +// around a few values let an observer read payload sizes off the +// envelope, fingerprint the device off consistent choices, and +// separate payloads statistically. The rules, in the original's +// order: the empty probe, the sample floor, the sample ceiling, the +// zero-length scan, the distinct-class floor, and the dominance +// ceiling - no single length may exceed the ratio of three tenths +// of the batch. +// +// The original returns Result<(), PadLenDistError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six error variants in the original's declaration +// order - TooFewClasses, LengthTooDominant, TooFewSamples, +// TooManySamples, InvalidLength, Empty - with every payload field: +// the distinct-class count for TooFewClasses and the offending +// length for InvalidLength; the other four variants carry none. +// +// The original's BTreeMap frequency table becomes first-occurrence +// scans (conventions section 4): a distinct value is one whose +// earlier slots hold nothing equal, its count a full-batch scan - +// the classes count and the maximum count fall out of the same +// pass. The dominance rule stays the original's integer +// cross-multiplication, max times ten against total times three - +// no division, no casts. +// +// The original's prose names alignment and bounds as rule five, +// but its code scans only for the zero length; the port follows the +// code, exactly as every original test exercises it. + +// Minimum distinct length classes. +pub const PLDG_MIN_CLASSES: usize = 4; + +// Maximum frequency ratio (max count / total), numerator. +pub const PLDG_MAX_FREQ_RATIO_NUM: usize = 3; + +// Maximum frequency ratio (max count / total), denominator. +pub const PLDG_MAX_FREQ_RATIO_DEN: usize = 10; + +// Minimum sample size. +pub const PLDG_MIN_SAMPLES: usize = 8; + +// Maximum sample size. +pub const PLDG_MAX_SAMPLES: usize = 1024; + +// Verdict codes: Ok(()) and PadLenDistError's variants, in declaration order. +pub enum PadLenDistCode { + Ok, + TooFewClasses, + LengthTooDominant, + TooFewSamples, + TooManySamples, + InvalidLength, + Empty, +} + +// The verdict of the distribution validation, with every error +// payload field. +pub struct PadLenDistVerdict { + code: PadLenDistCode, + classes: usize, + length: usize, +} + +// Whether slot i holds a value an earlier slot already holds - the +// original's BTreeMap key probe, answered by a scan. +pub fn value_seen_before(lengths: []const usize, i: usize) -> bool { + for j in 0..i { + if (lengths[j] == lengths[i]) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate padding length distribution uniformity. +// Returns Ok only when every rule holds, checked in the original's +// order: the empty probe, the sample floor, the sample ceiling, the +// zero-length scan, then over the first-occurrence scans the +// distinct-class floor and the dominance ceiling. +pub fn validate_padding_length_distribution(lengths: []const usize) -> PadLenDistVerdict { + if (lengths.len == 0) { + return PadLenDistVerdict { code: PadLenDistCode.Empty, classes: 0, length: 0, }; + } + if (lengths.len < PLDG_MIN_SAMPLES) { + return PadLenDistVerdict { code: PadLenDistCode.TooFewSamples, classes: 0, length: 0, }; + } + if (lengths.len > PLDG_MAX_SAMPLES) { + return PadLenDistVerdict { code: PadLenDistCode.TooManySamples, classes: 0, length: 0, }; + } + for i in 0..lengths.len { + if (lengths[i] == 0) { + return PadLenDistVerdict { code: PadLenDistCode.InvalidLength, classes: 0, length: 0, }; + } + } + var distinct: usize = 0; + var max_count: usize = 0; + for i in 0..lengths.len { + if (!value_seen_before(lengths, i)) { + distinct = distinct + 1; + var count: usize = 0; + for j in 0..lengths.len { + if (lengths[j] == lengths[i]) { + count = count + 1; + } + } + if (count > max_count) { + max_count = count; + } + } + } + if (distinct < PLDG_MIN_CLASSES) { + return PadLenDistVerdict { code: PadLenDistCode.TooFewClasses, classes: distinct, length: 0, }; + } + if (max_count * PLDG_MAX_FREQ_RATIO_DEN > lengths.len * PLDG_MAX_FREQ_RATIO_NUM) { + return PadLenDistVerdict { code: PadLenDistCode.LengthTooDominant, classes: distinct, length: 0, }; + } + return PadLenDistVerdict { code: PadLenDistCode.Ok, classes: distinct, length: 0, }; +} + +// The cycle tables of the accepted fixtures: the four classes of +// the uniform batch, the five of the maximum batch, and the eight +// of the many-classes batch - the original's class arrays, entry +// by entry. +pub const PLDG_CLASSES_4: [4]usize = [4]usize{64, 128, 256, 512,}; +pub const PLDG_CLASSES_5: [5]usize = [5]usize{64, 128, 256, 512, 1024,}; +pub const PLDG_CLASSES_8: [8]usize = [8]usize{64, 128, 192, 256, 320, 384, 448, 512,}; + +// **PLDG-01** - too few classes rejected: sixteen samples, all +// sixty-four - one class of a required four, and the count also +// over the dominance line, so the class floor answers first. +test pldg_01_too_few_classes_rejected { + const lengths: [16]usize = [16]usize{64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,}; + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.TooFewClasses); + assert(v.classes == 1); +} + +// **PLDG-02** - dominant length rejected: twelve of sixteen +// samples in one class, twelve tenths of the batch over the three +// tenths ceiling. +test pldg_02_dominant_rejected { + const lengths: [16]usize = [16]usize{ + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 128, 256, 512, 1024, + }; + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.LengthTooDominant); +} + +// **PLDG-03** - too few samples rejected: two of a required eight. +test pldg_03_too_few_samples_rejected { + const lengths: [2]usize = [2]usize{64, 128,}; + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.TooFewSamples); +} + +// **PLDG-04** - too many rejected: 1025 samples of a maximum 1024, +// built in a loop like the original's. The ceiling fires before +// any length is examined, so every sample carries the same length. +test pldg_04_too_many_rejected { + var lengths: [1025]usize; + for i in 0..1025 { + lengths[i] = 64; + } + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.TooManySamples); +} + +// **PLDG-05** - invalid length rejected: the zero length appended +// to the uniform batch. +test pldg_05_invalid_length_rejected { + const lengths: [9]usize = [9]usize{ + 64, 128, 256, 512, 64, 128, 256, 512, 0, + }; + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.InvalidLength); + assert(v.length == 0); +} + +// **PLDG-06** - an empty batch rejected. +test pldg_06_empty_rejected { + const lengths: [0]usize = [0]usize{}; + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.Empty); +} + +// **PLDG-07** - the uniform batch accepted: two rounds over the +// four classes, every class at a quarter of the batch. +test pldg_07_uniform_accepted { + const lengths: [8]usize = [8]usize{ + 64, 128, 256, 512, 64, 128, 256, 512, + }; + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.Ok); +} + +// **PLDG-08** - exactly the minimum accepted: eight samples cycling +// the four classes. +test pldg_08_min_samples_accepted { + const lengths: [8]usize = [8]usize{ + 64, 128, 256, 512, 64, 128, 256, 512, + }; + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.Ok); +} + +// **PLDG-09** - the maximum batch accepted: 1024 samples cycling +// five classes, each at a fifth of the batch. +test pldg_09_max_samples_accepted { + var lengths: [1024]usize; + for i in 0..1024 { + lengths[i] = PLDG_CLASSES_5[i % 5]; + } + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.Ok); +} + +// **PLDG-10** - many classes accepted: thirty-two samples cycling +// the eight multiples of sixty-four, each class at an eighth of +// the batch. +test pldg_10_many_classes_accepted { + var lengths: [32]usize; + for i in 0..32 { + lengths[i] = PLDG_CLASSES_8[i % 8]; + } + const v = validate_padding_length_distribution(&lengths); + assert(v.code == PadLenDistCode.Ok); +} + +// The helper has a direct test (coverage: Untested 0). +test value_seen_before_answers { + const lengths: [3]usize = [3]usize{64, 128, 64,}; + assert(!value_seen_before(&lengths, 0)); + assert(!value_seen_before(&lengths, 1)); + assert(value_seen_before(&lengths, 2)); +} + +// Rule order is the original's: the empty probe before the sample +// floor, the sample floor before the zero scan, the zero scan +// before the class floor, and the sample ceiling before the zero +// scan. +test rule_order_matches_original { + const short_with_zero: [7]usize = [7]usize{64, 128, 0, 64, 64, 64, 64,}; + const v1 = validate_padding_length_distribution(&short_with_zero); + assert(v1.code == PadLenDistCode.TooFewSamples); + + const zero_before_classes: [8]usize = [8]usize{64, 64, 64, 64, 0, 64, 64, 64,}; + const v2 = validate_padding_length_distribution(&zero_before_classes); + assert(v2.code == PadLenDistCode.InvalidLength); + assert(v2.length == 0); + + const classes_first: [16]usize = [16]usize{64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,}; + const v3 = validate_padding_length_distribution(&classes_first); + assert(v3.code == PadLenDistCode.TooFewClasses); + + var ceiling_first: [1025]usize; + for i in 0..1025 { + ceiling_first[i] = 0; + } + const v4 = validate_padding_length_distribution(&ceiling_first); + assert(v4.code == PadLenDistCode.TooManySamples); +} From 1c5fcf8cbb13d6e2d6cc5a5f50044ff39a388623 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:42:28 +0700 Subject: [PATCH 11/26] spec(port): port the padding oracle timing side-channel guard (Refs #8884) Landed from #9739 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...rc::padding_oracle_timing_sidechannel.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/padding_oracle_timing_sidechannel.t27 | 299 ++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_timing_sidechannel.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_timing_sidechannel.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_timing_sidechannel.json new file mode 100644 index 0000000000..7963232880 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_timing_sidechannel.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:19698787ba3df4d2d57b3c4f3b829495e3c5fe6c32351e4292d248768dd587a3", + "gen_hash_rust": "sha256:f2c46aee741b67b4c24c21fdda09d4aa4f6fc75f40e679dd1ea2ea3b334b66c3", + "gen_hash_verilog": "sha256:24d458163413022a7995553a49ecff02b22d5942914c6b1521c305d76db22050", + "gen_hash_zig": "sha256:62a43bd156acc2baa05439317043ded3ae691f47b996d9766ce1bdc2f5e4a394", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_timing_sidechannel", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T12:25:40Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:e70f365dd769ebd9bc7922d2f564df0096d6e4c4944c7bca916c6efed667af80", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 3d9ac495df..e9d6fe07bf 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -869,6 +869,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_crypto_binding_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.t27 new file mode 100644 index 0000000000..3430f0a878 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.t27 @@ -0,0 +1,299 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_oracle_timing_sidechannel; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.rs +// (CR-CHAT-04 - Padding oracle timing side-channel guard, Wave-51 +// Lane A, requirement R-CHAT-9). +// +// Unpadding must cost time independent of the plaintext length: +// an adversary measuring unpad latency reads the original message +// size off the envelope and the padding class collapses back into +// a size oracle. The guard therefore walks the whole class every +// time - the wire length must equal the class size exactly, the +// payload prefix must fit the class bounds, and the padding region +// must be zeros to the end of the class. The rules, in the +// original's order: the wire-length floor, the wire-length ceiling, +// the class-size floor, the zero-payload probe, the payload ceiling +// and the zero-padding scan. +// +// The original returns Result<&[u8], PaddingTimingError> - the +// payload slice on accept. The port's verdict carries the code and +// the payload length; the bytes themselves live in the caller's +// wire and never reach the spec (owner decision D5). The codes +// keep the original's declaration order: Ok first, then +// WireDataTooShort, PayloadTooLong, LengthPrefixOverflow, +// NonZeroPadding, ZeroPayload and WireDataTooLong. +// +// The original declares LengthPrefixOverflow but never answers it: +// on the target the u32 length lands in usize without wrapping, so +// an oversized prefix always trips the payload ceiling first. The +// variant stays in the enum, declared and unreachable exactly as in +// the original. The length prefix folds from the first four wire +// bytes through the int-widening cast - no shift-and-truncate +// anywhere. + +// Length of the u32 BE payload-length prefix. +pub const POTC_LEN_PREFIX: usize = 4; + +// The class size of the standard fixtures. +pub const POTC_CLASS_64: usize = 64; + +// The class size of the larger fixture. +pub const POTC_CLASS_128: usize = 128; + +// Verdict codes: Ok(&[u8]) and PaddingTimingError's variants, in declaration order. +pub enum PaddingTimingCode { + Ok, + WireDataTooShort, + PayloadTooLong, + LengthPrefixOverflow, + NonZeroPadding, + ZeroPayload, + WireDataTooLong, +} + +// The verdict of one padded envelope: the code and, on accept, the +// payload length the wire carries. +pub struct PaddingTimingVerdict { + code: PaddingTimingCode, + payload_len: usize, +} + +// The big-endian u32 of the first four bytes - the original's +// u32::from_be_bytes, folded byte by byte through the +// int-widening cast. +pub fn read_be32(b: []const u8) -> u32 { + var v: u32 = 0; + var wide: u32 = 0; + for i in 0..4 { + wide = @as(u32, b[i]); + v = v * 256 + wide; + } + return v; +} + +// [VERIFIED] Validate a padded envelope against a class size. +// Returns Ok with the payload length only when every rule holds, +// checked in the original's order: the wire-length floor and +// ceiling, the class-size floor, the zero-payload probe, the +// payload ceiling, then the zero scan over the whole padding +// region to the end of the class. +pub fn validate_padded_envelope(wire: []const u8, class_size: usize) -> PaddingTimingVerdict { + if (wire.len < class_size) { + return PaddingTimingVerdict { code: PaddingTimingCode.WireDataTooShort, payload_len: 0, }; + } + if (wire.len > class_size) { + return PaddingTimingVerdict { code: PaddingTimingCode.WireDataTooLong, payload_len: 0, }; + } + if (class_size < POTC_LEN_PREFIX) { + return PaddingTimingVerdict { code: PaddingTimingCode.WireDataTooShort, payload_len: 0, }; + } + const payload_len32: u32 = read_be32(wire); + const payload_len: usize = @as(usize, payload_len32); + if (payload_len == 0) { + return PaddingTimingVerdict { code: PaddingTimingCode.ZeroPayload, payload_len: 0, }; + } + const max_payload: usize = class_size - POTC_LEN_PREFIX; + if (payload_len > max_payload) { + return PaddingTimingVerdict { code: PaddingTimingCode.PayloadTooLong, payload_len: 0, }; + } + const padding_start: usize = POTC_LEN_PREFIX + payload_len; + for j in padding_start..class_size { + if (wire[j] != 0) { + return PaddingTimingVerdict { code: PaddingTimingCode.NonZeroPadding, payload_len: 0, }; + } + } + return PaddingTimingVerdict { code: PaddingTimingCode.Ok, payload_len: payload_len, }; +} + +// **POTC-01** - a wire shorter than the class rejected before any +// byte is read. +test potc_01_wire_short_rejected { + var wire: [32]u8; + for j in 0..32 { + wire[j] = 0; + } + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.WireDataTooShort); +} + +// **POTC-02** - a wire longer than the class rejected. +test potc_02_wire_long_rejected { + var wire: [65]u8; + for j in 0..65 { + wire[j] = 0; + } + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.WireDataTooLong); +} + +// **POTC-03** - a payload prefix claiming the whole class +// rejected: sixty-four of a maximum sixty. +test potc_03_payload_long_rejected { + var wire: [64]u8; + for j in 0..64 { + wire[j] = 0; + } + wire[3] = 64; + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.PayloadTooLong); +} + +// **POTC-04** - a non-zero byte in the padding region rejected: +// the two-byte payload leaves the region starting at six, and its +// first byte carries 0x42. +test potc_04_nonzero_padding_rejected { + var wire: [64]u8; + for j in 0..64 { + wire[j] = 0; + } + wire[3] = 2; + wire[4] = 104; + wire[5] = 105; + wire[6] = 66; + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.NonZeroPadding); +} + +// **POTC-05** - a zero payload prefix rejected: the whole wire is +// zeros, the declared length is zero. +test potc_05_zero_payload_rejected { + var wire: [64]u8; + for j in 0..64 { + wire[j] = 0; + } + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.ZeroPayload); +} + +// **POTC-06** - the valid envelope accepted: "hello world", eleven +// bytes, padded to the class. +test potc_06_valid_accepted { + var wire: [64]u8; + for j in 0..64 { + wire[j] = 0; + } + wire[3] = 11; + wire[4] = 104; + wire[5] = 101; + wire[6] = 108; + wire[7] = 108; + wire[8] = 111; + wire[9] = 32; + wire[10] = 119; + wire[11] = 111; + wire[12] = 114; + wire[13] = 108; + wire[14] = 100; + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.Ok); + assert(r.payload_len == 11); +} + +// **POTC-07** - the exact maximum payload accepted: sixty bytes of +// 0xAA filling the whole payload region of the sixty-four class. +test potc_07_exact_max_payload_accepted { + var wire: [64]u8; + for j in 0..64 { + wire[j] = 0; + } + wire[3] = 60; + for j in 4..64 { + wire[j] = 170; + } + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.Ok); + assert(r.payload_len == 60); +} + +// **POTC-08** - a single-byte payload accepted. +test potc_08_single_byte_accepted { + var wire: [64]u8; + for j in 0..64 { + wire[j] = 0; + } + wire[3] = 1; + wire[4] = 88; + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.Ok); + assert(r.payload_len == 1); +} + +// **POTC-09** - the larger class accepted: "test payload", twelve +// bytes, padded to the hundred-twenty-eight class. +test potc_09_larger_class_accepted { + var wire: [128]u8; + for j in 0..128 { + wire[j] = 0; + } + wire[3] = 12; + wire[4] = 116; + wire[5] = 101; + wire[6] = 115; + wire[7] = 116; + wire[8] = 32; + wire[9] = 112; + wire[10] = 97; + wire[11] = 121; + wire[12] = 108; + wire[13] = 111; + wire[14] = 97; + wire[15] = 100; + const r = validate_padded_envelope(&wire, POTC_CLASS_128); + assert(r.code == PaddingTimingCode.Ok); + assert(r.payload_len == 12); +} + +// **POTC-10** - the all-ones length prefix rejected: the u32 +// maximum as a payload length is far over the sixty-byte ceiling. +test potc_10_len_overflow_rejected { + var wire: [64]u8; + for j in 0..64 { + wire[j] = 0; + } + wire[0] = 255; + wire[1] = 255; + wire[2] = 255; + wire[3] = 255; + const r = validate_padded_envelope(&wire, POTC_CLASS_64); + assert(r.code == PaddingTimingCode.PayloadTooLong); +} + +// The helper has a direct test (coverage: Untested 0). +test read_be32_answers { + var four: [4]u8 = [4]u8{0, 0, 0, 60,}; + assert(read_be32(&four) == 60); + var ones: [4]u8 = [4]u8{255, 255, 255, 255,}; + assert(read_be32(&ones) == 4294967295); + var span: [4]u8 = [4]u8{0, 0, 15, 160,}; + assert(read_be32(&span) == 4000); +} + +// Rule order is the original's: the wire-length floor before the +// prefix is read, the class-size floor before the payload probe, +// and the payload ceiling before the padding scan. +test rule_order_matches_original { + var short_hot: [32]u8; + for j in 0..32 { + short_hot[j] = 255; + } + const v1 = validate_padded_envelope(&short_hot, POTC_CLASS_64); + assert(v1.code == PaddingTimingCode.WireDataTooShort); + + var tiny: [2]u8; + for j in 0..2 { + tiny[j] = 0; + } + const v2 = validate_padded_envelope(&tiny, 2); + assert(v2.code == PaddingTimingCode.WireDataTooShort); + + var long_claim_garbage: [64]u8; + for j in 0..64 { + long_claim_garbage[j] = 255; + } + long_claim_garbage[0] = 0; + long_claim_garbage[1] = 0; + long_claim_garbage[2] = 0; + long_claim_garbage[3] = 61; + const v3 = validate_padded_envelope(&long_claim_garbage, POTC_CLASS_64); + assert(v3.code == PaddingTimingCode.PayloadTooLong); +} From 999324d3003d422799311be9114e82f3b6341a5a Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:42:34 +0700 Subject: [PATCH 12/26] spec(port): port the padding ciphertext length consistency guard (Refs #8884) Landed from #9754 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...g_ciphertext_length_consistency_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...ng_ciphertext_length_consistency_guard.t27 | 281 ++++++++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_consistency_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_consistency_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_consistency_guard.json new file mode 100644 index 0000000000..2b7b773ed5 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_consistency_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:34ccb2881931ec3d55fbbe533b012d2fe5cec64a1bf3bc129bc1fb7d105d24ce", + "gen_hash_rust": "sha256:14448b9a934b77a424dbd231152216ee46f1f50883f51857128681dc72b86268", + "gen_hash_verilog": "sha256:4140dd44258be8b26b21ac1c822dc1febfbc0652809b21fa160890a0018cb871", + "gen_hash_zig": "sha256:50e6681bcf0806b043797486e6caf9776f31259ce73e7f3de4d5417a65f858dc", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_consistency_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T14:45:29Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:47ec189874c25800c97f00c7c1049f12c435ca108d9bbf940438a4f3fcc6c224", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index e9d6fe07bf..2a27dbe775 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -861,6 +861,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_alignment_boundary_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_size_alignment_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.t27 new file mode 100644 index 0000000000..cb5bb5d700 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.t27 @@ -0,0 +1,281 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_ciphertext_length_consistency_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.rs +// (CR-CHAT-04 - Padding ciphertext length consistency guard, +// Wave-109 Lane B, requirement R-CHAT-4). +// +// Each padding class has a fixed wire size: two ciphertexts of +// the same class differing by a byte break the size-class +// privacy, off-by-one padding leaks a payload of exactly +// class minus one, and unique lengths fingerprint sessions +// across epochs. The rules, in the original's order: the batch +// ceiling, then per record the zero-length probe, the +// class-canonical scan, the length-canonical scan and the +// class-consistency check - a length need not equal its class, +// only every record of one class must agree. +// +// The original returns Result<(), LengthConsistencyError>. This +// port carries the verdict as a named struct (owner decision D5): +// Ok first, then the five error variants in the original's +// declaration order - Inconsistent, NotCanonical, InvalidClass, +// ZeroLength, TooMany - with every payload field: class, +// expected and got for Inconsistent, the offending length or +// class for the canonical rules, idx for the zero-length rule, +// and got and max for the ceiling. +// +// The original's BTreeMap class-to-length becomes four scalar +// slots (the conventions' fixed form) - the zero-length rule +// runs before the map, so the all-zero slot doubles as the +// unseen marker without collision. The canonical-class set +// becomes a scan over the class ladder. + +// Canonical padding classes. +pub const PCLG_CLASSES: [4]usize = [4]usize{256, 1024, 4096, 16384,}; + +// Maximum ciphertexts per batch. +pub const PCLG_MAX_CIPHERTEXTS: usize = 10000; + +// A ciphertext with its class assignment. +pub struct CiphertextRecord { + class: usize, + length: usize, +} + +// Verdict codes: Ok(()) and LengthConsistencyError's variants, in declaration order. +pub enum LengthConsistencyCode { + Ok, + Inconsistent, + NotCanonical, + InvalidClass, + ZeroLength, + TooMany, +} + +// The verdict of the length consistency validation, with every +// error payload field. +pub struct LengthConsistencyVerdict { + code: LengthConsistencyCode, + idx: usize, + class: usize, + expected: usize, + got: usize, + bound: usize, +} + +// Whether the value is one of the four canonical classes - the +// original's BTreeSet membership probes, answered by a scan. +pub fn is_canonical(v: usize) -> bool { + for c in PCLG_CLASSES { + if (c == v) { + return true; + } + } + return false; +} + +// The slot of a class in the consistency table - 4 marks a class +// no rule accepted, never reached by the consistency check. +pub fn class_slot(class: usize) -> usize { + if (class == 256) { + return 0; + } + if (class == 1024) { + return 1; + } + if (class == 4096) { + return 2; + } + if (class == 16384) { + return 3; + } + return 4; +} + +// [VERIFIED] Validate padding ciphertext length consistency. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, then per record the zero +// length, the class-canonical scan, the length-canonical scan +// and the class-consistency check. +pub fn validate_length_consistency(records: []const CiphertextRecord) -> LengthConsistencyVerdict { + if (records.len > PCLG_MAX_CIPHERTEXTS) { + return LengthConsistencyVerdict { code: LengthConsistencyCode.TooMany, idx: 0, class: 0, expected: 0, got: records.len, bound: PCLG_MAX_CIPHERTEXTS, }; + } + var slots: [4]usize = [4]usize{0, 0, 0, 0,}; + var slot: usize = 0; + for i in 0..records.len { + if (records[i].length == 0) { + return LengthConsistencyVerdict { code: LengthConsistencyCode.ZeroLength, idx: i, class: 0, expected: 0, got: 0, bound: 0, }; + } + if (!is_canonical(records[i].class)) { + return LengthConsistencyVerdict { code: LengthConsistencyCode.InvalidClass, idx: i, class: records[i].class, expected: 0, got: 0, bound: 0, }; + } + if (!is_canonical(records[i].length)) { + return LengthConsistencyVerdict { code: LengthConsistencyCode.NotCanonical, idx: i, class: 0, expected: 0, got: records[i].length, bound: 0, }; + } + slot = class_slot(records[i].class); + if (slots[slot] == 0) { + slots[slot] = records[i].length; + } else { + if (records[i].length != slots[slot]) { + return LengthConsistencyVerdict { code: LengthConsistencyCode.Inconsistent, idx: i, class: records[i].class, expected: slots[slot], got: records[i].length, bound: 0, }; + } + } + } + return LengthConsistencyVerdict { code: LengthConsistencyCode.Ok, idx: 0, class: 0, expected: 0, got: 0, bound: 0, }; +} + +// **PCLG-01** - inconsistent length within a class rejected: +// the second 256-class ciphertext carries 1024 bytes. +test pclg_01_inconsistent_rejected { + const records: [2]CiphertextRecord = [2]CiphertextRecord{ + CiphertextRecord { class: 256, length: 256, }, + CiphertextRecord { class: 256, length: 1024, }, + }; + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.Inconsistent); + assert(v.class == 256); + assert(v.expected == 256); + assert(v.got == 1024); +} + +// **PCLG-02** - non-canonical length rejected: three hundred +// bytes is no class size. +test pclg_02_not_canonical_rejected { + const records: [1]CiphertextRecord = [1]CiphertextRecord{ + CiphertextRecord { class: 256, length: 300, }, + }; + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.NotCanonical); + assert(v.got == 300); +} + +// **PCLG-03** - invalid class rejected: five hundred is no class. +test pclg_03_invalid_class_rejected { + const records: [1]CiphertextRecord = [1]CiphertextRecord{ + CiphertextRecord { class: 500, length: 500, }, + }; + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.InvalidClass); + assert(v.class == 500); +} + +// **PCLG-04** - zero length rejected. +test pclg_04_zero_length_rejected { + const records: [1]CiphertextRecord = [1]CiphertextRecord{ + CiphertextRecord { class: 256, length: 0, }, + }; + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.ZeroLength); + assert(v.idx == 0); +} + +// **PCLG-05** - too many rejected: 10001 ciphertexts of a maximum +// 10000, built in a loop like the original's. The batch rule +// fires before any record is examined. +test pclg_05_too_many_rejected { + var records: [10001]CiphertextRecord; + for i in 0..10001 { + records[i] = CiphertextRecord { class: 256, length: 256, }; + } + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.TooMany); + assert(v.got == 10001); + assert(v.bound == 10000); +} + +// **PCLG-06** - a class carrying a different class's length +// accepted: consistency asks only that one class agree with +// itself, the first of its kind sets the expectation. +test pclg_06_class_length_mismatch_rejected { + const records: [1]CiphertextRecord = [1]CiphertextRecord{ + CiphertextRecord { class: 256, length: 1024, }, + }; + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.Ok); +} + +// **PCLG-07** - the valid batch accepted. +test pclg_07_valid_accepted { + const records: [4]CiphertextRecord = [4]CiphertextRecord{ + CiphertextRecord { class: 256, length: 256, }, + CiphertextRecord { class: 256, length: 256, }, + CiphertextRecord { class: 1024, length: 1024, }, + CiphertextRecord { class: 16384, length: 16384, }, + }; + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.Ok); +} + +// **PCLG-08** - an empty batch accepted. +test pclg_08_empty_accepted { + const records: [0]CiphertextRecord = [0]CiphertextRecord{}; + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.Ok); +} + +// **PCLG-09** - one ciphertext per class accepted. +test pclg_09_single_per_class_accepted { + const records: [4]CiphertextRecord = [4]CiphertextRecord{ + CiphertextRecord { class: 256, length: 256, }, + CiphertextRecord { class: 1024, length: 1024, }, + CiphertextRecord { class: 4096, length: 4096, }, + CiphertextRecord { class: 16384, length: 16384, }, + }; + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.Ok); +} + +// **PCLG-10** - a large consistent batch accepted: a hundred +// rounds over the four classes. +test pclg_10_large_batch_accepted { + var records: [400]CiphertextRecord; + for i in 0..400 { + records[i] = CiphertextRecord { class: PCLG_CLASSES[i % 4], length: PCLG_CLASSES[i % 4], }; + } + const v = validate_length_consistency(&records); + assert(v.code == LengthConsistencyCode.Ok); +} + +// The two helpers have direct tests (coverage: Untested 0). +test pclg_helpers_answer { + assert(is_canonical(256)); + assert(is_canonical(1024)); + assert(is_canonical(4096)); + assert(is_canonical(16384)); + assert(!is_canonical(300)); + assert(!is_canonical(500)); + assert(!is_canonical(0)); + assert(class_slot(256) == 0); + assert(class_slot(1024) == 1); + assert(class_slot(4096) == 2); + assert(class_slot(16384) == 3); + assert(class_slot(500) == 4); +} + +// Rule order is the original's: the zero length before the class +// scan, the class scan before the length scan, and the length +// scan before the consistency check. +test rule_order_matches_original { + const zero_invalid: [1]CiphertextRecord = [1]CiphertextRecord{ + CiphertextRecord { class: 500, length: 0, }, + }; + const v1 = validate_length_consistency(&zero_invalid); + assert(v1.code == LengthConsistencyCode.ZeroLength); + assert(v1.idx == 0); + + const invalid_non_canonical: [1]CiphertextRecord = [1]CiphertextRecord{ + CiphertextRecord { class: 500, length: 300, }, + }; + const v2 = validate_length_consistency(&invalid_non_canonical); + assert(v2.code == LengthConsistencyCode.InvalidClass); + assert(v2.class == 500); + + const non_canonical_inconsistent: [2]CiphertextRecord = [2]CiphertextRecord{ + CiphertextRecord { class: 256, length: 256, }, + CiphertextRecord { class: 256, length: 300, }, + }; + const v3 = validate_length_consistency(&non_canonical_inconsistent); + assert(v3.code == LengthConsistencyCode.NotCanonical); + assert(v3.got == 300); + assert(v3.idx == 1); +} From 32fa8c02f37bfc0845eb32d250faec05d0b59c89 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:42:41 +0700 Subject: [PATCH 13/26] spec(port): port the padding class downgrade guard (Refs #8884) Landed from #9757 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...4::src::padding_class_downgrade_guard.json | 28 +++ docs/reports/t27b_expectations.json | 1 + .../src/padding_class_downgrade_guard.t27 | 229 ++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_downgrade_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_downgrade_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_downgrade_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_downgrade_guard.json new file mode 100644 index 0000000000..b470166d0f --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_downgrade_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:6107e045b4377de051491e22e7ef5a2231cc3ee1b63a444ff795c2beda87134e", + "gen_hash_rust": "sha256:e59f03fce02f279364e8635209f9ad4b58c834ccd96978da1a6044534213cb0e", + "gen_hash_verilog": "sha256:c40a564ebda4aa514a10f39939efc8559318736e86eb7d7b64a4bff117df60a8", + "gen_hash_zig": "sha256:662eefcc9a23b824c8f30159a77851312dc9fd46ac120b6cf2397cf4b880fc53", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_downgrade_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T15:10:50Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:73ee43b21aee93eebddb92ab7eb36b705bd382cc6c519a197926e5f0190a84a9", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_downgrade_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 2a27dbe775..3efb74c540 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -863,6 +863,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_ciphertext_length_consistency_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_collision_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_downgrade_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_oracle.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_selection_entropy_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_transition_audit.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_downgrade_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_downgrade_guard.t27 new file mode 100644 index 0000000000..dbee8220f7 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_class_downgrade_guard.t27 @@ -0,0 +1,229 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_class_downgrade_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_class_downgrade_guard.rs +// (CR-CHAT-04 - Padding class downgrade guard, Wave-53 Lane B, +// requirement R-CHAT-9). +// +// Within a session the padding class may only grow: a shrink +// from 4096 to 256 tells an observer the plaintext became +// shorter and leaks its content. The rules, in the original's +// order: the batch ceiling, the empty log (accepted), then per +// entry the canonical-class probe and the non-decreasing check +// over the class ladder, and after the whole log the +// first-is-smallest and final-is-largest checks. +// +// The original returns Result<(), PadDowngradeError>. No +// variant carries a payload, so the verdict is the code alone +// (owner decision D5): Ok first, then the six variants in the +// original's declaration order - ClassDowngraded, +// NonCanonicalClass, DuplicateEntry, TooManyTransitions, +// FirstNotSmallest, FinalNotLargest. +// +// The original's DuplicateEntry is declared but never answered: +// its BTreeSet inserts (index, class) pairs and the index is +// unique per entry, so the insert never collides - repeated +// classes pass (PCDG-08 exercises exactly that). The variant +// stays in the enum, declared and unreachable exactly as in the +// original, and the dead insert drops out of the port. The +// original's Option class index becomes the sentinel +// four: the canonical check runs before any index use. + +// Canonical padding classes. +pub const PCDG_CLASSES: [4]usize = [4]usize{256, 1024, 4096, 16384,}; + +// Maximum transitions in a log. +pub const PCDG_MAX_TRANSITIONS: usize = 1024; + +// The sentinel class index for a non-canonical class. +pub const PCDG_NON_CANONICAL: usize = 4; + +// Verdict codes: Ok(()) and PadDowngradeError's variants, in declaration order. +pub enum PadDowngradeCode { + Ok, + ClassDowngraded, + NonCanonicalClass, + DuplicateEntry, + TooManyTransitions, + FirstNotSmallest, + FinalNotLargest, +} + +// The verdict of the downgrade validation. No variant of the +// original's error carries a payload, so the code is the whole +// verdict. +pub struct PadDowngradeVerdict { + code: PadDowngradeCode, +} + +// The ladder position of a class - the original's Option +// class_index, four marking a class the ladder does not hold. +pub fn class_index(cls: usize) -> usize { + if (cls == 256) { + return 0; + } + if (cls == 1024) { + return 1; + } + if (cls == 4096) { + return 2; + } + if (cls == 16384) { + return 3; + } + return PCDG_NON_CANONICAL; +} + +// [VERIFIED] Validate a sequence of padding class transitions. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, the empty log, then per +// entry the canonical class and the non-decreasing ladder walk, +// then the first-is-smallest and final-is-largest checks. +pub fn validate_padding_downgrade(transitions: []const usize) -> PadDowngradeVerdict { + if (transitions.len > PCDG_MAX_TRANSITIONS) { + return PadDowngradeVerdict { code: PadDowngradeCode.TooManyTransitions, }; + } + if (transitions.len == 0) { + return PadDowngradeVerdict { code: PadDowngradeCode.Ok, }; + } + var idx: usize = 0; + var prev_idx: usize = 0; + var first_idx: usize = 0; + for i in 0..transitions.len { + idx = class_index(transitions[i]); + if (idx == PCDG_NON_CANONICAL) { + return PadDowngradeVerdict { code: PadDowngradeCode.NonCanonicalClass, }; + } + if (i == 0) { + first_idx = idx; + } else { + if (idx < prev_idx) { + return PadDowngradeVerdict { code: PadDowngradeCode.ClassDowngraded, }; + } + } + prev_idx = idx; + } + if (first_idx != 0) { + return PadDowngradeVerdict { code: PadDowngradeCode.FirstNotSmallest, }; + } + if (idx != 3) { + return PadDowngradeVerdict { code: PadDowngradeCode.FinalNotLargest, }; + } + return PadDowngradeVerdict { code: PadDowngradeCode.Ok, }; +} + +// **PCDG-01** - a downgrade rejected: 256 after 1024. +test pcdg_01_downgrade_rejected { + const transitions: [3]usize = [3]usize{256, 1024, 256,}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.ClassDowngraded); +} + +// **PCDG-02** - a non-canonical class rejected: 512 is on no +// ladder rung. +test pcdg_02_non_canonical_rejected { + const transitions: [1]usize = [1]usize{512,}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.NonCanonicalClass); +} + +// **PCDG-03** - too many rejected: 1025 transitions of a maximum +// 1024, built in a loop like the original's. The batch rule +// fires before any entry is examined. +test pcdg_03_too_many_rejected { + var transitions: [1025]usize; + for i in 0..1025 { + transitions[i] = 256; + } + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.TooManyTransitions); +} + +// **PCDG-04** - first not smallest rejected: the log opens on +// 1024. +test pcdg_04_first_not_smallest_rejected { + const transitions: [3]usize = [3]usize{1024, 4096, 16384,}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.FirstNotSmallest); +} + +// **PCDG-05** - final not largest rejected: the log ends on +// 1024. +test pcdg_05_final_not_largest_rejected { + const transitions: [2]usize = [2]usize{256, 1024,}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.FinalNotLargest); +} + +// **PCDG-06** - the full progression accepted: every rung once, +// ascending. +test pcdg_06_full_progression_accepted { + const transitions: [4]usize = [4]usize{256, 1024, 4096, 16384,}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.Ok); +} + +// **PCDG-07** - an empty log accepted. +test pcdg_07_empty_accepted { + const transitions: [0]usize = [0]usize{}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.Ok); +} + +// **PCDG-08** - repeated classes accepted: the original's dedup +// keys on the entry index, so a repeated class never collides - +// the unreachable-duplicate property this port keeps. +test pcdg_08_same_class_accepted { + const transitions: [4]usize = [4]usize{256, 256, 256, 16384,}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.Ok); +} + +// **PCDG-09** - a skipped rung accepted: 256 straight to 16384. +test pcdg_09_skip_class_accepted { + const transitions: [2]usize = [2]usize{256, 16384,}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.Ok); +} + +// **PCDG-10** - the canonical ladder itself accepted. +test pcdg_10_minimal_accepted { + const transitions: [4]usize = [4]usize{256, 1024, 4096, 16384,}; + const v = validate_padding_downgrade(&transitions); + assert(v.code == PadDowngradeCode.Ok); +} + +// The helper has a direct test (coverage: Untested 0). +test class_index_answers { + assert(class_index(256) == 0); + assert(class_index(1024) == 1); + assert(class_index(4096) == 2); + assert(class_index(16384) == 3); + assert(class_index(512) == 4); + assert(class_index(0) == 4); + assert(class_index(16385) == 4); +} + +// Rule order is the original's: the batch ceiling before any +// entry, the canonical probe before the ladder walk, the ladder +// walk before the post-loop checks, and the first check before +// the final check. +test rule_order_matches_original { + var flooded: [1025]usize; + for i in 0..1025 { + flooded[i] = 512; + } + const v1 = validate_padding_downgrade(&flooded); + assert(v1.code == PadDowngradeCode.TooManyTransitions); + + const non_canonical: [2]usize = [2]usize{512, 256,}; + const v2 = validate_padding_downgrade(&non_canonical); + assert(v2.code == PadDowngradeCode.NonCanonicalClass); + + const downgraded_first_bad: [3]usize = [3]usize{1024, 4096, 256,}; + const v3 = validate_padding_downgrade(&downgraded_first_bad); + assert(v3.code == PadDowngradeCode.ClassDowngraded); + + const both_post_bad: [2]usize = [2]usize{1024, 1024,}; + const v4 = validate_padding_downgrade(&both_post_bad); + assert(v4.code == PadDowngradeCode.FirstNotSmallest); +} From c47fff0cd81d9b9a29a27e3e32921d85cceb4736 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:42:47 +0700 Subject: [PATCH 14/26] spec(port): port the AEAD/MAC tag truncation defence (Refs #8884) Landed from #9794 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...ings::CR-CHAT-04::src::mac_truncation.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../rings/CR-CHAT-04/src/mac_truncation.t27 | 299 ++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::mac_truncation.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/mac_truncation.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::mac_truncation.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::mac_truncation.json new file mode 100644 index 0000000000..d8cd2edafb --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::mac_truncation.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:2472637916229b9eb8b7084f360f2b0cfa1494161222a8f6c8ef50b8051c0e8f", + "gen_hash_rust": "sha256:80c92ce669dfc4a98d756a7220754de457336f7e1d7e144ebf267ac6fa16910f", + "gen_hash_verilog": "sha256:59d88717eef90fe5a44694b3b7e2e561e60536e9e1e143415075e71da53f3fad", + "gen_hash_zig": "sha256:153eff87ecaaf3e11d381d86eec93efbb4f6f9c11debbfb3406aa360b6f28924", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::mac_truncation", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T00:28:07Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:9c3547e53c1da0f70ee74ebb609c0027d6731529b0e15e8df880f8cc9ab3f0b9", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/mac_truncation.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 11, + "total": 11, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 3efb74c540..5522d642ff 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -858,6 +858,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/ratchet_tree_update_integrity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-03/src/unmerged_leaves_bound_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/header_extension_order_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/mac_truncation.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_alignment_boundary_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_block_size_alignment_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_byte_entropy_guard.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/mac_truncation.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/mac_truncation.t27 new file mode 100644 index 0000000000..85d217e931 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/mac_truncation.t27 @@ -0,0 +1,299 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::mac_truncation; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/mac_truncation.rs +// (CR-CHAT-04 - AEAD/MAC tag truncation defence, Wave-22 Lane B, +// L-CHAT-9-mt, requirement R-CHAT-4). +// +// Every encrypted chat frame ends with a sixteen-byte AEAD tag. +// The truncation family of attacks strips trailing tag bytes - +// a verifier comparing only the bytes that arrived forgives with +// probability 2^-8(16-k) instead of 2^-128, and a two-byte strip +// lands the forgery inside an online brute-force budget. The +// defence: sixteen is the only accepted tag length, the length +// is checked before any byte is compared, the compare reads all +// sixteen bytes with no early exit, and the frame split treats +// the trailer as its own domain. +// +// The original returns Result with four variants +// carrying no payloads. This port carries every outcome as one +// named struct (owner decision D5): Ok first, then the variants +// in the original's declaration order - Truncated, Oversized, +// Mismatch, FrameTooShort - plus the fields the accepted forms +// deliver: the constructed or split tag bytes and the split +// payload length (the original's returned slice becomes its +// length; the bytes live in the caller's frame). +// +// The original's subtle::ConstantTimeEq fold becomes a sum of +// absolute byte differences over all sixteen positions - every +// byte is read, no early exit, the sum is zero exactly when the +// tags match. The t27 backends carry no bitwise primitives, and +// the sum-zero equivalence is exact. The original's MacTag +// newtype becomes a record holding the fixed [16]u8 form. + +// The only accepted MAC tag length, in bytes. +pub const MAC_TAG_LEN: usize = 16; + +// A strongly-typed 16-byte MAC tag. +pub struct MacTag { + bytes: [16]u8, +} + +// Outcome codes: Ok and MacError's variants, in declaration order. +pub enum MacCode { + Ok, + Truncated, + Oversized, + Mismatch, + FrameTooShort, +} + +// The outcome of a tag construction, verification or frame split: +// the code, the tag the accepted forms deliver, and the split +// payload length. +pub struct MacOutcome { + code: MacCode, + payload_len: usize, + tag: [16]u8, +} + +// Constant-time equality over the fixed sixteen tag bytes: the +// absolute difference of every position accumulates, and the sum +// is zero exactly when the tags match - all bytes read, no +// early exit. +pub fn ct_eq(a: MacTag, b: MacTag) -> bool { + var da: usize = 0; + var db: usize = 0; + var diff: usize = 0; + for i in 0..16 { + da = @as(usize, a.bytes[i]); + db = @as(usize, b.bytes[i]); + if (da > db) { + diff = diff + (da - db); + } else { + diff = diff + (db - da); + } + } + return diff == 0; +} + +// Construct a tag from a slice: any length but sixteen answers +// Truncated or Oversized before a single byte is read, exactly +// the original's from_slice. +pub fn from_slice(bytes: []const u8) -> MacOutcome { + if (bytes.len < MAC_TAG_LEN) { + return MacOutcome { code: MacCode.Truncated, payload_len: 0, tag: ZERO_TAG, }; + } + if (bytes.len > MAC_TAG_LEN) { + return MacOutcome { code: MacCode.Oversized, payload_len: 0, tag: ZERO_TAG, }; + } + var tag: [16]u8 = ZERO_TAG; + for i in 0..16 { + tag[i] = bytes[i]; + } + return MacOutcome { code: MacCode.Ok, payload_len: 0, tag: tag, }; +} + +// Verify an arrived tag against the expected one: the length is +// checked first - the arrived length is the framing's public +// quantity - and only a full-length arrival reaches the +// sixteen-byte compare, which leaks no differing position. +pub fn verify_mac(expected: MacTag, arrived: []const u8) -> MacOutcome { + if (arrived.len < MAC_TAG_LEN) { + return MacOutcome { code: MacCode.Truncated, payload_len: 0, tag: ZERO_TAG, }; + } + if (arrived.len > MAC_TAG_LEN) { + return MacOutcome { code: MacCode.Oversized, payload_len: 0, tag: ZERO_TAG, }; + } + var arrived_tag: [16]u8 = ZERO_TAG; + for i in 0..16 { + arrived_tag[i] = arrived[i]; + } + if (ct_eq(expected, MacTag { bytes: arrived_tag, })) { + return MacOutcome { code: MacCode.Ok, payload_len: 0, tag: arrived_tag, }; + } + return MacOutcome { code: MacCode.Mismatch, payload_len: 0, tag: ZERO_TAG, }; +} + +// Split a wire frame into its payload length and its trailing +// tag. A frame shorter than the tag cannot hold one; the split +// reads the last sixteen bytes as the tag's own domain. +pub fn split_frame(frame: []const u8) -> MacOutcome { + if (frame.len < MAC_TAG_LEN) { + return MacOutcome { code: MacCode.FrameTooShort, payload_len: 0, tag: ZERO_TAG, }; + } + const cut: usize = frame.len - MAC_TAG_LEN; + var tag: [16]u8 = ZERO_TAG; + for j in 0..16 { + tag[j] = frame[cut + j]; + } + return MacOutcome { code: MacCode.Ok, payload_len: cut, tag: tag, }; +} + +// The all-zero tag - the empty form of the fixed sixteen bytes. +pub const ZERO_TAG: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The canonical tag of the fixtures, byte by byte. +pub const CANONICAL_BYTES: [16]u8 = [16]u8{1, 35, 69, 103, 137, 171, 205, 239, 254, 220, 186, 152, 118, 84, 50, 16,}; + +// The canonical tag with its eighth byte flipped by one - the +// single-byte mutation of MT-05. +pub const MUTATED_BYTES: [16]u8 = [16]u8{1, 35, 69, 103, 137, 171, 204, 239, 254, 220, 186, 152, 118, 84, 50, 16,}; + +// The canonical tag with the top bit of its last byte flipped - +// the single-bit flip of MT-09. +pub const FLIPPED_BYTES: [16]u8 = [16]u8{1, 35, 69, 103, 137, 171, 205, 239, 254, 220, 186, 152, 118, 84, 50, 144,}; + +// The first fifteen canonical bytes - the one-byte strip. +pub const SHORT_15: [15]u8 = [15]u8{1, 35, 69, 103, 137, 171, 205, 239, 254, 220, 186, 152, 118, 84, 50,}; + +// The first fourteen canonical bytes - the two-byte strip of the +// 2^-16 brute-force regime. +pub const SHORT_14: [14]u8 = [14]u8{1, 35, 69, 103, 137, 171, 205, 239, 254, 220, 186, 152, 118, 84,}; + +// The canonical tag plus one trailing byte - the length-extension +// attack of MT-04. +pub const LONG_17: [17]u8 = [17]u8{1, 35, 69, 103, 137, 171, 205, 239, 254, 220, 186, 152, 118, 84, 50, 16, 170,}; + +// **MT-01** - identical full-length tags accepted. +test mt_01_full_match_accepted { + const expected: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + const v = verify_mac(expected, &CANONICAL_BYTES); + assert(v.code == MacCode.Ok); +} + +// **MT-02** - a single trailing byte stripped rejected as +// Truncated - even though every arrived byte matches. +test mt_02_strip_one_byte_rejected { + const expected: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + const v = verify_mac(expected, &SHORT_15); + assert(v.code == MacCode.Truncated); +} + +// **MT-03** - a two-byte truncation rejected - the 2^-16 regime +// never reached. +test mt_03_strip_two_bytes_rejected { + const expected: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + const v = verify_mac(expected, &SHORT_14); + assert(v.code == MacCode.Truncated); +} + +// **MT-04** - a length-extension (oversized) tag rejected as +// Oversized. +test mt_04_oversized_rejected { + const expected: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + const v = verify_mac(expected, &LONG_17); + assert(v.code == MacCode.Oversized); +} + +// **MT-05** - a single-byte mutation at full length rejected as +// Mismatch - the eighth byte carries 0xCC where 0xCD belongs. +test mt_05_mismatch_rejected { + const expected: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + const v = verify_mac(expected, &MUTATED_BYTES); + assert(v.code == MacCode.Mismatch); +} + +// **MT-06** - an empty tag rejected at construction - the +// degenerate no-tag-at-all case. +test mt_06_empty_tag_rejected_at_construction { + const empty: [0]u8 = [0]u8{}; + const v = from_slice(&empty); + assert(v.code == MacCode.Truncated); +} + +// **MT-07** - split_frame of a 32-byte frame: sixteen payload +// bytes and the trailing canonical tag. +test mt_07_split_frame_canonical { + var frame: [32]u8; + for i in 0..16 { + frame[i] = 0; + } + for j in 0..16 { + frame[16 + j] = CANONICAL_BYTES[j]; + } + const v = split_frame(&frame); + assert(v.code == MacCode.Ok); + assert(v.payload_len == 16); + const got: MacTag = MacTag { bytes: v.tag, }; + const want: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + assert(ct_eq(got, want)); +} + +// **MT-08** - split_frame of a frame shorter than the tag +// rejected as FrameTooShort. +test mt_08_split_frame_too_short { + var frame: [15]u8; + for i in 0..15 { + frame[i] = 0; + } + const v = split_frame(&frame); + assert(v.code == MacCode.FrameTooShort); +} + +// **MT-09** - constant-time equality: one on the identical pair, +// zero on a single flipped bit. +test mt_09_ct_eq_single_bit_flip { + const t: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + const other: MacTag = MacTag { bytes: FLIPPED_BYTES, }; + assert(ct_eq(t, t)); + assert(!ct_eq(t, other)); +} + +// **MT-10** - end-to-end: build a frame, split it, verify the tag +// against the canonical expected - the payload's spot bytes +// confirm the split domain. +test mt_10_e2e_split_then_verify { + var frame: [32]u8; + frame[0] = 104; + frame[1] = 101; + frame[2] = 108; + frame[3] = 108; + frame[4] = 111; + frame[5] = 45; + frame[6] = 116; + frame[7] = 114; + frame[8] = 105; + frame[9] = 110; + frame[10] = 105; + frame[11] = 116; + frame[12] = 121; + frame[13] = 45; + frame[14] = 50; + frame[15] = 50; + for j in 0..16 { + frame[16 + j] = CANONICAL_BYTES[j]; + } + const v = split_frame(&frame); + assert(v.code == MacCode.Ok); + assert(v.payload_len == 16); + assert(frame[0] == 104); + assert(frame[15] == 50); + const expected: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + const arrived: MacTag = MacTag { bytes: v.tag, }; + const check = verify_mac(expected, &arrived.bytes); + assert(check.code == MacCode.Ok); +} + +// Rule order is the original's: the length checks before any +// byte compare (a short arrival whose bytes all match still +// answers Truncated), the frame floor before the split, and the +// split of an exactly-sixteen-byte frame yielding an empty +// payload with the whole frame as the tag. +test rule_order_matches_original { + const expected: MacTag = MacTag { bytes: CANONICAL_BYTES, }; + const short_matching = verify_mac(expected, &SHORT_15); + assert(short_matching.code == MacCode.Truncated); + + const oversized = from_slice(&LONG_17); + assert(oversized.code == MacCode.Oversized); + + var bare_tag_frame: [16]u8; + for j in 0..16 { + bare_tag_frame[j] = CANONICAL_BYTES[j]; + } + const bare = split_frame(&bare_tag_frame); + assert(bare.code == MacCode.Ok); + assert(bare.payload_len == 0); + const bare_tag: MacTag = MacTag { bytes: bare.tag, }; + assert(ct_eq(bare_tag, expected)); +} From 451db20f3f775b83c69947ae1aa3a18a8da5cd06 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:42:53 +0700 Subject: [PATCH 15/26] spec(port): port the safety-number OOB identity verification (Refs #8884) Landed from #9802 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...rings::CR-CHAT-04::src::safety_number.json | 28 +++ docs/reports/t27b_expectations.json | 1 + .../rings/CR-CHAT-04/src/safety_number.t27 | 237 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::safety_number.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/safety_number.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::safety_number.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::safety_number.json new file mode 100644 index 0000000000..0ff5472bd1 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::safety_number.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:0e3eca72ecc964fd826cf384ca27e651feffe7eff2255db57fb64407407009a3", + "gen_hash_rust": "sha256:523935ae1031e20735ae7a9fa62057f0636d671c08364042207c7c4b763b6cd2", + "gen_hash_verilog": "sha256:50564f05903758c6eeb492013799c727638cf9d9e04223580ee8ff6dfd71ae3b", + "gen_hash_zig": "sha256:c2af7ff8deacc4587f29a34a99dc917be3a3c95e302df43d262f9c822ed48f40", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::safety_number", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T00:37:25Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:c7b566d21e71d132f35cdb0a9a792aca02886240189cc8d5c54cbb6cd45407a8", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/safety_number.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 6, + "total": 6, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 5522d642ff..93ea95ba7e 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -873,6 +873,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_class_transition_uniformity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_length_distribution_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_oracle_timing_sidechannel.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/safety_number.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/safety_number.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/safety_number.t27 new file mode 100644 index 0000000000..040773e087 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/safety_number.t27 @@ -0,0 +1,237 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::safety_number; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/safety_number.rs +// (CR-CHAT-04 - Safety-number / OOB identity verification, +// Wave-14 Lane A, L-CHAT-2-oob, requirement R-CHAT-4). +// +// A MITM owning the routing layer swaps one party's identity key +// during the handshake and all in-band traffic then authenticates +// under the swapped key. The safety number is the out-of-band +// fingerprint: a deterministic, commutative digest of both +// identity keys, displayed to the user and compared OOB - the +// bytewise-smaller key hashes first, so both parties compute the +// same value off either argument order. +// +// The module's logic ports whole; the hash does not. t27 carries +// no bitwise primitives, so SHA-256 stays with the caller: the +// spec's safety_number takes both hash orderings and selects the +// one its canonical key ordering dictates - commutativity is the +// ordering logic's own property and the port proves it. The +// render re-hash likewise arrives as a caller-computed 32-byte +// block; the display arithmetic is the module's own and ports +// exactly - the original's (lo << 8) | hi | (tail << 16) ORs +// disjoint byte positions, so the t27 form is the equal sum +// lo * 256 + hi + tail * 65536. +// +// SNV-03 and SNV-06 of the original pin SHA-256's collision +// resistance - a property of the hash function, not of this +// module's logic: a swapped key or a flipped bit changes the +// digest because the caller's hash says so. They stay with the +// caller's SHA-256 and are not carried into the spec; the +// port tests the ordering, the selection, the display +// arithmetic and the constant-time compare. +// +// The original's verify returns Result with an Invariant carrying +// the "safety_number_mismatch" message; the port's verdict is +// the code alone (the lane convention - the message is opaque). +// The constant-time compare folds the absolute difference of +// all thirty bytes - every byte read, no early exit. + +// Identity key length (Ed25519 / X25519 compatible). +pub const SN_ID_KEY_LEN: usize = 32; + +// Safety-number digest length: the first thirty bytes of the hash. +pub const SN_DIGEST_LEN: usize = 30; + +// Display groups per digest. +pub const SN_GROUPS: usize = 12; + +// The modulus of one display group: five decimal digits. +pub const SN_GROUP_MOD: u32 = 100000; + +// Verdict codes of the out-of-band compare. +pub enum VerifyCode { + Ok, + Mismatch, +} + +// The verdict of the safety-number compare. The original's error +// carries only an opaque message, so the code is the whole +// verdict. +pub struct VerifyVerdict { + code: VerifyCode, +} + +// Bytewise lexicographic a <= b: the first differing byte +// decides, equal keys compare true. +pub fn key_lte(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + if (a[i] < b[i]) { + return true; + } + return false; + } + } + return true; +} + +// The canonical safety-number digest for two identity keys: the +// bytewise-smaller key hashes first, so the caller's two hash +// orderings reduce to one selection - and either argument order +// selects the same digest. +pub fn safety_number(a: [32]u8, b: [32]u8, hash_ab: [30]u8, hash_ba: [30]u8) -> [30]u8 { + if (key_lte(a, b)) { + return hash_ab; + } + return hash_ba; +} + +// Render the display groups off the caller's re-hash of the +// digest: twelve groups, each (lo * 256 + hi + tail * 65536) +// modulo a hundred thousand - the original's shift-and-OR over +// disjoint byte positions as the equal sum. +pub fn render(r: [32]u8) -> [12]u32 { + var groups: [12]u32 = [12]u32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + var lo: u32 = 0; + var hi: u32 = 0; + var tail: u32 = 0; + for i in 0..12 { + lo = @as(u32, r[2 * i]); + hi = @as(u32, r[2 * i + 1]); + tail = @as(u32, r[24 + (i % 8)]); + groups[i] = (lo * 256 + hi + tail * 65536) % SN_GROUP_MOD; + } + return groups; +} + +// Compare a received safety number against the locally computed +// one in constant time: the absolute difference of every digest +// byte accumulates, the sum zero exactly on a match. +pub fn verify(local: [30]u8, remote: [30]u8) -> VerifyVerdict { + var dl: usize = 0; + var dr: usize = 0; + var diff: usize = 0; + for i in 0..30 { + dl = @as(usize, local[i]); + dr = @as(usize, remote[i]); + if (dl > dr) { + diff = diff + (dl - dr); + } else { + diff = diff + (dr - dl); + } + } + if (diff == 0) { + return VerifyVerdict { code: VerifyCode.Ok, }; + } + return VerifyVerdict { code: VerifyCode.Mismatch, }; +} + +// The identity keys of the fixtures, one leading byte set - the +// original's k(seed) helper, entry by entry. +pub const KEY_01: [32]u8 = [32]u8{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; +pub const KEY_02: [32]u8 = [32]u8{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; +pub const KEY_10: [32]u8 = [32]u8{16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; +pub const KEY_20: [32]u8 = [32]u8{32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; +pub const KEY_30: [32]u8 = [32]u8{48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The caller's hash orderings of the fixtures - the digests the +// canonical ordering selects between. +pub const DIGEST_AB: [30]u8 = [30]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,}; +pub const DIGEST_BA: [30]u8 = [30]u8{30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,}; +pub const DIGEST_AC: [30]u8 = [30]u8{51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,}; + +// The caller's re-hash of the display fixture: the bytes 0..31. +pub const REHASH_32: [32]u8 = [32]u8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,}; + +// **SNV-01** - commutativity: either argument order selects the +// same digest, because the bytewise-smaller key hashes first. +test snv_01_commutative { + const ab = safety_number(KEY_01, KEY_02, DIGEST_AB, DIGEST_BA); + const ba = safety_number(KEY_02, KEY_01, DIGEST_BA, DIGEST_AB); + const equal = verify(ab, ba); + assert(equal.code == VerifyCode.Ok); +} + +// **SNV-02** - determinism and display: identical inputs render +// identically; the display carries twelve groups, each a +// five-digit value under a hundred thousand. +test snv_02_deterministic_display { + const g1 = render(REHASH_32); + const g2 = render(REHASH_32); + var same: bool = true; + for i in 0..12 { + if (g1[i] != g2[i]) { + same = false; + } + } + assert(same); + assert(g1[0] == 72865); + assert(g1[1] == 38915); + assert(g1[2] == 4965); + assert(g1[3] == 71015); + assert(g1[4] == 37065); + assert(g1[5] == 3115); + assert(g1[6] == 69165); + assert(g1[7] == 35215); + assert(g1[8] == 76977); + assert(g1[9] == 43027); + assert(g1[10] == 9077); + assert(g1[11] == 75127); + var bounded: bool = true; + for i in 0..12 { + if (g1[i] >= SN_GROUP_MOD) { + bounded = false; + } + } + assert(bounded); +} + +// **SNV-04** - verify accepts the swapped-order digest: both +// parties computed the same safety number off either side. +test snv_04_verify_accepts_match { + const local = safety_number(KEY_10, KEY_20, DIGEST_AB, DIGEST_BA); + const remote = safety_number(KEY_20, KEY_10, DIGEST_BA, DIGEST_AB); + const v = verify(local, remote); + assert(v.code == VerifyCode.Ok); +} + +// **SNV-05** - verify rejects a mismatched digest: the caller's +// hashes for a swapped key differ, and the compare answers +// Mismatch - the application must refuse plaintext until the +// human re-confirms out of band. +test snv_05_verify_rejects_mismatch { + const local = safety_number(KEY_10, KEY_20, DIGEST_AB, DIGEST_BA); + const remote = safety_number(KEY_10, KEY_30, DIGEST_AC, DIGEST_BA); + const v = verify(local, remote); + assert(v.code == VerifyCode.Mismatch); +} + +// The helper has direct tests (coverage: Untested 0). +test key_lte_answers { + assert(key_lte(KEY_01, KEY_02)); + assert(!key_lte(KEY_02, KEY_01)); + assert(key_lte(KEY_01, KEY_01)); + assert(!key_lte(KEY_20, KEY_10)); + assert(key_lte(KEY_10, KEY_20)); +} + +// The ordering boundary: equal keys select the first ordering +// both ways, and a differing deep byte decides the order. +test ordering_boundary_answers { + const both = safety_number(KEY_01, KEY_01, DIGEST_AB, DIGEST_BA); + const mirror = safety_number(KEY_01, KEY_01, DIGEST_BA, DIGEST_AB); + const v = verify(both, mirror); + assert(v.code == VerifyCode.Mismatch); + + var deep_a: [32]u8; + var deep_b: [32]u8; + for i in 0..32 { + deep_a[i] = 0; + deep_b[i] = 0; + } + deep_a[31] = 5; + deep_b[31] = 3; + assert(key_lte(deep_b, deep_a)); + assert(!key_lte(deep_a, deep_b)); +} From c3294a06c70852c2524586801a619a3feaa7e846 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:00 +0700 Subject: [PATCH 16/26] spec(port): port the cover-traffic burst detection guard (Refs #8887) Landed from #9805 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...7::src::cover_traffic_burst_detection.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/cover_traffic_burst_detection.t27 | 319 ++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_burst_detection.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_burst_detection.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_burst_detection.json new file mode 100644 index 0000000000..f39d894cec --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_burst_detection.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:74c446112a5b8e575c62af187e9dc98adec7297a149d70a6a693d351b4e6ab15", + "gen_hash_rust": "sha256:8ccf7865b9577af3638cf6b1c61fec1e8ae8e69338267477b208e9a95d2f4721", + "gen_hash_verilog": "sha256:9f9ac829b15868e13a49c9bea1f677a6cc8d67604094334440b6e64c9f5b20c6", + "gen_hash_zig": "sha256:2943bee494b137f745e72b60d3ed500d1ec31fdbbd0febe31492ccb80b9adf3c", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_burst_detection", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T00:53:04Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:4c73c0099ff3c9c5409de05d01f2e21e755e216de7ccde1567ae141b0059cdb3", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 93ea95ba7e..2e5dad884f 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -877,6 +877,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-05/src/envelope_size_distribution_uniformity.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_arrival_autocorrelation_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27 new file mode 100644 index 0000000000..6088733b11 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27 @@ -0,0 +1,319 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_burst_detection; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.rs +// (CR-CHAT-07 - Cover-traffic burst pattern detection, Wave-41 +// Lane B, R-CHAT-10). +// +// Fixed-size padding hides sizes and canonical gaps hide pacing, +// but bursts still read through: a cluster of emissions closer +// than the minimum canonical gap says the user is actively +// typing, and a long silence says idle - together they break +// the always-online, indistinguishable promise. The rules, in +// the original's fixed order: fewer than two records pass +// outright, then per consecutive gap the minimum floor, the +// canonical-gap membership and the silence ceiling, then per +// record the forward thirty-second window with its emission +// ceiling and its cover-ratio floor (the covers over the total +// at least one third, compared as the original's integer +// cross-multiplication). +// +// The original returns Result<(), BurstError> with five variants +// carrying no payloads; the verdict is the code alone (owner +// decision D5), Ok first, the variants in the original's +// declaration order. The original's super::CANONICAL_GAPS_MS +// becomes the ported ladder [1000, 5000, 30000, 300000] with +// the same membership-plus-open-top rule: a gap above the last +// rung counts as canonical, exactly as the original reads. The +// take_while window walk becomes a scanning flag that stops at +// the first record past the window's end; the saturating gap +// subtraction guards explicitly. + +// Minimum gap between consecutive emissions (ms). +pub const BURST_MIN_GAP_MS: u64 = 1000; + +// Maximum emissions in a burst window before flagging. +pub const BURST_MAX_EMISSIONS: usize = 10; + +// Burst window duration (ms). +pub const BURST_WINDOW_MS: u64 = 30000; + +// Maximum silence gap allowed (ms). +pub const BURST_MAX_SILENCE_MS: u64 = 300000; + +// Minimum cover-to-total ratio, numerator. +pub const BURST_MIN_COVER_NUM: u64 = 1; + +// Minimum cover-to-total ratio, denominator. +pub const BURST_MIN_COVER_DEN: u64 = 3; + +// The canonical gap ladder of the ring's scheduler, with its +// open top: a gap above the last rung counts as canonical. +pub const BURST_CANONICAL_GAPS_MS: [4]u64 = [4]u64{1000, 5000, 30000, 300000,}; + +// The kind of one emission on the wire. +pub enum EmissionKind { + Real, + Cover, +} + +// One recorded emission with its timestamp. +pub struct EmissionRecord { + timestamp_ms: u64, + kind: EmissionKind, +} + +// Verdict codes: Ok(()) and BurstError's variants, in declaration order. +pub enum BurstCode { + Ok, + GapBelowMinimum, + TooManyEmissions, + SilenceTooLong, + CoverRatioTooLow, + NonCanonicalGap, +} + +// The verdict of the burst-pattern validation. No variant of the +// original's error carries a payload, so the code is the whole +// verdict. +pub struct BurstVerdict { + code: BurstCode, +} + +// Whether the gap sits on the canonical ladder or above its +// last rung - the original's contains-or-over-the-top probe. +pub fn is_canonical_gap(gap: u64) -> bool { + if (gap == 1000) { + return true; + } + if (gap == 5000) { + return true; + } + if (gap == 30000) { + return true; + } + if (gap == 300000) { + return true; + } + if (gap > 300000) { + return true; + } + return false; +} + +// [VERIFIED] Validate a sequence of emission records against the +// burst-pattern rules. Returns Ok only when every rule holds, +// checked in the original's fixed order: fewer than two records +// pass, then per gap the floor, the canonical membership and the +// silence ceiling, then per record the forward window with its +// emission ceiling and cover-ratio floor. +pub fn validate_burst_pattern(records: []const EmissionRecord) -> BurstVerdict { + if (records.len < 2) { + return BurstVerdict { code: BurstCode.Ok, }; + } + var gap: u64 = 0; + for i in 1..records.len { + if (records[i].timestamp_ms >= records[i - 1].timestamp_ms) { + gap = records[i].timestamp_ms - records[i - 1].timestamp_ms; + } else { + gap = 0; + } + if (gap < BURST_MIN_GAP_MS) { + return BurstVerdict { code: BurstCode.GapBelowMinimum, }; + } + if (!is_canonical_gap(gap)) { + return BurstVerdict { code: BurstCode.NonCanonicalGap, }; + } + if (gap > BURST_MAX_SILENCE_MS) { + return BurstVerdict { code: BurstCode.SilenceTooLong, }; + } + } + for i in 0..records.len { + const window_end: u64 = records[i].timestamp_ms + BURST_WINDOW_MS; + var count: usize = 1; + var covers: usize = 0; + if (records[i].kind == EmissionKind.Cover) { + covers = 1; + } + var scanning: bool = true; + for j in 0..records.len { + if (j > i) { + if (scanning) { + if (records[j].timestamp_ms <= window_end) { + count = count + 1; + if (records[j].kind == EmissionKind.Cover) { + covers = covers + 1; + } + } else { + scanning = false; + } + } + } + } + if (count > BURST_MAX_EMISSIONS) { + return BurstVerdict { code: BurstCode.TooManyEmissions, }; + } + if (count >= 2) { + const ratio_num: u64 = @as(u64, covers) * BURST_MIN_COVER_DEN; + const ratio_den: u64 = @as(u64, count) * BURST_MIN_COVER_NUM; + if (ratio_num < ratio_den) { + return BurstVerdict { code: BurstCode.CoverRatioTooLow, }; + } + } + } + return BurstVerdict { code: BurstCode.Ok, }; +} + +// **BURST-01** - a five-hundred-millisecond gap rejected. +test burst_01_gap_below_minimum_rejected { + const records: [2]EmissionRecord = [2]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 500, kind: EmissionKind.Cover, }, + }; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.GapBelowMinimum); +} + +// **BURST-02** - a four-hundred-second silence rejected. +test burst_02_silence_too_long_rejected { + const records: [2]EmissionRecord = [2]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 400000, kind: EmissionKind.Cover, }, + }; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.SilenceTooLong); +} + +// **BURST-03** - fifteen emissions inside one window rejected: +// built on one-second gaps, alternating kinds. +test burst_03_too_many_emissions_rejected { + var records: [15]EmissionRecord; + for i in 0..15 { + var kind: EmissionKind = EmissionKind.Real; + if (i % 2 == 1) { + kind = EmissionKind.Cover; + } + records[i] = EmissionRecord { timestamp_ms: @as(u64, i) * 1000, kind: kind, }; + } + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.TooManyEmissions); +} + +// **BURST-04** - a fifteen-hundred-millisecond gap rejected: off +// every canonical rung. +test burst_04_non_canonical_gap_rejected { + const records: [2]EmissionRecord = [2]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 1500, kind: EmissionKind.Cover, }, + }; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.NonCanonicalGap); +} + +// **BURST-05** - three real emissions in a window rejected: no +// covers at all. +test burst_05_cover_ratio_too_low_rejected { + const records: [3]EmissionRecord = [3]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 5000, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 10000, kind: EmissionKind.Real, }, + }; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.CoverRatioTooLow); +} + +// **BURST-06** - a single record accepted without any check. +test burst_06_single_record_accepted { + const records: [1]EmissionRecord = [1]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + }; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.Ok); +} + +// **BURST-07** - an empty record set accepted. +test burst_07_empty_records_accepted { + const records: [0]EmissionRecord = [0]EmissionRecord{}; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.Ok); +} + +// **BURST-08** - the valid alternating pattern accepted: covers +// and reals on five-second gaps, every window holding at least a +// third of covers. +test burst_08_valid_alternating_accepted { + const records: [4]EmissionRecord = [4]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Cover, }, + EmissionRecord { timestamp_ms: 5000, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 10000, kind: EmissionKind.Cover, }, + EmissionRecord { timestamp_ms: 15000, kind: EmissionKind.Real, }, + }; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.Ok); +} + +// **BURST-09** - the top canonical rung accepted: a gap of +// exactly three hundred seconds. +test burst_09_largest_canonical_gap_accepted { + const records: [2]EmissionRecord = [2]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 300000, kind: EmissionKind.Cover, }, + }; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.Ok); +} + +// **BURST-10** - the exact minimum gap accepted: one second. +test burst_10_exact_min_gap_accepted { + const records: [2]EmissionRecord = [2]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Cover, }, + EmissionRecord { timestamp_ms: 1000, kind: EmissionKind.Real, }, + }; + const v = validate_burst_pattern(&records); + assert(v.code == BurstCode.Ok); +} + +// The helper has a direct test (coverage: Untested 0). +test is_canonical_gap_answers { + assert(is_canonical_gap(1000)); + assert(is_canonical_gap(5000)); + assert(is_canonical_gap(30000)); + assert(is_canonical_gap(300000)); + assert(is_canonical_gap(400000)); + assert(!is_canonical_gap(999)); + assert(!is_canonical_gap(1500)); + assert(!is_canonical_gap(0)); +} + +// Rule order is the original's: the gap floor before the +// canonical membership, the canonical membership before the +// silence ceiling, and inside a window the emission ceiling +// before the cover-ratio floor. +test rule_order_matches_original { + const tight = [2]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 500, kind: EmissionKind.Cover, }, + }; + const v1 = validate_burst_pattern(&tight); + assert(v1.code == BurstCode.GapBelowMinimum); + + const off_rung = [2]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 1500, kind: EmissionKind.Cover, }, + }; + const v2 = validate_burst_pattern(&off_rung); + assert(v2.code == BurstCode.NonCanonicalGap); + + const over_top = [2]EmissionRecord{ + EmissionRecord { timestamp_ms: 0, kind: EmissionKind.Real, }, + EmissionRecord { timestamp_ms: 350000, kind: EmissionKind.Cover, }, + }; + const v3 = validate_burst_pattern(&over_top); + assert(v3.code == BurstCode.SilenceTooLong); + + var crowded: [15]EmissionRecord; + for i in 0..15 { + crowded[i] = EmissionRecord { timestamp_ms: @as(u64, i) * 1000, kind: EmissionKind.Real, }; + } + const v4 = validate_burst_pattern(&crowded); + assert(v4.code == BurstCode.TooManyEmissions); +} From 2a24830943b25130daa815a1ca211a3972009ccc Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:07 +0700 Subject: [PATCH 17/26] spec(port): port the cover-traffic emission jitter guard (Refs #8887) Landed from #9810 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...::cover_traffic_emission_jitter_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../cover_traffic_emission_jitter_guard.t27 | 304 ++++++++++++++++++ 3 files changed, 333 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_jitter_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_jitter_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_jitter_guard.json new file mode 100644 index 0000000000..820e807ede --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_jitter_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:70dfc94a7069a5f6d3b3397031fb2472f863fb94fc9e66b2fe3d89bbabe49412", + "gen_hash_rust": "sha256:23c1015926bf8a897e12e435fe0bbc0438b1344bda7c6d6621997182e694c261", + "gen_hash_verilog": "sha256:c5831200d0515abb474940d8125cedaef748f1e1c0bf6f0ca0485c3ed6b8e324", + "gen_hash_zig": "sha256:fbeb22497ca104114b325b3f40b1ac8384d46b98b4b2ca50e86501a7980bb0ff", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_jitter_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T01:23:53Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:edf2d1756d1516e5ebe1132829bba1f68e1843beecaf497f6497b24d0596ac7d", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 2e5dad884f..7efe1fecfa 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -878,6 +878,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/blind_signature_sender_token.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_arrival_autocorrelation_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27 new file mode 100644 index 0000000000..76335322a0 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27 @@ -0,0 +1,304 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_jitter_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.rs +// (CR-CHAT-07 - Cover traffic emission jitter guard, Wave-89 +// Lane A, R-CHAT-10). +// +// Cover emissions follow a cadence, but without jitter the +// cadence reads as a clock signal, the observer predicts the +// next emission exactly and any deviation is a real message, +// and the scheduler itself becomes identifiable. The rules, in +// the original's order: the batch ceiling, then per emission +// the strictly-increasing actual timestamps, the zero-jitter +// probe, the jitter floor and the jitter ceiling, then over +// the whole batch the constant-jitter rejection - two or more +// emissions must not share one jitter value. +// +// The original returns Result<(), JitterError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six variants in the original's declaration +// order - JitterTooLow, JitterTooHigh, ZeroJitter, +// TooManyEmissions, ConstantJitter, TimestampsNotIncreasing - +// with every payload field: got and min or max for the bound +// rules, idx for the zero rule, and the shared value for the +// constant rule. +// +// The original's BTreeSet distinct-jitter probe becomes the +// scan: an emission's jitter is distinct when no earlier +// emission shares it; the constant rule fires when the batch +// holds at least two emissions and every jitter equals the +// first. The jitter is the absolute difference of the actual +// and scheduled times, guarded explicitly. + +// Minimum jitter (ms). +pub const CTEJ_MIN_JITTER_MS: u64 = 10; + +// Maximum jitter (ms). +pub const CTEJ_MAX_JITTER_MS: u64 = 5000; + +// Maximum emissions to validate. +pub const CTEJ_MAX_EMISSIONS: usize = 4096; + +// A cover traffic emission record. +pub struct CoverEmission { + scheduled_ms: u64, + actual_ms: u64, +} + +// Verdict codes: Ok(()) and JitterError's variants, in declaration order. +pub enum JitterCode { + Ok, + JitterTooLow, + JitterTooHigh, + ZeroJitter, + TooManyEmissions, + ConstantJitter, + TimestampsNotIncreasing, +} + +// The verdict of the jitter validation, with every error payload +// field. +pub struct JitterVerdict { + code: JitterCode, + idx: usize, + got: usize, + bound: usize, + got64: u64, + bound64: u64, +} + +// The absolute jitter of an emission - the original's +// jitter_ms method, the subtraction guarded explicitly. +pub fn jitter_ms(e: CoverEmission) -> u64 { + if (e.actual_ms >= e.scheduled_ms) { + return e.actual_ms - e.scheduled_ms; + } + return e.scheduled_ms - e.actual_ms; +} + +// Whether emission i shares its jitter with an earlier one - one +// half of the original's BTreeSet probe, answered by a scan. +pub fn jitter_seen_before(emissions: []const CoverEmission, i: usize) -> bool { + for j in 0..i { + if (jitter_ms(emissions[j]) == jitter_ms(emissions[i])) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate cover traffic emission jitter. Returns Ok +// only when every rule holds, checked in the original's order: +// the batch ceiling, then per emission the increasing +// timestamps, the zero jitter, the floor and the ceiling, then +// the constant-jitter rejection over the whole batch. +pub fn validate_cover_jitter(emissions: []const CoverEmission) -> JitterVerdict { + if (emissions.len > CTEJ_MAX_EMISSIONS) { + return JitterVerdict { code: JitterCode.TooManyEmissions, idx: 0, got: emissions.len, bound: CTEJ_MAX_EMISSIONS, got64: 0, bound64: 0, }; + } + for i in 0..emissions.len { + if (i > 0) { + if (emissions[i].actual_ms <= emissions[i - 1].actual_ms) { + return JitterVerdict { code: JitterCode.TimestampsNotIncreasing, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + } + const j: u64 = jitter_ms(emissions[i]); + if (j == 0) { + return JitterVerdict { code: JitterCode.ZeroJitter, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (j < CTEJ_MIN_JITTER_MS) { + return JitterVerdict { code: JitterCode.JitterTooLow, idx: i, got: 0, bound: 0, got64: j, bound64: CTEJ_MIN_JITTER_MS, }; + } + if (j > CTEJ_MAX_JITTER_MS) { + return JitterVerdict { code: JitterCode.JitterTooHigh, idx: i, got: 0, bound: 0, got64: j, bound64: CTEJ_MAX_JITTER_MS, }; + } + } + if (emissions.len >= 2) { + var all_same: bool = true; + const first: u64 = jitter_ms(emissions[0]); + for i in 1..emissions.len { + if (jitter_ms(emissions[i]) != first) { + all_same = false; + } + } + if (all_same) { + return JitterVerdict { code: JitterCode.ConstantJitter, idx: 0, got: 0, bound: 0, got64: first, bound64: 0, }; + } + } + return JitterVerdict { code: JitterCode.Ok, idx: 0, got: 0, bound: 0, got64: 0, bound64: 0, }; +} + +// The scheduled-vs-actual jitters of the too-many fixture, read +// by the loop instead of a computed expression: the jitter of +// record i is 100 + (i mod 50). +pub const CTEJ_JITTERS_4097: [4097]u64 = [4097]u64{100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146}; + +// **CTEJ-01** - jitter below the minimum rejected: five +// milliseconds of a required ten. +test ctej_01_jitter_too_low_rejected { + const es: [1]CoverEmission = [1]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1005, }, + }; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.JitterTooLow); + assert(v.got64 == 5); + assert(v.bound64 == 10); +} + +// **CTEJ-02** - jitter above the maximum rejected: nine thousand +// milliseconds of a ceiling five thousand. +test ctej_02_jitter_too_high_rejected { + const es: [1]CoverEmission = [1]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 10000, }, + }; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.JitterTooHigh); + assert(v.got64 == 9000); + assert(v.bound64 == 5000); +} + +// **CTEJ-03** - zero jitter rejected: the actual emission equals +// the schedule exactly. +test ctej_03_zero_jitter_rejected { + const es: [1]CoverEmission = [1]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1000, }, + }; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.ZeroJitter); + assert(v.idx == 0); +} + +// **CTEJ-04** - too many rejected: 4097 emissions of a maximum +// 4096, built in a loop like the original's, the scheduled times +// stepping by a second and the jitters walking 100 + (i mod 50). +// The batch rule fires before any emission is examined. +test ctej_04_too_many_rejected { + var es: [4097]CoverEmission; + for i in 0..4097 { + const sched: u64 = @as(u64, i) * 1000; + es[i] = CoverEmission { scheduled_ms: sched, actual_ms: sched + CTEJ_JITTERS_4097[i], }; + } + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.TooManyEmissions); + assert(v.got == 4097); + assert(v.bound == 4096); +} + +// **CTEJ-05** - constant jitter rejected: three emissions all +// carrying twenty milliseconds - the shared value rides the +// verdict. +test ctej_05_constant_jitter_rejected { + const es: [3]CoverEmission = [3]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1020, }, + CoverEmission { scheduled_ms: 2000, actual_ms: 2020, }, + CoverEmission { scheduled_ms: 3000, actual_ms: 3020, }, + }; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.ConstantJitter); + assert(v.got64 == 20); +} + +// **CTEJ-06** - timestamps not increasing rejected: the second +// emission lands before the first. +test ctej_06_timestamps_not_increasing_rejected { + const es: [2]CoverEmission = [2]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1050, }, + CoverEmission { scheduled_ms: 2000, actual_ms: 1040, }, + }; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.TimestampsNotIncreasing); + assert(v.idx == 1); +} + +// **CTEJ-07** - the valid batch accepted: jitters fifteen, +// thirty, twelve, fifty - all inside the band, all distinct. +test ctej_07_valid_accepted { + const es: [4]CoverEmission = [4]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1015, }, + CoverEmission { scheduled_ms: 2000, actual_ms: 2030, }, + CoverEmission { scheduled_ms: 3000, actual_ms: 3012, }, + CoverEmission { scheduled_ms: 4000, actual_ms: 4050, }, + }; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.Ok); +} + +// **CTEJ-08** - an empty batch accepted. +test ctej_08_empty_accepted { + const es: [0]CoverEmission = [0]CoverEmission{}; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.Ok); +} + +// **CTEJ-09** - a single emission accepted: the constant check +// needs two. +test ctej_09_single_accepted { + const es: [1]CoverEmission = [1]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1020, }, + }; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.Ok); +} + +// **CTEJ-10** - a negative jitter accepted: the actual emission +// falls a hundred milliseconds short of the schedule, the next +// seventy short - the absolute difference is what the band +// bounds. +test ctej_10_negative_jitter_accepted { + const es: [2]CoverEmission = [2]CoverEmission{ + CoverEmission { scheduled_ms: 1100, actual_ms: 1000, }, + CoverEmission { scheduled_ms: 2100, actual_ms: 2030, }, + }; + const v = validate_cover_jitter(&es); + assert(v.code == JitterCode.Ok); +} + +// The two helpers have direct tests (coverage: Untested 0). +test ctej_helpers_answer { + const early = CoverEmission { scheduled_ms: 1100, actual_ms: 1000, }; + const late = CoverEmission { scheduled_ms: 1000, actual_ms: 1050, }; + const on_time = CoverEmission { scheduled_ms: 1000, actual_ms: 1000, }; + assert(jitter_ms(late) == 50); + assert(jitter_ms(early) == 100); + assert(jitter_ms(on_time) == 0); + const pair: [2]CoverEmission = [2]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1020, }, + CoverEmission { scheduled_ms: 2000, actual_ms: 2020, }, + }; + assert(!jitter_seen_before(&pair, 0)); + assert(jitter_seen_before(&pair, 1)); +} + +// Rule order is the original's: the timestamps before the zero +// probe, the zero probe before the floor, the floor before the +// ceiling, and the per-emission rules before the batch +// constant-jitter rejection. +test rule_order_matches_original { + const backstep_zero: [2]CoverEmission = [2]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1020, }, + CoverEmission { scheduled_ms: 2000, actual_ms: 1020, }, + }; + const v1 = validate_cover_jitter(&backstep_zero); + assert(v1.code == JitterCode.TimestampsNotIncreasing); + assert(v1.idx == 1); + + const zero_low: [1]CoverEmission = [1]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1000, }, + }; + const v2 = validate_cover_jitter(&zero_low); + assert(v2.code == JitterCode.ZeroJitter); + + const low_high: [1]CoverEmission = [1]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 20000, }, + }; + const v3 = validate_cover_jitter(&low_high); + assert(v3.code == JitterCode.JitterTooHigh); + + const twins_constant: [2]CoverEmission = [2]CoverEmission{ + CoverEmission { scheduled_ms: 1000, actual_ms: 1020, }, + CoverEmission { scheduled_ms: 2000, actual_ms: 2020, }, + }; + const v4 = validate_cover_jitter(&twins_constant); + assert(v4.code == JitterCode.ConstantJitter); + assert(v4.got64 == 20); +} From 248d0759aa81badf1b76e2e7aa8220af6aa0fdd4 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:14 +0700 Subject: [PATCH 18/26] spec(port): port the cover-traffic emission pattern randomness guard (Refs #8887) Landed from #9811 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...fic_emission_pattern_randomness_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...ffic_emission_pattern_randomness_guard.t27 | 349 ++++++++++++++++++ 3 files changed, 378 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard.json new file mode 100644 index 0000000000..f23e72088c --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:f384e714b1f61a205e6e86b746a33b48bcaf20cca826e256aa1efb4d68eee37c", + "gen_hash_rust": "sha256:44c0930818553b0ef664d227740af5b2838f3e02439e39bd21bed8526e90dcbc", + "gen_hash_verilog": "sha256:053f4b1f3849ba4940c452ae750fffe435bf2840c4c039bb82fa5295d3b3878b", + "gen_hash_zig": "sha256:55dc70f098def20eca600da4f70b1867e99a17b9746af45089d10bdbf3ed1d18", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T01:35:29Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:dd90be7e7b346e8a203eaf692c85817774df85767b1af801b7f1500a28aa1a4d", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 11, + "total": 11, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 7efe1fecfa..4f87f493fc 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -879,6 +879,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_decoy_ratio_governor.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_arrival_autocorrelation_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27 new file mode 100644 index 0000000000..09110347e8 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27 @@ -0,0 +1,349 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.rs +// (CR-CHAT-07 - Cover traffic emission pattern randomness guard, +// Wave-129 Lane B, R-CHAT-10). +// +// Emission intervals must look random: a periodic pattern is +// trivially distinguished from real traffic, a repeating one +// falls to autocorrelation, and long same-side runs fail a +// Wald-Wolfowitz runs test. The rules, in the original's order: +// the batch ceiling, the interval floor, then per interval the +// zero probe and the single-interval ceiling, then over the +// whole batch the entropy floor and the runs floor (a quarter of +// the batch, at least two). +// +// The original returns Result<(), PatternRandomnessError>. This +// port carries the verdict as a named struct (owner decision D5): +// Ok first, then the six variants in the original's declaration +// order - LowEntropy, TooFew, ZeroInterval, IntervalTooLarge, +// TooFewRuns, TooMany - with every payload field: the measured +// and minimum entropy, got and min for the floor, idx with got +// and max for the ceiling rule, the counted and expected runs, +// and got with max for the batch bounds. +// +// The original's compute_entropy buckets the intervals by a +// float mean-and-deviation scale and folds a Shannon log2, and +// its count_runs compares against a float mean - the int-to- +// float casts and the log fold do not lower under the backends +// (the gen-rust @log emission is a zero-arity call, E0061). Per +// the lane convention for statistics over buffers (fill-byte +// randomness #9698, payload entropy #9747, cipher-mode entropy +// #9752), the entropy and the runs count arrive as numbers the +// caller computed over the intervals. The structural rules - +// the zero probe, the single-interval ceiling, the batch bounds +// - read the intervals directly and stay fully in the spec, and +// the runs floor compares the caller's count against the batch +// quarter, in the original's arithmetic. + +// Minimum entropy in bits per interval. +pub const CEPR_MIN_ENTROPY: f64 = 2.0; + +// Minimum intervals for statistical tests. +pub const CEPR_MIN_INTERVALS: usize = 8; + +// Maximum single interval in milliseconds. +pub const CEPR_MAX_INTERVAL: u64 = 300000; + +// Maximum intervals per batch. +pub const CEPR_MAX_INTERVALS: usize = 4096; + +// An inter-emission interval observation. +pub struct IntervalObservation { + interval_ms: u64, +} + +// Verdict codes: Ok(()) and PatternRandomnessError's variants, in declaration order. +pub enum PatternRandomnessCode { + Ok, + LowEntropy, + TooFew, + ZeroInterval, + IntervalTooLarge, + TooFewRuns, + TooMany, +} + +// The verdict of the pattern randomness validation, with every +// error payload field. +pub struct PatternRandomnessVerdict { + code: PatternRandomnessCode, + idx: usize, + got: usize, + bound: usize, + got_f: f64, + bound_f: f64, + got64: u64, + bound64: u64, +} + +// [VERIFIED] Validate cover traffic emission pattern +// randomness. Returns Ok only when every rule holds, checked in +// the original's order: the batch ceiling, the interval floor, +// then per interval the zero probe and the single-interval +// ceiling, then the entropy floor and the runs floor over the +// caller's statistics. +pub fn validate_pattern_randomness( + observations: []const IntervalObservation, + entropy: f64, + runs: usize, +) -> PatternRandomnessVerdict { + if (observations.len > CEPR_MAX_INTERVALS) { + return PatternRandomnessVerdict { code: PatternRandomnessCode.TooMany, idx: 0, got: observations.len, bound: CEPR_MAX_INTERVALS, got_f: 0.0, bound_f: 0.0, got64: 0, bound64: 0, }; + } + if (observations.len < CEPR_MIN_INTERVALS) { + return PatternRandomnessVerdict { code: PatternRandomnessCode.TooFew, idx: 0, got: observations.len, bound: CEPR_MIN_INTERVALS, got_f: 0.0, bound_f: 0.0, got64: 0, bound64: 0, }; + } + for i in 0..observations.len { + if (observations[i].interval_ms == 0) { + return PatternRandomnessVerdict { code: PatternRandomnessCode.ZeroInterval, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, got64: 0, bound64: 0, }; + } + if (observations[i].interval_ms > CEPR_MAX_INTERVAL) { + return PatternRandomnessVerdict { code: PatternRandomnessCode.IntervalTooLarge, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, got64: observations[i].interval_ms, bound64: CEPR_MAX_INTERVAL, }; + } + } + if (entropy < CEPR_MIN_ENTROPY) { + return PatternRandomnessVerdict { code: PatternRandomnessCode.LowEntropy, idx: 0, got: 0, bound: 0, got_f: entropy, bound_f: CEPR_MIN_ENTROPY, got64: 0, bound64: 0, }; + } + var expected_min: usize = observations.len / 4; + if (expected_min < 2) { + expected_min = 2; + } + if (runs < expected_min) { + return PatternRandomnessVerdict { code: PatternRandomnessCode.TooFewRuns, idx: 0, got: runs, bound: expected_min, got_f: 0.0, bound_f: 0.0, got64: 0, bound64: 0, }; + } + return PatternRandomnessVerdict { code: PatternRandomnessCode.Ok, idx: 0, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, got64: 0, bound64: 0, }; +} + +// The interval walks of the loop fixtures, precomputed tables: +// the too-many batch reads (i mod 3000) + 100, the large varied +// batch alternates 500 + (i * 17 mod 1000) with 2000 + (i * 31 +// mod 1000). +pub const CEPR_INTERVALS_4097: [4097]u64 = [4097]u64{100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168, 1169, 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1179, 1180, 1181, 1182, 1183, 1184, 1185, 1186, 1187, 1188, 1189, 1190, 1191, 1192, 1193, 1194, 1195, 1196, 1197, 1198, 1199, 1200, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1216, 1217, 1218, 1219, 1220, 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, 1230, 1231, 1232, 1233, 1234, 1235, 1236, 1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258, 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, 1278, 1279, 1280, 1281, 1282, 1283, 1284, 1285, 1286, 1287, 1288, 1289, 1290, 1291, 1292, 1293, 1294, 1295, 1296, 1297, 1298, 1299, 1300, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1311, 1312, 1313, 1314, 1315, 1316, 1317, 1318, 1319, 1320, 1321, 1322, 1323, 1324, 1325, 1326, 1327, 1328, 1329, 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337, 1338, 1339, 1340, 1341, 1342, 1343, 1344, 1345, 1346, 1347, 1348, 1349, 1350, 1351, 1352, 1353, 1354, 1355, 1356, 1357, 1358, 1359, 1360, 1361, 1362, 1363, 1364, 1365, 1366, 1367, 1368, 1369, 1370, 1371, 1372, 1373, 1374, 1375, 1376, 1377, 1378, 1379, 1380, 1381, 1382, 1383, 1384, 1385, 1386, 1387, 1388, 1389, 1390, 1391, 1392, 1393, 1394, 1395, 1396, 1397, 1398, 1399, 1400, 1401, 1402, 1403, 1404, 1405, 1406, 1407, 1408, 1409, 1410, 1411, 1412, 1413, 1414, 1415, 1416, 1417, 1418, 1419, 1420, 1421, 1422, 1423, 1424, 1425, 1426, 1427, 1428, 1429, 1430, 1431, 1432, 1433, 1434, 1435, 1436, 1437, 1438, 1439, 1440, 1441, 1442, 1443, 1444, 1445, 1446, 1447, 1448, 1449, 1450, 1451, 1452, 1453, 1454, 1455, 1456, 1457, 1458, 1459, 1460, 1461, 1462, 1463, 1464, 1465, 1466, 1467, 1468, 1469, 1470, 1471, 1472, 1473, 1474, 1475, 1476, 1477, 1478, 1479, 1480, 1481, 1482, 1483, 1484, 1485, 1486, 1487, 1488, 1489, 1490, 1491, 1492, 1493, 1494, 1495, 1496, 1497, 1498, 1499, 1500, 1501, 1502, 1503, 1504, 1505, 1506, 1507, 1508, 1509, 1510, 1511, 1512, 1513, 1514, 1515, 1516, 1517, 1518, 1519, 1520, 1521, 1522, 1523, 1524, 1525, 1526, 1527, 1528, 1529, 1530, 1531, 1532, 1533, 1534, 1535, 1536, 1537, 1538, 1539, 1540, 1541, 1542, 1543, 1544, 1545, 1546, 1547, 1548, 1549, 1550, 1551, 1552, 1553, 1554, 1555, 1556, 1557, 1558, 1559, 1560, 1561, 1562, 1563, 1564, 1565, 1566, 1567, 1568, 1569, 1570, 1571, 1572, 1573, 1574, 1575, 1576, 1577, 1578, 1579, 1580, 1581, 1582, 1583, 1584, 1585, 1586, 1587, 1588, 1589, 1590, 1591, 1592, 1593, 1594, 1595, 1596, 1597, 1598, 1599, 1600, 1601, 1602, 1603, 1604, 1605, 1606, 1607, 1608, 1609, 1610, 1611, 1612, 1613, 1614, 1615, 1616, 1617, 1618, 1619, 1620, 1621, 1622, 1623, 1624, 1625, 1626, 1627, 1628, 1629, 1630, 1631, 1632, 1633, 1634, 1635, 1636, 1637, 1638, 1639, 1640, 1641, 1642, 1643, 1644, 1645, 1646, 1647, 1648, 1649, 1650, 1651, 1652, 1653, 1654, 1655, 1656, 1657, 1658, 1659, 1660, 1661, 1662, 1663, 1664, 1665, 1666, 1667, 1668, 1669, 1670, 1671, 1672, 1673, 1674, 1675, 1676, 1677, 1678, 1679, 1680, 1681, 1682, 1683, 1684, 1685, 1686, 1687, 1688, 1689, 1690, 1691, 1692, 1693, 1694, 1695, 1696, 1697, 1698, 1699, 1700, 1701, 1702, 1703, 1704, 1705, 1706, 1707, 1708, 1709, 1710, 1711, 1712, 1713, 1714, 1715, 1716, 1717, 1718, 1719, 1720, 1721, 1722, 1723, 1724, 1725, 1726, 1727, 1728, 1729, 1730, 1731, 1732, 1733, 1734, 1735, 1736, 1737, 1738, 1739, 1740, 1741, 1742, 1743, 1744, 1745, 1746, 1747, 1748, 1749, 1750, 1751, 1752, 1753, 1754, 1755, 1756, 1757, 1758, 1759, 1760, 1761, 1762, 1763, 1764, 1765, 1766, 1767, 1768, 1769, 1770, 1771, 1772, 1773, 1774, 1775, 1776, 1777, 1778, 1779, 1780, 1781, 1782, 1783, 1784, 1785, 1786, 1787, 1788, 1789, 1790, 1791, 1792, 1793, 1794, 1795, 1796, 1797, 1798, 1799, 1800, 1801, 1802, 1803, 1804, 1805, 1806, 1807, 1808, 1809, 1810, 1811, 1812, 1813, 1814, 1815, 1816, 1817, 1818, 1819, 1820, 1821, 1822, 1823, 1824, 1825, 1826, 1827, 1828, 1829, 1830, 1831, 1832, 1833, 1834, 1835, 1836, 1837, 1838, 1839, 1840, 1841, 1842, 1843, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1851, 1852, 1853, 1854, 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1866, 1867, 1868, 1869, 1870, 1871, 1872, 1873, 1874, 1875, 1876, 1877, 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 1887, 1888, 1889, 1890, 1891, 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1901, 1902, 1903, 1904, 1905, 1906, 1907, 1908, 1909, 1910, 1911, 1912, 1913, 1914, 1915, 1916, 1917, 1918, 1919, 1920, 1921, 1922, 1923, 1924, 1925, 1926, 1927, 1928, 1929, 1930, 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1938, 1939, 1940, 1941, 1942, 1943, 1944, 1945, 1946, 1947, 1948, 1949, 1950, 1951, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1959, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2069, 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079, 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, 2100, 2101, 2102, 2103, 2104, 2105, 2106, 2107, 2108, 2109, 2110, 2111, 2112, 2113, 2114, 2115, 2116, 2117, 2118, 2119, 2120, 2121, 2122, 2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2131, 2132, 2133, 2134, 2135, 2136, 2137, 2138, 2139, 2140, 2141, 2142, 2143, 2144, 2145, 2146, 2147, 2148, 2149, 2150, 2151, 2152, 2153, 2154, 2155, 2156, 2157, 2158, 2159, 2160, 2161, 2162, 2163, 2164, 2165, 2166, 2167, 2168, 2169, 2170, 2171, 2172, 2173, 2174, 2175, 2176, 2177, 2178, 2179, 2180, 2181, 2182, 2183, 2184, 2185, 2186, 2187, 2188, 2189, 2190, 2191, 2192, 2193, 2194, 2195, 2196, 2197, 2198, 2199, 2200, 2201, 2202, 2203, 2204, 2205, 2206, 2207, 2208, 2209, 2210, 2211, 2212, 2213, 2214, 2215, 2216, 2217, 2218, 2219, 2220, 2221, 2222, 2223, 2224, 2225, 2226, 2227, 2228, 2229, 2230, 2231, 2232, 2233, 2234, 2235, 2236, 2237, 2238, 2239, 2240, 2241, 2242, 2243, 2244, 2245, 2246, 2247, 2248, 2249, 2250, 2251, 2252, 2253, 2254, 2255, 2256, 2257, 2258, 2259, 2260, 2261, 2262, 2263, 2264, 2265, 2266, 2267, 2268, 2269, 2270, 2271, 2272, 2273, 2274, 2275, 2276, 2277, 2278, 2279, 2280, 2281, 2282, 2283, 2284, 2285, 2286, 2287, 2288, 2289, 2290, 2291, 2292, 2293, 2294, 2295, 2296, 2297, 2298, 2299, 2300, 2301, 2302, 2303, 2304, 2305, 2306, 2307, 2308, 2309, 2310, 2311, 2312, 2313, 2314, 2315, 2316, 2317, 2318, 2319, 2320, 2321, 2322, 2323, 2324, 2325, 2326, 2327, 2328, 2329, 2330, 2331, 2332, 2333, 2334, 2335, 2336, 2337, 2338, 2339, 2340, 2341, 2342, 2343, 2344, 2345, 2346, 2347, 2348, 2349, 2350, 2351, 2352, 2353, 2354, 2355, 2356, 2357, 2358, 2359, 2360, 2361, 2362, 2363, 2364, 2365, 2366, 2367, 2368, 2369, 2370, 2371, 2372, 2373, 2374, 2375, 2376, 2377, 2378, 2379, 2380, 2381, 2382, 2383, 2384, 2385, 2386, 2387, 2388, 2389, 2390, 2391, 2392, 2393, 2394, 2395, 2396, 2397, 2398, 2399, 2400, 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, 2409, 2410, 2411, 2412, 2413, 2414, 2415, 2416, 2417, 2418, 2419, 2420, 2421, 2422, 2423, 2424, 2425, 2426, 2427, 2428, 2429, 2430, 2431, 2432, 2433, 2434, 2435, 2436, 2437, 2438, 2439, 2440, 2441, 2442, 2443, 2444, 2445, 2446, 2447, 2448, 2449, 2450, 2451, 2452, 2453, 2454, 2455, 2456, 2457, 2458, 2459, 2460, 2461, 2462, 2463, 2464, 2465, 2466, 2467, 2468, 2469, 2470, 2471, 2472, 2473, 2474, 2475, 2476, 2477, 2478, 2479, 2480, 2481, 2482, 2483, 2484, 2485, 2486, 2487, 2488, 2489, 2490, 2491, 2492, 2493, 2494, 2495, 2496, 2497, 2498, 2499, 2500, 2501, 2502, 2503, 2504, 2505, 2506, 2507, 2508, 2509, 2510, 2511, 2512, 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, 2521, 2522, 2523, 2524, 2525, 2526, 2527, 2528, 2529, 2530, 2531, 2532, 2533, 2534, 2535, 2536, 2537, 2538, 2539, 2540, 2541, 2542, 2543, 2544, 2545, 2546, 2547, 2548, 2549, 2550, 2551, 2552, 2553, 2554, 2555, 2556, 2557, 2558, 2559, 2560, 2561, 2562, 2563, 2564, 2565, 2566, 2567, 2568, 2569, 2570, 2571, 2572, 2573, 2574, 2575, 2576, 2577, 2578, 2579, 2580, 2581, 2582, 2583, 2584, 2585, 2586, 2587, 2588, 2589, 2590, 2591, 2592, 2593, 2594, 2595, 2596, 2597, 2598, 2599, 2600, 2601, 2602, 2603, 2604, 2605, 2606, 2607, 2608, 2609, 2610, 2611, 2612, 2613, 2614, 2615, 2616, 2617, 2618, 2619, 2620, 2621, 2622, 2623, 2624, 2625, 2626, 2627, 2628, 2629, 2630, 2631, 2632, 2633, 2634, 2635, 2636, 2637, 2638, 2639, 2640, 2641, 2642, 2643, 2644, 2645, 2646, 2647, 2648, 2649, 2650, 2651, 2652, 2653, 2654, 2655, 2656, 2657, 2658, 2659, 2660, 2661, 2662, 2663, 2664, 2665, 2666, 2667, 2668, 2669, 2670, 2671, 2672, 2673, 2674, 2675, 2676, 2677, 2678, 2679, 2680, 2681, 2682, 2683, 2684, 2685, 2686, 2687, 2688, 2689, 2690, 2691, 2692, 2693, 2694, 2695, 2696, 2697, 2698, 2699, 2700, 2701, 2702, 2703, 2704, 2705, 2706, 2707, 2708, 2709, 2710, 2711, 2712, 2713, 2714, 2715, 2716, 2717, 2718, 2719, 2720, 2721, 2722, 2723, 2724, 2725, 2726, 2727, 2728, 2729, 2730, 2731, 2732, 2733, 2734, 2735, 2736, 2737, 2738, 2739, 2740, 2741, 2742, 2743, 2744, 2745, 2746, 2747, 2748, 2749, 2750, 2751, 2752, 2753, 2754, 2755, 2756, 2757, 2758, 2759, 2760, 2761, 2762, 2763, 2764, 2765, 2766, 2767, 2768, 2769, 2770, 2771, 2772, 2773, 2774, 2775, 2776, 2777, 2778, 2779, 2780, 2781, 2782, 2783, 2784, 2785, 2786, 2787, 2788, 2789, 2790, 2791, 2792, 2793, 2794, 2795, 2796, 2797, 2798, 2799, 2800, 2801, 2802, 2803, 2804, 2805, 2806, 2807, 2808, 2809, 2810, 2811, 2812, 2813, 2814, 2815, 2816, 2817, 2818, 2819, 2820, 2821, 2822, 2823, 2824, 2825, 2826, 2827, 2828, 2829, 2830, 2831, 2832, 2833, 2834, 2835, 2836, 2837, 2838, 2839, 2840, 2841, 2842, 2843, 2844, 2845, 2846, 2847, 2848, 2849, 2850, 2851, 2852, 2853, 2854, 2855, 2856, 2857, 2858, 2859, 2860, 2861, 2862, 2863, 2864, 2865, 2866, 2867, 2868, 2869, 2870, 2871, 2872, 2873, 2874, 2875, 2876, 2877, 2878, 2879, 2880, 2881, 2882, 2883, 2884, 2885, 2886, 2887, 2888, 2889, 2890, 2891, 2892, 2893, 2894, 2895, 2896, 2897, 2898, 2899, 2900, 2901, 2902, 2903, 2904, 2905, 2906, 2907, 2908, 2909, 2910, 2911, 2912, 2913, 2914, 2915, 2916, 2917, 2918, 2919, 2920, 2921, 2922, 2923, 2924, 2925, 2926, 2927, 2928, 2929, 2930, 2931, 2932, 2933, 2934, 2935, 2936, 2937, 2938, 2939, 2940, 2941, 2942, 2943, 2944, 2945, 2946, 2947, 2948, 2949, 2950, 2951, 2952, 2953, 2954, 2955, 2956, 2957, 2958, 2959, 2960, 2961, 2962, 2963, 2964, 2965, 2966, 2967, 2968, 2969, 2970, 2971, 2972, 2973, 2974, 2975, 2976, 2977, 2978, 2979, 2980, 2981, 2982, 2983, 2984, 2985, 2986, 2987, 2988, 2989, 2990, 2991, 2992, 2993, 2994, 2995, 2996, 2997, 2998, 2999, 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031, 3032, 3033, 3034, 3035, 3036, 3037, 3038, 3039, 3040, 3041, 3042, 3043, 3044, 3045, 3046, 3047, 3048, 3049, 3050, 3051, 3052, 3053, 3054, 3055, 3056, 3057, 3058, 3059, 3060, 3061, 3062, 3063, 3064, 3065, 3066, 3067, 3068, 3069, 3070, 3071, 3072, 3073, 3074, 3075, 3076, 3077, 3078, 3079, 3080, 3081, 3082, 3083, 3084, 3085, 3086, 3087, 3088, 3089, 3090, 3091, 3092, 3093, 3094, 3095, 3096, 3097, 3098, 3099, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084, 1085, 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, 1166, 1167, 1168, 1169, 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1179, 1180, 1181, 1182, 1183, 1184, 1185, 1186, 1187, 1188, 1189, 1190, 1191, 1192, 1193, 1194, 1195, 1196}; + +pub const CEPR_INTERVALS_100: [100]u64 = [100]u64{500, 2031, 534, 2093, 568, 2155, 602, 2217, 636, 2279, 670, 2341, 704, 2403, 738, 2465, 772, 2527, 806, 2589, 840, 2651, 874, 2713, 908, 2775, 942, 2837, 976, 2899, 1010, 2961, 1044, 2023, 1078, 2085, 1112, 2147, 1146, 2209, 1180, 2271, 1214, 2333, 1248, 2395, 1282, 2457, 1316, 2519, 1350, 2581, 1384, 2643, 1418, 2705, 1452, 2767, 1486, 2829, 520, 2891, 554, 2953, 588, 2015, 622, 2077, 656, 2139, 690, 2201, 724, 2263, 758, 2325, 792, 2387, 826, 2449, 860, 2511, 894, 2573, 928, 2635, 962, 2697, 996, 2759, 1030, 2821, 1064, 2883, 1098, 2945, 1132, 2007, 1166, 2069}; + +// **CEPR-01** - low entropy rejected: eight identical intervals +// measure entropy zero against the floor of two bits. +test cepr_01_low_entropy_rejected { + var os: [8]IntervalObservation; + for i in 0..8 { + os[i] = IntervalObservation { interval_ms: 1000, }; + } + const v = validate_pattern_randomness(&os, 0.0, 1); + assert(v.code == PatternRandomnessCode.LowEntropy); + assert(v.got_f == 0.0); + assert(v.bound_f == 2.0); +} + +// **CEPR-02** - too few rejected: three intervals of a required +// eight. +test cepr_02_too_few_rejected { + const os: [3]IntervalObservation = [3]IntervalObservation{ + IntervalObservation { interval_ms: 1000, }, + IntervalObservation { interval_ms: 2000, }, + IntervalObservation { interval_ms: 1500, }, + }; + const v = validate_pattern_randomness(&os, 3.0, 3); + assert(v.code == PatternRandomnessCode.TooFew); + assert(v.got == 3); + assert(v.bound == 8); +} + +// **CEPR-03** - a zero interval rejected at the appended +// position. +test cepr_03_zero_interval_rejected { + const os: [9]IntervalObservation = [9]IntervalObservation{ + IntervalObservation { interval_ms: 1000, }, + IntervalObservation { interval_ms: 2500, }, + IntervalObservation { interval_ms: 800, }, + IntervalObservation { interval_ms: 3200, }, + IntervalObservation { interval_ms: 1500, }, + IntervalObservation { interval_ms: 500, }, + IntervalObservation { interval_ms: 2800, }, + IntervalObservation { interval_ms: 1200, }, + IntervalObservation { interval_ms: 0, }, + }; + const v = validate_pattern_randomness(&os, 3.0, 8); + assert(v.code == PatternRandomnessCode.ZeroInterval); + assert(v.idx == 8); +} + +// **CEPR-04** - a single interval over the ceiling rejected: +// 300001 milliseconds of a maximum 300000, at the appended +// position. +test cepr_04_interval_too_large_rejected { + const os: [9]IntervalObservation = [9]IntervalObservation{ + IntervalObservation { interval_ms: 1000, }, + IntervalObservation { interval_ms: 2500, }, + IntervalObservation { interval_ms: 800, }, + IntervalObservation { interval_ms: 3200, }, + IntervalObservation { interval_ms: 1500, }, + IntervalObservation { interval_ms: 500, }, + IntervalObservation { interval_ms: 2800, }, + IntervalObservation { interval_ms: 1200, }, + IntervalObservation { interval_ms: 300001, }, + }; + const v = validate_pattern_randomness(&os, 3.0, 8); + assert(v.code == PatternRandomnessCode.IntervalTooLarge); + assert(v.idx == 8); + assert(v.got64 == 300001); + assert(v.bound64 == 300000); +} + +// **CEPR-05** - the predictable pattern rejected: fifteen +// hundred-millisecond intervals then five five-second ones - +// entropy 0.81 under the floor, two runs of a required five. +// The original accepts either rejection; the port pins the +// caller's true measurements and the entropy floor answers +// first. +test cepr_05_too_few_runs_rejected { + var os: [20]IntervalObservation; + for i in 0..20 { + var iv: u64 = 100; + if (i >= 15) { + iv = 5000; + } + os[i] = IntervalObservation { interval_ms: iv, }; + } + const v = validate_pattern_randomness(&os, 0.8112781244591328, 2); + assert(v.code == PatternRandomnessCode.LowEntropy); +} + +// **CEPR-06** - too many rejected: 4097 intervals of a maximum +// 4096, built in a loop like the original's, the intervals +// walking (i mod 3000) + 100. The batch rule fires before any +// interval is examined. +test cepr_06_too_many_rejected { + var os: [4097]IntervalObservation; + for i in 0..4097 { + os[i] = IntervalObservation { interval_ms: CEPR_INTERVALS_4097[i], }; + } + const v = validate_pattern_randomness(&os, 6.0, 2000); + assert(v.code == PatternRandomnessCode.TooMany); + assert(v.got == 4097); + assert(v.bound == 4096); +} + +// **CEPR-07** - the varied batch accepted: entropy three bits, +// seven runs of a required two. +test cepr_07_varied_accepted { + const os: [8]IntervalObservation = [8]IntervalObservation{ + IntervalObservation { interval_ms: 1000, }, + IntervalObservation { interval_ms: 2500, }, + IntervalObservation { interval_ms: 800, }, + IntervalObservation { interval_ms: 3200, }, + IntervalObservation { interval_ms: 1500, }, + IntervalObservation { interval_ms: 500, }, + IntervalObservation { interval_ms: 2800, }, + IntervalObservation { interval_ms: 1200, }, + }; + const v = validate_pattern_randomness(&os, 3.0, 7); + assert(v.code == PatternRandomnessCode.Ok); +} + +// **CEPR-08** - the exact minimum accepted: eight intervals, +// entropy three bits, eight runs. +test cepr_08_exact_minimum_accepted { + const os: [8]IntervalObservation = [8]IntervalObservation{ + IntervalObservation { interval_ms: 500, }, + IntervalObservation { interval_ms: 3000, }, + IntervalObservation { interval_ms: 800, }, + IntervalObservation { interval_ms: 2500, }, + IntervalObservation { interval_ms: 1200, }, + IntervalObservation { interval_ms: 1800, }, + IntervalObservation { interval_ms: 600, }, + IntervalObservation { interval_ms: 2200, }, + }; + const v = validate_pattern_randomness(&os, 3.0, 8); + assert(v.code == PatternRandomnessCode.Ok); +} + +// **CEPR-09** - the boundary interval accepted: the varied batch +// plus exactly the 300000-millisecond ceiling - entropy 2.197 +// over the floor, two runs at the required two. +test cepr_09_boundary_interval_accepted { + const os: [9]IntervalObservation = [9]IntervalObservation{ + IntervalObservation { interval_ms: 1000, }, + IntervalObservation { interval_ms: 2500, }, + IntervalObservation { interval_ms: 800, }, + IntervalObservation { interval_ms: 3200, }, + IntervalObservation { interval_ms: 1500, }, + IntervalObservation { interval_ms: 500, }, + IntervalObservation { interval_ms: 2800, }, + IntervalObservation { interval_ms: 1200, }, + IntervalObservation { interval_ms: 300000, }, + }; + const v = validate_pattern_randomness(&os, 2.197159723424149, 2); + assert(v.code == PatternRandomnessCode.Ok); +} + +// **CEPR-10** - the large varied batch accepted: a hundred +// alternating intervals, entropy 6.12 bits, a hundred runs of a +// required twenty-five. +test cepr_10_large_batch_accepted { + var os: [100]IntervalObservation; + for i in 0..100 { + os[i] = IntervalObservation { interval_ms: CEPR_INTERVALS_100[i], }; + } + const v = validate_pattern_randomness(&os, 6.121209564709828, 100); + assert(v.code == PatternRandomnessCode.Ok); +} + +// Rule order is the original's: the interval floor before any +// record, the zero probe before the ceiling, the per-interval +// rules before the entropy floor, and the entropy floor before +// the runs floor. +test rule_order_matches_original { + const short_zero: [3]IntervalObservation = [3]IntervalObservation{ + IntervalObservation { interval_ms: 0, }, + IntervalObservation { interval_ms: 2000, }, + IntervalObservation { interval_ms: 1500, }, + }; + const v1 = validate_pattern_randomness(&short_zero, 3.0, 3); + assert(v1.code == PatternRandomnessCode.TooFew); + + const zero_huge: [8]IntervalObservation = [8]IntervalObservation{ + IntervalObservation { interval_ms: 1000, }, + IntervalObservation { interval_ms: 2500, }, + IntervalObservation { interval_ms: 800, }, + IntervalObservation { interval_ms: 3200, }, + IntervalObservation { interval_ms: 1500, }, + IntervalObservation { interval_ms: 500, }, + IntervalObservation { interval_ms: 2800, }, + IntervalObservation { interval_ms: 400000, }, + }; + const v2 = validate_pattern_randomness(&zero_huge, 3.0, 7); + assert(v2.code == PatternRandomnessCode.IntervalTooLarge); + assert(v2.idx == 7); + + var huge_low: [8]IntervalObservation; + for i in 0..8 { + huge_low[i] = IntervalObservation { interval_ms: 1000, }; + } + huge_low[7] = IntervalObservation { interval_ms: 400000, }; + const v3 = validate_pattern_randomness(&huge_low, 3.0, 2); + assert(v3.code == PatternRandomnessCode.IntervalTooLarge); + + var low_runs: [8]IntervalObservation; + for i in 0..8 { + low_runs[i] = IntervalObservation { interval_ms: 1000, }; + } + const v4 = validate_pattern_randomness(&low_runs, 1.5, 1); + assert(v4.code == PatternRandomnessCode.LowEntropy); + + const varied_runs: [8]IntervalObservation = [8]IntervalObservation{ + IntervalObservation { interval_ms: 1000, }, + IntervalObservation { interval_ms: 2500, }, + IntervalObservation { interval_ms: 800, }, + IntervalObservation { interval_ms: 3200, }, + IntervalObservation { interval_ms: 1500, }, + IntervalObservation { interval_ms: 500, }, + IntervalObservation { interval_ms: 2800, }, + IntervalObservation { interval_ms: 1200, }, + }; + const v5 = validate_pattern_randomness(&varied_runs, 3.0, 1); + assert(v5.code == PatternRandomnessCode.TooFewRuns); + assert(v5.got == 1); + assert(v5.bound == 2); +} From 8f34b81e8786b97e5fbc0fb8f539fc612f7a0eb1 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:20 +0700 Subject: [PATCH 19/26] spec(port): port the cover-traffic entropy fingerprint guard (Refs #8887) Landed from #9813 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...rc::cover_traffic_entropy_fingerprint.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/cover_traffic_entropy_fingerprint.t27 | 262 ++++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_entropy_fingerprint.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_entropy_fingerprint.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_entropy_fingerprint.json new file mode 100644 index 0000000000..f49b7fafb0 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_entropy_fingerprint.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:1ee9720f5c94986372515311b0f2d10575aa67f9dd118ab84a7d729a81bcb04c", + "gen_hash_rust": "sha256:80f09ad30ffdf34a2715a95531dea1b533def5afd9c9614e3fc2400ad7b19cb9", + "gen_hash_verilog": "sha256:ae5748f608f7e3b3cc11ea2543ee94f4b7806703a28193c868e8ea5282c87738", + "gen_hash_zig": "sha256:5d265128b4aa06ab075ee5b4640e46a30885ca0ca65d08435f2bdebe00aab1cc", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_entropy_fingerprint", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T01:46:14Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:e1fcfc2d01b7e495b64dff1b22bea9cbe2cae230f978f1ab13103b496ef8643f", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 4f87f493fc..b11c30e888 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -880,6 +880,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_burst_detection.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_arrival_autocorrelation_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.t27 new file mode 100644 index 0000000000..d71b492015 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.t27 @@ -0,0 +1,262 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_entropy_fingerprint; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.rs +// (CR-CHAT-07 - Cover traffic entropy fingerprint guard, Wave-50 +// Lane B, R-CHAT-10). +// +// Cover traffic must be indistinguishable from real AEAD +// ciphertext at the byte level: a statistical filter on +// entropy, byte frequency or zero runs strips the cover and +// collapses the anonymity set. The rules, in the original's +// order: the empty probe, the length floor, the alignment grid, +// the expected length class, the zero-run ceiling, and the +// entropy floor. +// +// The original returns Result<(), CoverEntropyError>. No +// variant carries a payload, so the verdict is the code alone +// (owner decision D5), Ok first, then the six variants in the +// original's declaration order - EmptyCover, NotAligned, +// TooShort, ZeroRunTooLong, LowEntropy, LengthClassMismatch. +// +// The original's shannon_entropy folds a log2 over a +// frequency table - the int-to-float casts and the log fold do +// not lower under the backends - so the entropy arrives as a +// number the caller computed, the lane convention for +// statistics over byte buffers (fill-byte randomness #9698, +// payload entropy #9747, cipher-mode entropy #9752, pattern +// randomness #9811). The max_zero_run walk needs no cast and +// ports whole. Every fixture's statistics are Python-verified +// exact: the good cover carries sixty-four distinct bytes (an +// entropy of six bits), the 0x42 cover measures zero, the +// nine-zero cover 0.586 with its run of nine, the exact +// eight-zero boundary 5.625 with its run of eight. + +// Alignment requirement for cover length. +pub const CTEF_ALIGNMENT: usize = 64; + +// Minimum cover length. +pub const CTEF_MIN_COVER_LEN: usize = 64; + +// Maximum consecutive zero bytes allowed. +pub const CTEF_MAX_ZERO_RUN: usize = 8; + +// Minimum Shannon entropy per 256-byte block (bits). +pub const CTEF_MIN_ENTROPY: f64 = 3.0; + +// The cover length of the fixtures. +pub const CTEF_COVER_LEN: usize = 64; + +// Verdict codes: Ok(()) and CoverEntropyError's variants, in declaration order. +pub enum CoverEntropyCode { + Ok, + EmptyCover, + NotAligned, + TooShort, + ZeroRunTooLong, + LowEntropy, + LengthClassMismatch, +} + +// The verdict of the cover entropy validation. No variant of the +// original's error carries a payload, so the code is the whole +// verdict. +pub struct CoverEntropyVerdict { + code: CoverEntropyCode, +} + +// The longest run of zero bytes - the original's max_zero_run, +// ports whole: the walk counts consecutive zeros and keeps the +// maximum. +pub fn max_zero_run(cover: []const u8) -> usize { + var max_run: usize = 0; + var current: usize = 0; + for i in 0..cover.len { + if (cover[i] == 0) { + current = current + 1; + if (current > max_run) { + max_run = current; + } + } else { + current = 0; + } + } + return max_run; +} + +// [VERIFIED] Validate a cover traffic sample for entropy and +// structural properties. Returns Ok only when every rule holds, +// checked in the original's order: the empty probe, the length +// floor, the alignment grid, the expected length class, the +// zero-run ceiling, and the entropy floor over the caller's +// measurement. +pub fn validate_cover_entropy( + cover: []const u8, + expected_len: usize, + entropy: f64, +) -> CoverEntropyVerdict { + if (cover.len == 0) { + return CoverEntropyVerdict { code: CoverEntropyCode.EmptyCover, }; + } + if (cover.len < CTEF_MIN_COVER_LEN) { + return CoverEntropyVerdict { code: CoverEntropyCode.TooShort, }; + } + if (cover.len % CTEF_ALIGNMENT != 0) { + return CoverEntropyVerdict { code: CoverEntropyCode.NotAligned, }; + } + if (cover.len != expected_len) { + return CoverEntropyVerdict { code: CoverEntropyCode.LengthClassMismatch, }; + } + if (max_zero_run(cover) > CTEF_MAX_ZERO_RUN) { + return CoverEntropyVerdict { code: CoverEntropyCode.ZeroRunTooLong, }; + } + if (entropy < CTEF_MIN_ENTROPY) { + return CoverEntropyVerdict { code: CoverEntropyCode.LowEntropy, }; + } + return CoverEntropyVerdict { code: CoverEntropyCode.Ok, }; +} + +// The good cover of the fixtures: sixty-four distinct bytes, +// (i * 131) ^ 0x55 walking the index - an entropy of six bits +// and no zero run. +pub const GOOD_COVER: [64]u8 = [64]u8{85, 214, 83, 220, 89, 218, 71, 192, 77, 206, 75, 244, 113, 242, 127, 248, 101, 230, 99, 236, 105, 234, 23, 144, 29, 158, 27, 132, 1, 130, 15, 136, 53, 182, 51, 188, 57, 186, 39, 160, 45, 174, 43, 84, 209, 82, 223, 88, 197, 70, 195, 76, 201, 74, 247, 112, 253, 126, 251, 100, 225, 98, 239, 104}; + +// The low-entropy cover: sixty-four copies of 0x42 - an entropy +// of zero. +pub const LOW_COVER: [64]u8 = [64]u8{66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66}; + +// The good cover with its first nine bytes zeroed - a run of +// nine over the ceiling of eight. +pub const ZERO_RUN_COVER: [64]u8 = [64]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 206, 75, 244, 113, 242, 127, 248, 101, 230, 99, 236, 105, 234, 23, 144, 29, 158, 27, 132, 1, 130, 15, 136, 53, 182, 51, 188, 57, 186, 39, 160, 45, 174, 43, 84, 209, 82, 223, 88, 197, 70, 195, 76, 201, 74, 247, 112, 253, 126, 251, 100, 225, 98, 239, 104}; + +// The good cover with its first eight bytes zeroed - a run of +// exactly eight, the boundary that passes. +pub const EXACT_RUN_COVER: [64]u8 = [64]u8{0, 0, 0, 0, 0, 0, 0, 0, 77, 206, 75, 244, 113, 242, 127, 248, 101, 230, 99, 236, 105, 234, 23, 144, 29, 158, 27, 132, 1, 130, 15, 136, 53, 182, 51, 188, 57, 186, 39, 160, 45, 174, 43, 84, 209, 82, 223, 88, 197, 70, 195, 76, 201, 74, 247, 112, 253, 126, 251, 100, 225, 98, 239, 104}; + +// The minimum-length cover: sixty-four distinct bytes walking +// (i * 173) - an entropy of six bits and one zero byte. +pub const MIN_LEN_COVER: [64]u8 = [64]u8{0, 173, 90, 7, 180, 97, 14, 187, 104, 21, 194, 111, 28, 201, 118, 35, 208, 125, 42, 215, 132, 49, 222, 139, 56, 229, 146, 63, 236, 153, 70, 243, 160, 77, 250, 167, 84, 1, 174, 91, 8, 181, 98, 15, 188, 105, 22, 195, 112, 29, 202, 119, 36, 209, 126, 43, 216, 133, 50, 223, 140, 57, 230, 147}; + +// The high-entropy cover: sixty-four distinct bytes walking +// (i * 251 + 17). +pub const HIGH_COVER: [64]u8 = [64]u8{17, 12, 7, 2, 253, 248, 243, 238, 233, 228, 223, 218, 213, 208, 203, 198, 193, 188, 183, 178, 173, 168, 163, 158, 153, 148, 143, 138, 133, 128, 123, 118, 113, 108, 103, 98, 93, 88, 83, 78, 73, 68, 63, 58, 53, 48, 43, 38, 33, 28, 23, 18, 13, 8, 3, 254, 249, 244, 239, 234, 229, 224, 219, 214}; + +// **CTEF-01** - an empty cover rejected. +test ctef_01_empty_rejected { + const cover: [0]u8 = [0]u8{}; + const v = validate_cover_entropy(&cover, 0, 0.0); + assert(v.code == CoverEntropyCode.EmptyCover); +} + +// **CTEF-02** - a sixteen-byte cover rejected: too short of the +// sixty-four floor. +test ctef_02_too_short_rejected { + const cover: [16]u8 = [16]u8{170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170, 170,}; + const v = validate_cover_entropy(&cover, 16, 0.0); + assert(v.code == CoverEntropyCode.TooShort); +} + +// **CTEF-03** - a misaligned cover rejected: sixty-five bytes, +// one past the alignment grid. +test ctef_03_not_aligned_rejected { + var cover: [129]u8; + for i in 0..129 { + cover[i] = 85; + } + const v = validate_cover_entropy(&cover, 129, 6.0); + assert(v.code == CoverEntropyCode.NotAligned); +} + +// **CTEF-04** - a nine-zero run rejected: the ceiling is eight. +test ctef_04_zero_run_rejected { + const v = validate_cover_entropy(&ZERO_RUN_COVER, 64, 0.5858732928619205); + assert(v.code == CoverEntropyCode.ZeroRunTooLong); +} + +// **CTEF-05** - low entropy rejected: a single repeated byte +// measures zero bits against the floor of three. +test ctef_05_low_entropy_rejected { + const v = validate_cover_entropy(&LOW_COVER, 64, 0.0); + assert(v.code == CoverEntropyCode.LowEntropy); +} + +// **CTEF-06** - the length class mismatch rejected: the cover +// carries sixty-four bytes, the envelope expects one +// twenty-eight. +test ctef_06_length_mismatch_rejected { + const v = validate_cover_entropy(&GOOD_COVER, 128, 6.0); + assert(v.code == CoverEntropyCode.LengthClassMismatch); +} + +// **CTEF-07** - the good cover accepted: six bits of entropy, +// no zero run. +test ctef_07_good_accepted { + const v = validate_cover_entropy(&GOOD_COVER, 64, 6.0); + assert(v.code == CoverEntropyCode.Ok); +} + +// **CTEF-08** - the exact eight-zero boundary accepted: 5.625 +// bits of entropy over the floor, the run at the ceiling. +test ctef_08_exact_zero_run_accepted { + const v = validate_cover_entropy(&EXACT_RUN_COVER, 64, 5.625); + assert(v.code == CoverEntropyCode.Ok); +} + +// **CTEF-09** - the minimum length accepted: sixty-four bytes +// aligned on the grid, six bits of entropy. +test ctef_09_min_len_accepted { + const v = validate_cover_entropy(&MIN_LEN_COVER, 64, 6.0); + assert(v.code == CoverEntropyCode.Ok); +} + +// **CTEF-10** - the high-entropy cover accepted. +test ctef_10_high_entropy_accepted { + const v = validate_cover_entropy(&HIGH_COVER, 64, 6.0); + assert(v.code == CoverEntropyCode.Ok); +} + +// The helper has a direct test (coverage: Untested 0). +test max_zero_run_answers { + assert(max_zero_run(&GOOD_COVER) == 0); + assert(max_zero_run(&ZERO_RUN_COVER) == 9); + assert(max_zero_run(&EXACT_RUN_COVER) == 8); + var deep_zero: [20]u8; + for i in 0..20 { + deep_zero[i] = 7; + } + deep_zero[10] = 0; + deep_zero[11] = 0; + assert(max_zero_run(&deep_zero) == 2); +} + +// Rule order is the original's: the empty probe first, the +// length floor before the alignment, the alignment before the +// class check, the class check before the zero-run ceiling, +// and the zero-run ceiling before the entropy floor. +test rule_order_matches_original { + const empty_misaligned: [0]u8 = [0]u8{}; + const v1 = validate_cover_entropy(&empty_misaligned, 65, 0.0); + assert(v1.code == CoverEntropyCode.EmptyCover); + + var short_misaligned: [16]u8; + for i in 0..16 { + short_misaligned[i] = 85; + } + const v2 = validate_cover_entropy(&short_misaligned, 16, 0.0); + assert(v2.code == CoverEntropyCode.TooShort); + + var misaligned_mismatch: [129]u8; + for i in 0..129 { + misaligned_mismatch[i] = 85; + } + const v3 = validate_cover_entropy(&misaligned_mismatch, 64, 6.0); + assert(v3.code == CoverEntropyCode.NotAligned); + + const mismatch_zero_run = validate_cover_entropy(&ZERO_RUN_COVER, 128, 0.5858732928619205); + assert(mismatch_zero_run.code == CoverEntropyCode.LengthClassMismatch); + + const zero_run_low = validate_cover_entropy(&ZERO_RUN_COVER, 64, 0.0); + assert(zero_run_low.code == CoverEntropyCode.ZeroRunTooLong); + + const low_aligned = validate_cover_entropy(&LOW_COVER, 64, 0.0); + assert(low_aligned.code == CoverEntropyCode.LowEntropy); +} From 5d09ef92cf054a08ea24d0beca718066ee5fad67 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:27 +0700 Subject: [PATCH 20/26] spec(port): port the cover-traffic generation rate stability guard (Refs #8887) Landed from #9814 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...affic_generation_rate_stability_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...raffic_generation_rate_stability_guard.t27 | 295 ++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_generation_rate_stability_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_generation_rate_stability_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_generation_rate_stability_guard.json new file mode 100644 index 0000000000..c080bb8535 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_generation_rate_stability_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:f2630b7fd7b77a032ddd820e3e1f191bb6be01ce14f2d213117e420472eff1b8", + "gen_hash_rust": "sha256:0436fb18bfc7b041bd42fded7d20f3a8a8e23f879b32734388a76b8e0afd5f74", + "gen_hash_verilog": "sha256:a661660a6aa153a03e9e0f2d587180646e315aef15e6e5a777c797f629107b82", + "gen_hash_zig": "sha256:dc4b09def94979dbcd98f8ee2fd6362980fb608eb92469f4fe6b229ec3c934b7", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_generation_rate_stability_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T01:56:52Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:90d6211f9ec2f2e95734c2d452009edbccaa70033893bd079bb7ae93a28e913b", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 11, + "total": 11, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index b11c30e888..6ec4abcfcd 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -881,6 +881,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_jitter_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_arrival_autocorrelation_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.t27 new file mode 100644 index 0000000000..05f19373d9 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.t27 @@ -0,0 +1,295 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_generation_rate_stability_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.rs +// (CR-CHAT-07 - Cover traffic generation rate stability guard, +// Wave-125 Lane B, R-CHAT-10). +// +// The generation rate must hold steady across epochs: a rate +// that drops when the user idles and rises when active tells +// presence, a unique per-epoch rate profile follows the user +// across boundaries, and a network observer reads the +// coefficient of variation straight off the wire. The rules, +// in the original's order: the batch ceiling, the epoch floor, +// then per epoch the rate floor, the rate ceiling and the +// strictly-increasing epoch number, then over the whole batch +// the CV ceiling of three tenths. +// +// The original returns Result<(), RateStabilityError>. This +// port carries the verdict as a named struct (owner decision +// D5): Ok first, then the six variants in the original's +// declaration order - HighCV, TooLow, TooHigh, +// NonMonotonicEpoch, TooFewEpochs, TooMany - with every payload +// field: the measured and ceiling CV, idx with got and min or +// max for the rate bounds, idx with prev and current for the +// monotonic break, and got with min or max for the batch +// bounds. +// +// The original's compute_cv takes a float mean and a square +// root - the int-to-float casts do not lower under the +// backends - so the coefficient of variation arrives as a +// number the caller computed, the lane convention for +// statistics over buffers (fill-byte randomness #9698, payload +// entropy #9747, pattern randomness #9811). The structural +// rules - the rate bounds, the epoch monotonicity, the batch +// bounds - stay fully in the spec, and every fixture's CV is +// Python-verified exact: the stable and boundary batches +// measure zero, the slight batch 0.0632, the unstable +// 1.4100. + +// Maximum coefficient of variation in generation rate. +pub const CGRS_MAX_CV: f64 = 0.3; + +// Minimum cover traffic rate (emissions per epoch). +pub const CGRS_MIN_RATE: u64 = 1; + +// Maximum cover traffic rate. +pub const CGRS_MAX_RATE: u64 = 1000; + +// Minimum epochs for CV check. +pub const CGRS_MIN_EPOCHS: usize = 3; + +// Maximum epochs per batch. +pub const CGRS_MAX_EPOCHS: usize = 512; + +// A cover traffic generation rate observation per epoch. +pub struct EpochRateObservation { + epoch: u64, + rate: u64, +} + +// Verdict codes: Ok(()) and RateStabilityError's variants, in declaration order. +pub enum RateStabilityCode { + Ok, + HighCV, + TooLow, + TooHigh, + NonMonotonicEpoch, + TooFewEpochs, + TooMany, +} + +// The verdict of the rate stability validation, with every +// error payload field. +pub struct RateStabilityVerdict { + code: RateStabilityCode, + idx: usize, + got: usize, + bound: usize, + got_f: f64, + bound_f: f64, + prev: u64, + current: u64, +} + +// [VERIFIED] Validate cover traffic generation rate stability. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, the epoch floor, then +// per epoch the rate floor, the rate ceiling and the +// strictly-increasing epoch, then the CV ceiling over the +// caller's measurement. +pub fn validate_rate_stability( + epochs: []const EpochRateObservation, + cv: f64, +) -> RateStabilityVerdict { + if (epochs.len > CGRS_MAX_EPOCHS) { + return RateStabilityVerdict { code: RateStabilityCode.TooMany, idx: 0, got: epochs.len, bound: CGRS_MAX_EPOCHS, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + if (epochs.len < CGRS_MIN_EPOCHS) { + return RateStabilityVerdict { code: RateStabilityCode.TooFewEpochs, idx: 0, got: epochs.len, bound: CGRS_MIN_EPOCHS, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + var prev_epoch: u64 = 0; + for i in 0..epochs.len { + if (epochs[i].rate < CGRS_MIN_RATE) { + return RateStabilityVerdict { code: RateStabilityCode.TooLow, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, prev: 0, current: epochs[i].rate, }; + } + if (epochs[i].rate > CGRS_MAX_RATE) { + return RateStabilityVerdict { code: RateStabilityCode.TooHigh, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, prev: 0, current: epochs[i].rate, }; + } + if (i > 0) { + if (epochs[i].epoch <= prev_epoch) { + return RateStabilityVerdict { code: RateStabilityCode.NonMonotonicEpoch, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, prev: prev_epoch, current: epochs[i].epoch, }; + } + } + prev_epoch = epochs[i].epoch; + } + if (cv > CGRS_MAX_CV) { + return RateStabilityVerdict { code: RateStabilityCode.HighCV, idx: 0, got: 0, bound: 0, got_f: cv, bound_f: CGRS_MAX_CV, prev: 0, current: 0, }; + } + return RateStabilityVerdict { code: RateStabilityCode.Ok, idx: 0, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; +} + +// **CGRS-01** - high CV rejected: rates one, a thousand, one - +// a CV of 1.4100 over the ceiling of three tenths. +test cgrs_01_high_cv_rejected { + const es: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 1, }, + EpochRateObservation { epoch: 2, rate: 1000, }, + EpochRateObservation { epoch: 3, rate: 1, }, + }; + const v = validate_rate_stability(&es, 1.4099793900306605); + assert(v.code == RateStabilityCode.HighCV); + assert(v.got_f == 1.4099793900306605); + assert(v.bound_f == 0.3); +} + +// **CGRS-02** - a rate below the minimum rejected: zero +// emissions in the second epoch. +test cgrs_02_too_low_rejected { + const es: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 10, }, + EpochRateObservation { epoch: 2, rate: 0, }, + EpochRateObservation { epoch: 3, rate: 10, }, + }; + const v = validate_rate_stability(&es, 0.0); + assert(v.code == RateStabilityCode.TooLow); + assert(v.idx == 1); + assert(v.current == 0); +} + +// **CGRS-03** - a rate above the maximum rejected: 1001 +// emissions of a ceiling thousand. +test cgrs_03_too_high_rejected { + const es: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 10, }, + EpochRateObservation { epoch: 2, rate: 1001, }, + EpochRateObservation { epoch: 3, rate: 10, }, + }; + const v = validate_rate_stability(&es, 0.0); + assert(v.code == RateStabilityCode.TooHigh); + assert(v.idx == 1); + assert(v.current == 1001); +} + +// **CGRS-04** - a non-monotonic epoch rejected: epoch three +// after epoch five. +test cgrs_04_non_monotonic_rejected { + const es: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 5, rate: 10, }, + EpochRateObservation { epoch: 3, rate: 10, }, + EpochRateObservation { epoch: 4, rate: 10, }, + }; + const v = validate_rate_stability(&es, 0.0); + assert(v.code == RateStabilityCode.NonMonotonicEpoch); + assert(v.idx == 1); + assert(v.prev == 5); + assert(v.current == 3); +} + +// **CGRS-05** - too few epochs rejected: two of a required +// three. +test cgrs_05_too_few_rejected { + const es: [2]EpochRateObservation = [2]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 10, }, + EpochRateObservation { epoch: 2, rate: 10, }, + }; + const v = validate_rate_stability(&es, 0.0); + assert(v.code == RateStabilityCode.TooFewEpochs); + assert(v.got == 2); + assert(v.bound == 3); +} + +// **CGRS-06** - too many rejected: 513 epochs of a maximum 512, +// built in a loop like the original's, the epochs walking one +// upward. The batch rule fires before any epoch is examined. +test cgrs_06_too_many_rejected { + var es: [513]EpochRateObservation; + for i in 0..513 { + es[i] = EpochRateObservation { epoch: @as(u64, i) + 1, rate: 10, }; + } + const v = validate_rate_stability(&es, 0.0); + assert(v.code == RateStabilityCode.TooMany); + assert(v.got == 513); + assert(v.bound == 512); +} + +// **CGRS-07** - the stable batch accepted: five epochs at ten +// emissions - a CV of zero. +test cgrs_07_stable_accepted { + const es: [5]EpochRateObservation = [5]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 10, }, + EpochRateObservation { epoch: 2, rate: 10, }, + EpochRateObservation { epoch: 3, rate: 10, }, + EpochRateObservation { epoch: 4, rate: 10, }, + EpochRateObservation { epoch: 5, rate: 10, }, + }; + const v = validate_rate_stability(&es, 0.0); + assert(v.code == RateStabilityCode.Ok); +} + +// **CGRS-08** - exactly the minimum epochs accepted: three at +// ten. +test cgrs_08_exact_minimum_accepted { + const es: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 10, }, + EpochRateObservation { epoch: 2, rate: 10, }, + EpochRateObservation { epoch: 3, rate: 10, }, + }; + const v = validate_rate_stability(&es, 0.0); + assert(v.code == RateStabilityCode.Ok); +} + +// **CGRS-09** - slight variation accepted: ten, eleven, nine, +// ten, ten - a CV of 0.0632 under the ceiling. +test cgrs_09_slight_variation_accepted { + const es: [5]EpochRateObservation = [5]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 10, }, + EpochRateObservation { epoch: 2, rate: 11, }, + EpochRateObservation { epoch: 3, rate: 9, }, + EpochRateObservation { epoch: 4, rate: 10, }, + EpochRateObservation { epoch: 5, rate: 10, }, + }; + const v = validate_rate_stability(&es, 0.06324555320336758); + assert(v.code == RateStabilityCode.Ok); +} + +// **CGRS-10** - the boundary rate accepted: one emission per +// epoch, exactly the floor, a CV of zero. +test cgrs_10_boundary_rate_accepted { + const es: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 1, }, + EpochRateObservation { epoch: 2, rate: 1, }, + EpochRateObservation { epoch: 3, rate: 1, }, + }; + const v = validate_rate_stability(&es, 0.0); + assert(v.code == RateStabilityCode.Ok); +} + +// Rule order is the original's: the epoch floor before any +// record, the rate floor before the rate ceiling, the rate +// bounds before the epoch monotonicity, and the per-epoch rules +// before the batch CV - with the boundary-rate batch passing +// and the unstable one caught by the CV last. +test rule_order_matches_original { + const short_low: [2]EpochRateObservation = [2]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 0, }, + EpochRateObservation { epoch: 2, rate: 10, }, + }; + const v1 = validate_rate_stability(&short_low, 0.0); + assert(v1.code == RateStabilityCode.TooFewEpochs); + + const zero_backstep: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 10, }, + EpochRateObservation { epoch: 2, rate: 0, }, + EpochRateObservation { epoch: 1, rate: 10, }, + }; + const v2 = validate_rate_stability(&zero_backstep, 0.0); + assert(v2.code == RateStabilityCode.TooLow); + assert(v2.idx == 1); + + const backstep_low: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 5, rate: 10, }, + EpochRateObservation { epoch: 3, rate: 10, }, + EpochRateObservation { epoch: 1, rate: 0, }, + }; + const v3 = validate_rate_stability(&backstep_low, 0.0); + assert(v3.code == RateStabilityCode.NonMonotonicEpoch); + assert(v3.idx == 1); + + const unstable_low_cv: [3]EpochRateObservation = [3]EpochRateObservation{ + EpochRateObservation { epoch: 1, rate: 1, }, + EpochRateObservation { epoch: 2, rate: 1000, }, + EpochRateObservation { epoch: 3, rate: 1, }, + }; + const v4 = validate_rate_stability(&unstable_low_cv, 0.2); + assert(v4.code == RateStabilityCode.Ok); +} From 64e6eca9dd9aba28cbe7a737ef34cd6e94552037 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:33 +0700 Subject: [PATCH 21/26] spec(port): port the cover-traffic inter-epoch continuity guard (Refs #8887) Landed from #9819 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ..._traffic_inter_epoch_continuity_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...r_traffic_inter_epoch_continuity_guard.t27 | 286 ++++++++++++++++++ 3 files changed, 315 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_inter_epoch_continuity_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_inter_epoch_continuity_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_inter_epoch_continuity_guard.json new file mode 100644 index 0000000000..50a5593c09 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_inter_epoch_continuity_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:68088bdc226dda84dc35caf614daef5cb72cd09485113c8d856ec45e397967da", + "gen_hash_rust": "sha256:19ef421ba6d508cc3d310f9f7d7e2483163d1b1f5947e9075dd70a7e1c6d58c2", + "gen_hash_verilog": "sha256:401c6bb60b5bf5dc106f88faaab97685b06dc23ae5d93b45f8eaa5bcf37c9999", + "gen_hash_zig": "sha256:24af86410de99bd42806a0b8e3c08946873483df9470183d5705b46d7c670a2d", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_inter_epoch_continuity_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T02:33:22Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:ae2daf7d6ab2105a0a8602556887ead27e6940e9f12cfc494d535e28bf5b1188", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 6ec4abcfcd..4f973dce4c 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -883,6 +883,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_entropy_fingerprint.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_arrival_autocorrelation_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/commit.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27 new file mode 100644 index 0000000000..82dab9788d --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27 @@ -0,0 +1,286 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_inter_epoch_continuity_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.rs +// (CR-CHAT-07 - Cover traffic inter-epoch continuity guard, +// Wave-103 Lane A, R-CHAT-10). +// +// The cover rate must hold across epoch boundaries: a rate drop +// at epoch N names the key-rotation point, rate changes +// correlated with membership changes reveal who joined or left, +// and the epoch where the rate moves is the epoch where the +// content moved. The rules, in the original's order: the batch +// ceiling, then per epoch the zero-epoch probe, the rate floor, +// the rate ceiling, the strictly-increasing epoch number and +// the consecutive-rate deviation ceiling (a tenth, measured +// against the previous rate). +// +// The original returns Result<(), ContinuityError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six variants in the original's declaration +// order - DeviationExceeded, NotIncreasing, ZeroEpoch, BelowMin, +// AboveMax, TooMany - with every payload field: idx with prev +// and current rates and the ceiling deviation for +// DeviationExceeded, idx with prev and current epochs for +// NotIncreasing, idx with the rate and its bound for the rate +// rules, and got with max for the ceiling. +// +// The original's arithmetic is f64 throughout - the rates live +// in the records as f64, the deviation is the absolute +// difference over the previous rate - so the port needs no +// casts and no caller-computed statistics: the absolute +// difference is the guarded if, and every fixture value is +// exact in f64. The boundary fixture measures a deviation of +// 0.099 against the ceiling of 0.1. + +// Maximum rate deviation between consecutive epochs (msgs/sec). +pub const CTIC_MAX_RATE_DEV: f64 = 0.1; + +// Minimum emission rate (msgs/sec). +pub const CTIC_MIN_RATE: f64 = 0.5; + +// Maximum emission rate (msgs/sec). +pub const CTIC_MAX_RATE: f64 = 10.0; + +// Maximum epochs per batch. +pub const CTIC_MAX_EPOCHS: usize = 256; + +// An epoch's cover traffic rate record. +pub struct EpochRate { + epoch: u64, + rate: f64, +} + +// Verdict codes: Ok(()) and ContinuityError's variants, in declaration order. +pub enum ContinuityCode { + Ok, + DeviationExceeded, + NotIncreasing, + ZeroEpoch, + BelowMin, + AboveMax, + TooMany, +} + +// The verdict of the inter-epoch continuity validation, with +// every error payload field. +pub struct ContinuityVerdict { + code: ContinuityCode, + idx: usize, + got: usize, + bound: usize, + prev_epoch: u64, + current_epoch: u64, + prev_rate: f64, + current_rate: f64, + bound_f: f64, +} + +// The absolute difference of two rates - the original's .abs() +// on the deviation numerator, the guarded if. +pub fn rate_abs_diff(a: f64, b: f64) -> f64 { + if (a > b) { + return a - b; + } + return b - a; +} + +// [VERIFIED] Validate cover traffic inter-epoch continuity. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, then per epoch the zero +// epoch, the rate floor, the rate ceiling, the strictly- +// increasing epoch and the consecutive-rate deviation ceiling. +pub fn validate_inter_epoch_continuity(epochs: []const EpochRate) -> ContinuityVerdict { + if (epochs.len > CTIC_MAX_EPOCHS) { + return ContinuityVerdict { code: ContinuityCode.TooMany, idx: 0, got: epochs.len, bound: CTIC_MAX_EPOCHS, prev_epoch: 0, current_epoch: 0, prev_rate: 0.0, current_rate: 0.0, bound_f: 0.0, }; + } + var prev_epoch: u64 = 0; + var prev_rate: f64 = 0.0; + for i in 0..epochs.len { + if (epochs[i].epoch == 0) { + return ContinuityVerdict { code: ContinuityCode.ZeroEpoch, idx: i, got: 0, bound: 0, prev_epoch: 0, current_epoch: 0, prev_rate: 0.0, current_rate: 0.0, bound_f: 0.0, }; + } + if (epochs[i].rate < CTIC_MIN_RATE) { + return ContinuityVerdict { code: ContinuityCode.BelowMin, idx: i, got: 0, bound: 0, prev_epoch: 0, current_epoch: 0, prev_rate: 0.0, current_rate: epochs[i].rate, bound_f: CTIC_MIN_RATE, }; + } + if (epochs[i].rate > CTIC_MAX_RATE) { + return ContinuityVerdict { code: ContinuityCode.AboveMax, idx: i, got: 0, bound: 0, prev_epoch: 0, current_epoch: 0, prev_rate: 0.0, current_rate: epochs[i].rate, bound_f: CTIC_MAX_RATE, }; + } + if (i > 0) { + if (epochs[i].epoch <= prev_epoch) { + return ContinuityVerdict { code: ContinuityCode.NotIncreasing, idx: i, got: 0, bound: 0, prev_epoch: prev_epoch, current_epoch: epochs[i].epoch, prev_rate: 0.0, current_rate: 0.0, bound_f: 0.0, }; + } + const dev: f64 = rate_abs_diff(epochs[i].rate, prev_rate) / prev_rate; + if (dev > CTIC_MAX_RATE_DEV) { + return ContinuityVerdict { code: ContinuityCode.DeviationExceeded, idx: i, got: 0, bound: 0, prev_epoch: 0, current_epoch: 0, prev_rate: prev_rate, current_rate: epochs[i].rate, bound_f: CTIC_MAX_RATE_DEV, }; + } + } + prev_epoch = epochs[i].epoch; + prev_rate = epochs[i].rate; + } + return ContinuityVerdict { code: ContinuityCode.Ok, idx: 0, got: 0, bound: 0, prev_epoch: 0, current_epoch: 0, prev_rate: 0.0, current_rate: 0.0, bound_f: 0.0, }; +} + +// **CTIC-01** - the deviation ceiling rejected: 3.0 after 2.0 - +// a deviation of 0.5 over the ceiling of 0.1. +test ctic_01_deviation_exceeded_rejected { + const es: [2]EpochRate = [2]EpochRate{ + EpochRate { epoch: 1, rate: 2.0, }, + EpochRate { epoch: 2, rate: 3.0, }, + }; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.DeviationExceeded); + assert(v.idx == 1); + assert(v.prev_rate == 2.0); + assert(v.current_rate == 3.0); + assert(v.bound_f == 0.1); +} + +// **CTIC-02** - a non-increasing epoch rejected: epoch three +// after epoch five. +test ctic_02_not_increasing_rejected { + const es: [2]EpochRate = [2]EpochRate{ + EpochRate { epoch: 5, rate: 2.0, }, + EpochRate { epoch: 3, rate: 2.0, }, + }; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.NotIncreasing); + assert(v.idx == 1); + assert(v.prev_epoch == 5); + assert(v.current_epoch == 3); +} + +// **CTIC-03** - the zero epoch rejected. +test ctic_03_zero_epoch_rejected { + const es: [1]EpochRate = [1]EpochRate{ + EpochRate { epoch: 0, rate: 2.0, }, + }; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.ZeroEpoch); + assert(v.idx == 0); +} + +// **CTIC-04** - a rate below the minimum rejected: 0.1 of a +// floor 0.5. +test ctic_04_below_min_rejected { + const es: [1]EpochRate = [1]EpochRate{ + EpochRate { epoch: 1, rate: 0.1, }, + }; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.BelowMin); + assert(v.idx == 0); + assert(v.current_rate == 0.1); + assert(v.bound_f == 0.5); +} + +// **CTIC-05** - a rate above the maximum rejected: 15.0 of a +// ceiling 10.0. +test ctic_05_above_max_rejected { + const es: [1]EpochRate = [1]EpochRate{ + EpochRate { epoch: 1, rate: 15.0, }, + }; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.AboveMax); + assert(v.idx == 0); + assert(v.current_rate == 15.0); + assert(v.bound_f == 10.0); +} + +// **CTIC-06** - too many rejected: 257 epochs of a maximum 256, +// built in a loop like the original's, the epochs walking one +// upward. The batch rule fires before any epoch is examined. +test ctic_06_too_many_rejected { + var es: [257]EpochRate; + for i in 0..257 { + es[i] = EpochRate { epoch: @as(u64, i) + 1, rate: 2.0, }; + } + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.TooMany); + assert(v.got == 257); + assert(v.bound == 256); +} + +// **CTIC-07** - the valid batch accepted: 2.0, 2.05, 1.95 - +// deviations 0.025 and 0.049, both under the ceiling. +test ctic_07_valid_accepted { + const es: [3]EpochRate = [3]EpochRate{ + EpochRate { epoch: 1, rate: 2.0, }, + EpochRate { epoch: 2, rate: 2.05, }, + EpochRate { epoch: 3, rate: 1.95, }, + }; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.Ok); +} + +// **CTIC-08** - an empty batch accepted. +test ctic_08_empty_accepted { + const es: [0]EpochRate = [0]EpochRate{}; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.Ok); +} + +// **CTIC-09** - a single epoch accepted: the deviation check +// needs a predecessor. +test ctic_09_single_accepted { + const es: [1]EpochRate = [1]EpochRate{ + EpochRate { epoch: 1, rate: 2.0, }, + }; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.Ok); +} + +// **CTIC-10** - the deviation boundary accepted: 2.198 after +// 2.0 - a deviation of 0.099 against the ceiling of 0.1. +test ctic_10_boundary_accepted { + const es: [2]EpochRate = [2]EpochRate{ + EpochRate { epoch: 1, rate: 2.0, }, + EpochRate { epoch: 2, rate: 2.198, }, + }; + const v = validate_inter_epoch_continuity(&es); + assert(v.code == ContinuityCode.Ok); +} + +// The helper has a direct test (coverage: Untested 0). +test rate_abs_diff_answers { + assert(rate_abs_diff(3.0, 2.0) == 1.0); + assert(rate_abs_diff(2.0, 3.0) == 1.0); + assert(rate_abs_diff(2.0, 2.0) == 0.0); + assert(rate_abs_diff(0.1, 0.0) == 0.1); +} + +// Rule order is the original's: the zero epoch before the rate +// floor, the rate floor before the rate ceiling, the rate +// bounds before the epoch monotonicity, and the monotonicity +// before the deviation - with both violations on one epoch. +test rule_order_matches_original { + const zero_low: [1]EpochRate = [1]EpochRate{ + EpochRate { epoch: 0, rate: 0.1, }, + }; + const v1 = validate_inter_epoch_continuity(&zero_low); + assert(v1.code == ContinuityCode.ZeroEpoch); + + const low_backstep: [2]EpochRate = [2]EpochRate{ + EpochRate { epoch: 5, rate: 2.0, }, + EpochRate { epoch: 3, rate: 0.1, }, + }; + const v2 = validate_inter_epoch_continuity(&low_backstep); + assert(v2.code == ContinuityCode.BelowMin); + assert(v2.idx == 1); + + const backstep_deviant: [2]EpochRate = [2]EpochRate{ + EpochRate { epoch: 5, rate: 2.0, }, + EpochRate { epoch: 3, rate: 9.0, }, + }; + const v3 = validate_inter_epoch_continuity(&backstep_deviant); + assert(v3.code == ContinuityCode.NotIncreasing); + assert(v3.idx == 1); + + const deviant_pair: [2]EpochRate = [2]EpochRate{ + EpochRate { epoch: 1, rate: 2.0, }, + EpochRate { epoch: 2, rate: 8.0, }, + }; + const v4 = validate_inter_epoch_continuity(&deviant_pair); + assert(v4.code == ContinuityCode.DeviationExceeded); + assert(v4.idx == 1); + assert(v4.prev_rate == 2.0); + assert(v4.current_rate == 8.0); +} From 9342a856f8534f0527db0ccd781574766a8768f9 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:40 +0700 Subject: [PATCH 22/26] spec(port): port the cover-traffic payload length distribution guard (Refs #8887) Landed from #9820 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...fic_payload_length_distribution_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...ffic_payload_length_distribution_guard.t27 | 328 ++++++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_length_distribution_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_length_distribution_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_length_distribution_guard.json new file mode 100644 index 0000000000..ca272e7ea1 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_length_distribution_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:f3e29f186ca11d0faa26934f9ba3ebcc4db07faac9564211f7b3e046f78f3410", + "gen_hash_rust": "sha256:ff6be6d160c830e96af03487efd7304823e82cbaf8dd7907f8a10ff6ad9ca5ac", + "gen_hash_verilog": "sha256:bbe76087e1e90685e316ed1a2c634eaab28005519aec821c01b8d3e9046bcfa9", + "gen_hash_zig": "sha256:a54f290b13b8a87d651f6b668987a163932b42565e1c4499f453e181efcd41ef", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_length_distribution_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T02:43:35Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:79857c3c6887aa47b7296ed21801f27b2d6fd96c45435562f8d4f544cef05e9d", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 4f973dce4c..2d989a6766 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -884,6 +884,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_generation_rate_stability_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_arrival_autocorrelation_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/commit.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27 new file mode 100644 index 0000000000..8b5a835622 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27 @@ -0,0 +1,328 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_length_distribution_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.rs +// (CR-CHAT-07 - Cover traffic payload length distribution guard, +// Wave-140 Lane B, R-CHAT-10). +// +// Cover payload lengths must follow the same distribution as +// real traffic: a divergent length distribution lets an +// observer chi-square the two apart, specific length patterns +// fingerprint the generator, and once cover is identified +// everything else is known real. The rules, in the original's +// order: the batch ceiling, then per batch the repeated-id +// scan, the class floor, the class ceiling, the observation +// floor and the chi-squared ceiling. +// +// The original returns Result<(), LengthDistError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six variants in the original's declaration +// order - HighChi, TooFewObs, TooManyClasses, TooFewClasses, +// DuplicateBatch, TooManyBatches - with every payload field: +// idx with the measured and ceiling chi for HighChi, idx with +// got and min or max for the observation and class rules, idx +// for the duplicate rule, and got with max for the batch +// ceiling. +// +// The chi-squared statistic arrives in the record exactly as +// the original carries it - a number the caller computed over +// the cover and real length histograms (the record's own field, +// no porting of the histogram needed). The BTreeSet dedup +// becomes a scan over the batches already visited. + +// Maximum chi-squared statistic. +pub const CTPL_MAX_CHI_SQUARED: f64 = 50.0; + +// Minimum observations per batch. +pub const CTPL_MIN_OBSERVATIONS: usize = 10; + +// Minimum length classes. +pub const CTPL_MIN_CLASSES: usize = 2; + +// Maximum length classes. +pub const CTPL_MAX_CLASSES: usize = 16; + +// Maximum batches. +pub const CTPL_MAX_BATCHES: usize = 128; + +// Class ID length. +pub const CTPL_CLASS_ID_LEN: usize = 8; + +// A length distribution comparison record. +pub struct LengthDistRecord { + batch_id: [8]u8, + observations: usize, + num_classes: usize, + chi_squared: f64, +} + +// Verdict codes: Ok(()) and LengthDistError's variants, in declaration order. +pub enum LengthDistCode { + Ok, + HighChi, + TooFewObs, + TooManyClasses, + TooFewClasses, + DuplicateBatch, + TooManyBatches, +} + +// The verdict of the length distribution validation, with every +// error payload field. +pub struct LengthDistVerdict { + code: LengthDistCode, + idx: usize, + got: usize, + bound: usize, + got_f: f64, + bound_f: f64, +} + +// Whole-form equality over the fixed eight batch-id bytes. +pub fn batch_ids_equal(a: [8]u8, b: [8]u8) -> bool { + for i in 0..8 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether batch i repeats an earlier id - the original's +// BTreeSet insert probe, answered by a scan. +pub fn batch_seen_before(batches: []const LengthDistRecord, i: usize) -> bool { + for j in 0..i { + if (batch_ids_equal(batches[j].batch_id, batches[i].batch_id)) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate cover traffic payload length +// distribution. Returns Ok only when every rule holds, checked +// in the original's order: the batch ceiling, then per batch +// the repeated id, the class floor, the class ceiling, the +// observation floor and the chi-squared ceiling. +pub fn validate_length_distribution(batches: []const LengthDistRecord) -> LengthDistVerdict { + if (batches.len > CTPL_MAX_BATCHES) { + return LengthDistVerdict { code: LengthDistCode.TooManyBatches, idx: 0, got: batches.len, bound: CTPL_MAX_BATCHES, got_f: 0.0, bound_f: 0.0, }; + } + for i in 0..batches.len { + if (batch_seen_before(batches, i)) { + return LengthDistVerdict { code: LengthDistCode.DuplicateBatch, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + if (batches[i].num_classes < CTPL_MIN_CLASSES) { + return LengthDistVerdict { code: LengthDistCode.TooFewClasses, idx: i, got: batches[i].num_classes, bound: CTPL_MIN_CLASSES, got_f: 0.0, bound_f: 0.0, }; + } + if (batches[i].num_classes > CTPL_MAX_CLASSES) { + return LengthDistVerdict { code: LengthDistCode.TooManyClasses, idx: i, got: batches[i].num_classes, bound: CTPL_MAX_CLASSES, got_f: 0.0, bound_f: 0.0, }; + } + if (batches[i].observations < CTPL_MIN_OBSERVATIONS) { + return LengthDistVerdict { code: LengthDistCode.TooFewObs, idx: i, got: batches[i].observations, bound: CTPL_MIN_OBSERVATIONS, got_f: 0.0, bound_f: 0.0, }; + } + if (batches[i].chi_squared > CTPL_MAX_CHI_SQUARED) { + return LengthDistVerdict { code: LengthDistCode.HighChi, idx: i, got: 0, bound: 0, got_f: batches[i].chi_squared, bound_f: CTPL_MAX_CHI_SQUARED, }; + } + } + return LengthDistVerdict { code: LengthDistCode.Ok, idx: 0, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; +} + +// The batch ids of the fixtures, one byte repeated eight times +// - the original's bid(byte) helper, entry by entry. +pub const BID_01: [8]u8 = [8]u8{1, 1, 1, 1, 1, 1, 1, 1,}; +pub const BID_02: [8]u8 = [8]u8{2, 2, 2, 2, 2, 2, 2, 2,}; + +// The leading id bytes of the loop fixtures. +pub const CTPL_ID_BYTES_20: [20]u8 = [20]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,}; + +// The observation counts of the many-valid fixture: 50 + i * 10. +pub const CTPL_OBS_20: [20]usize = [20]usize{50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240,}; + +// The chi-squared values of the many-valid fixture: 10 + i. +pub const CTPL_CHI_20: [20]f64 = [20]f64{10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0,}; + +// The two id bytes of the too-many fixture: the original writes +// the big-endian u64 of i + 1 into id[0..8], and for every value +// in 1..=129 only the last two of those eight bytes can carry a +// nonzero digit. +pub const CTPL_ID6_129: [129]u8 = [129]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; +pub const CTPL_ID7_129: [129]u8 = [129]u8{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129}; + +// **CTPL-01** - high chi rejected: 60.0 of a ceiling 50.0. +test ctpl_01_high_chi_rejected { + const bs: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 4, chi_squared: 60.0, }, + }; + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.HighChi); + assert(v.idx == 0); + assert(v.got_f == 60.0); + assert(v.bound_f == 50.0); +} + +// **CTPL-02** - too few observations rejected: nine of a +// required ten. +test ctpl_02_too_few_obs_rejected { + const bs: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 9, num_classes: 4, chi_squared: 15.0, }, + }; + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.TooFewObs); + assert(v.idx == 0); + assert(v.got == 9); + assert(v.bound == 10); +} + +// **CTPL-03** - too many classes rejected: seventeen of a +// ceiling sixteen. +test ctpl_03_too_many_classes_rejected { + const bs: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 17, chi_squared: 15.0, }, + }; + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.TooManyClasses); + assert(v.idx == 0); + assert(v.got == 17); + assert(v.bound == 16); +} + +// **CTPL-04** - too few classes rejected: one class of a floor +// two. +test ctpl_04_too_few_classes_rejected { + const bs: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 1, chi_squared: 15.0, }, + }; + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.TooFewClasses); + assert(v.idx == 0); + assert(v.got == 1); + assert(v.bound == 2); +} + +// **CTPL-05** - a duplicate batch id rejected: the second batch +// repeats the first's id. +test ctpl_05_duplicate_rejected { + const bs: [2]LengthDistRecord = [2]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 4, chi_squared: 15.0, }, + LengthDistRecord { batch_id: BID_01, observations: 200, num_classes: 8, chi_squared: 25.0, }, + }; + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.DuplicateBatch); + assert(v.idx == 1); +} + +// **CTPL-06** - too many rejected: 129 batches of a maximum 128, +// built in a loop like the original's, each id carrying the +// big-endian u64 of its index plus one. The batch rule fires +// before any batch is examined. +test ctpl_06_too_many_rejected { + var bs: [129]LengthDistRecord; + for i in 0..129 { + var id: [8]u8 = [8]u8{0, 0, 0, 0, 0, 0, 0, 0,}; + id[6] = CTPL_ID6_129[i]; + id[7] = CTPL_ID7_129[i]; + bs[i] = LengthDistRecord { batch_id: id, observations: 100, num_classes: 4, chi_squared: 15.0, }; + } + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.TooManyBatches); + assert(v.got == 129); + assert(v.bound == 128); +} + +// **CTPL-07** - the valid batches accepted: chi 15.0 and 25.0, +// both under the ceiling. +test ctpl_07_valid_accepted { + const bs: [2]LengthDistRecord = [2]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 4, chi_squared: 15.0, }, + LengthDistRecord { batch_id: BID_02, observations: 200, num_classes: 8, chi_squared: 25.0, }, + }; + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.Ok); +} + +// **CTPL-08** - an empty batch set accepted. +test ctpl_08_empty_accepted { + const bs: [0]LengthDistRecord = [0]LengthDistRecord{}; + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.Ok); +} + +// **CTPL-09** - the boundary chi accepted: exactly 50.0 - the +// rule is strictly greater. +test ctpl_09_boundary_chi_accepted { + const bs: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 4, chi_squared: 50.0, }, + }; + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.Ok); +} + +// **CTPL-10** - many valid batches accepted: twenty batches, +// the observations walking fifty upward and the chi values ten +// upward - every chi under the ceiling. +test ctpl_10_many_valid_accepted { + var bs: [20]LengthDistRecord; + for i in 0..20 { + var id: [8]u8 = [8]u8{0, 0, 0, 0, 0, 0, 0, 0,}; + id[0] = CTPL_ID_BYTES_20[i]; + bs[i] = LengthDistRecord { batch_id: id, observations: CTPL_OBS_20[i], num_classes: 4, chi_squared: CTPL_CHI_20[i], }; + } + const v = validate_length_distribution(&bs); + assert(v.code == LengthDistCode.Ok); +} + +// The two helpers have direct tests (coverage: Untested 0). +test ctpl_helpers_answer { + assert(batch_ids_equal(BID_01, BID_01)); + assert(!batch_ids_equal(BID_01, BID_02)); + const three: [3]LengthDistRecord = [3]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 4, chi_squared: 15.0, }, + LengthDistRecord { batch_id: BID_02, observations: 100, num_classes: 4, chi_squared: 15.0, }, + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 4, chi_squared: 15.0, }, + }; + assert(!batch_seen_before(&three, 0)); + assert(!batch_seen_before(&three, 1)); + assert(batch_seen_before(&three, 2)); +} + +// Rule order is the original's: the duplicate before the class +// floor, the class floor before the class ceiling, the class +// bounds before the observation floor, and the observation +// floor before the chi ceiling. +test rule_order_matches_original { + const dup_few: [2]LengthDistRecord = [2]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 4, chi_squared: 15.0, }, + LengthDistRecord { batch_id: BID_01, observations: 9, num_classes: 1, chi_squared: 60.0, }, + }; + const v1 = validate_length_distribution(&dup_few); + assert(v1.code == LengthDistCode.DuplicateBatch); + assert(v1.idx == 1); + + const few_obs: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 9, num_classes: 1, chi_squared: 60.0, }, + }; + const v2 = validate_length_distribution(&few_obs); + assert(v2.code == LengthDistCode.TooFewClasses); + assert(v2.idx == 0); + + const many_few: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 9, num_classes: 17, chi_squared: 60.0, }, + }; + const v3 = validate_length_distribution(&many_few); + assert(v3.code == LengthDistCode.TooManyClasses); + assert(v3.idx == 0); + + const few_obs_high: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 9, num_classes: 4, chi_squared: 60.0, }, + }; + const v4 = validate_length_distribution(&few_obs_high); + assert(v4.code == LengthDistCode.TooFewObs); + assert(v4.idx == 0); + + const high_only: [1]LengthDistRecord = [1]LengthDistRecord{ + LengthDistRecord { batch_id: BID_01, observations: 100, num_classes: 4, chi_squared: 60.0, }, + }; + const v5 = validate_length_distribution(&high_only); + assert(v5.code == LengthDistCode.HighChi); + assert(v5.idx == 0); +} From 536c09263b7a5fc68d78d17d9e5bf67af8efafde Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:46 +0700 Subject: [PATCH 23/26] spec(port): port the cover-traffic payload size variance guard (Refs #8887) Landed from #9822 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...r_traffic_payload_size_variance_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...er_traffic_payload_size_variance_guard.t27 | 279 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_size_variance_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_size_variance_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_size_variance_guard.json new file mode 100644 index 0000000000..c51d015cc1 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_size_variance_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:c2e26b1e99ef7be49a63be5e518c6ba24f34541b0a778c1077aa14fefc3a64f3", + "gen_hash_rust": "sha256:5060d849616b13e2ff9d4e7093b3fc922612ff4e0d08e869367a94fe2de1d436", + "gen_hash_verilog": "sha256:c8289b2b8923fdb7ad5caacc2c96c5e059bcf18efcf491abf1949432b756e6a8", + "gen_hash_zig": "sha256:54f4ef396ab4ba97fe48b48b3c615eda94b95633792651daf6fd2e192c8200c3", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_size_variance_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T02:53:12Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:97d9050ddc441a5e64d1c6460bfcbf2a38dc4c82d149e9379efdef3f0dff60f0", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 11, + "total": 11, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 2d989a6766..2e4e0b4207 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -885,6 +885,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_arrival_autocorrelation_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/commit.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.t27 new file mode 100644 index 0000000000..5220ab6675 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.t27 @@ -0,0 +1,279 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_payload_size_variance_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.rs +// (CR-CHAT-07 - Cover traffic payload size variance guard, +// Wave-121 Lane B, R-CHAT-10). +// +// Cover payload sizes must hold a narrow band: a wide variance +// fingerprints the generator, sizes moving in sync with real +// messages match cover to real, and a high coefficient of +// variation separates the two trains trivially. The rules, in +// the original's order: the batch ceiling, then per observation +// the zero-timestamp probe, the strictly-increasing timestamps, +// the size floor (256) and the size ceiling (4096), then over +// at least two observations the coefficient-of-variation +// ceiling of two tenths. +// +// The original returns Result<(), SizeVarianceError>. This port +// carries the verdict as a named struct (owner decision D5): Ok +// first, then the six variants in the original's declaration +// order - HighCV, TooSmall, TooLarge, ZeroTimestamp, NonMonotonic, +// TooMany - with every payload field: the measured and ceiling +// CV, idx with got and min or max for the size bounds, idx for +// the zero timestamp, idx with prev and current for the +// monotonic break, and got with max for the batch bounds. +// +// The original's compute_cv takes a float mean and a square +// root of usize sums - the int-to-float casts do not lower +// under the backends - so the coefficient of variation arrives +// as a number the caller computed, the lane convention for +// statistics over buffers (fill-byte randomness #9698, payload +// entropy #9747, pattern randomness #9811, rate stability +// #9814). The structural rules stay fully in the spec. Every +// fixture's CV is Python-verified exact: the band-edge pair +// 0.8824 over the ceiling, the uniform batch zero, the slight +// batch 0.0148. + +// Maximum coefficient of variation (stddev / mean). +pub const CPSV_MAX_CV: f64 = 0.2; + +// Minimum cover payload size. +pub const CPSV_MIN_SIZE: usize = 256; + +// Maximum cover payload size. +pub const CPSV_MAX_SIZE: usize = 4096; + +// Maximum emissions per batch. +pub const CPSV_MAX_EMISSIONS: usize = 1024; + +// A cover traffic size observation. +pub struct CoverSizeObservation { + size: usize, + timestamp_ms: u64, +} + +// Verdict codes: Ok(()) and SizeVarianceError's variants, in declaration order. +pub enum SizeVarianceCode { + Ok, + HighCV, + TooSmall, + TooLarge, + ZeroTimestamp, + NonMonotonic, + TooMany, +} + +// The verdict of the size variance validation, with every error +// payload field. +pub struct SizeVarianceVerdict { + code: SizeVarianceCode, + idx: usize, + got: usize, + bound: usize, + got_f: f64, + bound_f: f64, + prev: u64, + current: u64, +} + +// [VERIFIED] Validate cover traffic payload size variance. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, then per observation the +// zero timestamp, the strictly-increasing timestamps, the size +// floor and the size ceiling, then over at least two +// observations the caller's CV against the ceiling. +pub fn validate_size_variance( + observations: []const CoverSizeObservation, + cv: f64, +) -> SizeVarianceVerdict { + if (observations.len > CPSV_MAX_EMISSIONS) { + return SizeVarianceVerdict { code: SizeVarianceCode.TooMany, idx: 0, got: observations.len, bound: CPSV_MAX_EMISSIONS, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + var prev_ts: u64 = 0; + for i in 0..observations.len { + if (observations[i].timestamp_ms == 0) { + return SizeVarianceVerdict { code: SizeVarianceCode.ZeroTimestamp, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + if (i > 0) { + if (observations[i].timestamp_ms <= prev_ts) { + return SizeVarianceVerdict { code: SizeVarianceCode.NonMonotonic, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, prev: prev_ts, current: observations[i].timestamp_ms, }; + } + } + if (observations[i].size < CPSV_MIN_SIZE) { + return SizeVarianceVerdict { code: SizeVarianceCode.TooSmall, idx: i, got: observations[i].size, bound: CPSV_MIN_SIZE, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + if (observations[i].size > CPSV_MAX_SIZE) { + return SizeVarianceVerdict { code: SizeVarianceCode.TooLarge, idx: i, got: observations[i].size, bound: CPSV_MAX_SIZE, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; + } + prev_ts = observations[i].timestamp_ms; + } + if (observations.len >= 2) { + if (cv > CPSV_MAX_CV) { + return SizeVarianceVerdict { code: SizeVarianceCode.HighCV, idx: 0, got: 0, bound: 0, got_f: cv, bound_f: CPSV_MAX_CV, prev: 0, current: 0, }; + } + } + return SizeVarianceVerdict { code: SizeVarianceCode.Ok, idx: 0, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, prev: 0, current: 0, }; +} + +// **CPSV-01** - high CV rejected: the band-edge pair, 256 and +// 4096 - a CV of 0.8824 over the ceiling of two tenths. +test cpsv_01_high_cv_rejected { + const os: [2]CoverSizeObservation = [2]CoverSizeObservation{ + CoverSizeObservation { size: 256, timestamp_ms: 1000, }, + CoverSizeObservation { size: 4096, timestamp_ms: 2000, }, + }; + const v = validate_size_variance(&os, 0.8823529411764706); + assert(v.code == SizeVarianceCode.HighCV); + assert(v.got_f == 0.8823529411764706); + assert(v.bound_f == 0.2); +} + +// **CPSV-02** - a size below the minimum rejected: 100 bytes of +// a floor 256. +test cpsv_02_too_small_rejected { + const os: [1]CoverSizeObservation = [1]CoverSizeObservation{ + CoverSizeObservation { size: 100, timestamp_ms: 1000, }, + }; + const v = validate_size_variance(&os, 0.0); + assert(v.code == SizeVarianceCode.TooSmall); + assert(v.idx == 0); + assert(v.got == 100); + assert(v.bound == 256); +} + +// **CPSV-03** - a size above the maximum rejected: 4097 bytes of +// a ceiling 4096. +test cpsv_03_too_large_rejected { + const os: [1]CoverSizeObservation = [1]CoverSizeObservation{ + CoverSizeObservation { size: 4097, timestamp_ms: 1000, }, + }; + const v = validate_size_variance(&os, 0.0); + assert(v.code == SizeVarianceCode.TooLarge); + assert(v.idx == 0); + assert(v.got == 4097); + assert(v.bound == 4096); +} + +// **CPSV-04** - the zero timestamp rejected. +test cpsv_04_zero_timestamp_rejected { + const os: [1]CoverSizeObservation = [1]CoverSizeObservation{ + CoverSizeObservation { size: 1024, timestamp_ms: 0, }, + }; + const v = validate_size_variance(&os, 0.0); + assert(v.code == SizeVarianceCode.ZeroTimestamp); + assert(v.idx == 0); +} + +// **CPSV-05** - a non-monotonic timestamp rejected: 1000 after +// 2000. +test cpsv_05_non_monotonic_rejected { + const os: [2]CoverSizeObservation = [2]CoverSizeObservation{ + CoverSizeObservation { size: 1024, timestamp_ms: 2000, }, + CoverSizeObservation { size: 1024, timestamp_ms: 1000, }, + }; + const v = validate_size_variance(&os, 0.0); + assert(v.code == SizeVarianceCode.NonMonotonic); + assert(v.idx == 1); + assert(v.prev == 2000); + assert(v.current == 1000); +} + +// **CPSV-06** - too many rejected: 1025 observations of a +// maximum 1024, built in a loop like the original's, the +// timestamps stepping by one. The batch rule fires before any +// observation is examined. +test cpsv_06_too_many_rejected { + var os: [1025]CoverSizeObservation; + for i in 0..1025 { + os[i] = CoverSizeObservation { size: 1024, timestamp_ms: @as(u64, i) + 1, }; + } + const v = validate_size_variance(&os, 0.0); + assert(v.code == SizeVarianceCode.TooMany); + assert(v.got == 1025); + assert(v.bound == 1024); +} + +// **CPSV-07** - the uniform batch accepted: four observations at +// 1024 bytes - a CV of zero. +test cpsv_07_uniform_accepted { + const os: [4]CoverSizeObservation = [4]CoverSizeObservation{ + CoverSizeObservation { size: 1024, timestamp_ms: 1000, }, + CoverSizeObservation { size: 1024, timestamp_ms: 2000, }, + CoverSizeObservation { size: 1024, timestamp_ms: 3000, }, + CoverSizeObservation { size: 1024, timestamp_ms: 4000, }, + }; + const v = validate_size_variance(&os, 0.0); + assert(v.code == SizeVarianceCode.Ok); +} + +// **CPSV-08** - an empty batch accepted. +test cpsv_08_empty_accepted { + const os: [0]CoverSizeObservation = [0]CoverSizeObservation{}; + const v = validate_size_variance(&os, 0.0); + assert(v.code == SizeVarianceCode.Ok); +} + +// **CPSV-09** - a single observation accepted: the CV check +// needs two. +test cpsv_09_single_accepted { + const os: [1]CoverSizeObservation = [1]CoverSizeObservation{ + CoverSizeObservation { size: 1024, timestamp_ms: 1000, }, + }; + const v = validate_size_variance(&os, 0.9); + assert(v.code == SizeVarianceCode.Ok); +} + +// **CPSV-10** - slight variance accepted: 1000, 1020, 980, 1010 +// - a CV of 0.0148 under the ceiling. +test cpsv_10_slight_variance_accepted { + const os: [4]CoverSizeObservation = [4]CoverSizeObservation{ + CoverSizeObservation { size: 1000, timestamp_ms: 1000, }, + CoverSizeObservation { size: 1020, timestamp_ms: 2000, }, + CoverSizeObservation { size: 980, timestamp_ms: 3000, }, + CoverSizeObservation { size: 1010, timestamp_ms: 4000, }, + }; + const v = validate_size_variance(&os, 0.014753316167330712); + assert(v.code == SizeVarianceCode.Ok); +} + +// Rule order is the original's: the zero timestamp before the +// monotonicity, the monotonicity before the size floor, the +// size floor before the size ceiling, and the per-observation +// rules before the batch CV - with both violations on one +// observation. +test rule_order_matches_original { + const zero_backstep: [2]CoverSizeObservation = [2]CoverSizeObservation{ + CoverSizeObservation { size: 1024, timestamp_ms: 2000, }, + CoverSizeObservation { size: 1024, timestamp_ms: 0, }, + }; + const v1 = validate_size_variance(&zero_backstep, 0.0); + assert(v1.code == SizeVarianceCode.ZeroTimestamp); + assert(v1.idx == 1); + + const backstep_tiny: [2]CoverSizeObservation = [2]CoverSizeObservation{ + CoverSizeObservation { size: 1024, timestamp_ms: 2000, }, + CoverSizeObservation { size: 100, timestamp_ms: 1000, }, + }; + const v2 = validate_size_variance(&backstep_tiny, 0.0); + assert(v2.code == SizeVarianceCode.NonMonotonic); + assert(v2.idx == 1); + + const tiny_band: [1]CoverSizeObservation = [1]CoverSizeObservation{ + CoverSizeObservation { size: 100, timestamp_ms: 1000, }, + }; + const v3 = validate_size_variance(&tiny_band, 0.0); + assert(v3.code == SizeVarianceCode.TooSmall); + + const huge_only: [1]CoverSizeObservation = [1]CoverSizeObservation{ + CoverSizeObservation { size: 4097, timestamp_ms: 1000, }, + }; + const v4 = validate_size_variance(&huge_only, 0.0); + assert(v4.code == SizeVarianceCode.TooLarge); + + const band_high_cv: [2]CoverSizeObservation = [2]CoverSizeObservation{ + CoverSizeObservation { size: 256, timestamp_ms: 1000, }, + CoverSizeObservation { size: 4096, timestamp_ms: 2000, }, + }; + const v5 = validate_size_variance(&band_high_cv, 0.8823529411764706); + assert(v5.code == SizeVarianceCode.HighCV); +} From 7a18ac1ca8dbb914d15cc1ae6f7ddfd52736e304 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:52 +0700 Subject: [PATCH 24/26] spec(port): port the cover-traffic starvation guard (Refs #8887) Landed from #9826 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...HAT-07::src::cover_traffic_starvation.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../src/cover_traffic_starvation.t27 | 345 ++++++++++++++++++ 3 files changed, 374 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_starvation.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_starvation.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_starvation.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_starvation.json new file mode 100644 index 0000000000..8b83a33232 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_starvation.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:d3d02c37909709163cafaea58b4f2866f92b8618c12c259bc0ca8a11ce4b761c", + "gen_hash_rust": "sha256:3a3812c3ae07179d1562df06ff953de60cd620e4741a86b5fa0981f1a5f753c5", + "gen_hash_verilog": "sha256:b5a0b3583510e37281a4d182f989a3f20199e7cf2b83bd01e7aa4075577d6a4e", + "gen_hash_zig": "sha256:de2d9255dc086a34846f7408d694c96589c280a5d8a6d08236b6c2d641318a63", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_starvation", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T03:22:24Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:8c2e5acb5d9dfa8193f4aaf7381a0623f2b9943719c85ab86ec640398bba61d1", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_starvation.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 12, + "total": 12, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 2e4e0b4207..fed016be70 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -886,6 +886,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_inter_epoch_continuity_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_starvation.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/commit.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_starvation.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_starvation.t27 new file mode 100644 index 0000000000..5d5348d7e9 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_starvation.t27 @@ -0,0 +1,345 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_starvation; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_starvation.rs +// (CR-CHAT-07 - Cover-traffic starvation guard, Wave-25 Lane B, +// L-CHAT-7-cts, R-CHAT-10). +// +// Cover traffic only works when every wire tick produces +// something at a steady cadence: a paused schedule leaves a gap +// that breaks the canonical quantisation, a suppressed cover +// stream lets the reals stand out, and a forced cover burst +// re-leaks the correlation uniform cadence hides. The guard +// pins two invariants on a contiguous window: every +// inter-emission gap sits on the canonical ladder, and the +// cover share never drops below one quarter - compared as the +// original's integer cross-multiplication, so no division and +// no float anywhere. +// +// The rules, in the original's order: the window floor (an +// empty or short window is no signal), the gap-length coupling +// (exactly one fewer gap than emissions), the per-gap canonical +// membership, and the cover-floor cross-multiplication. +// +// The original returns Result<(), CoverStarvationError>. This +// port carries the verdict as a named struct (owner decision +// D5): Ok first, then the four variants in the original's +// declaration order - WindowTooShort, MismatchedGapLength, +// NonCanonicalGap, CoverFloorBreached - with every payload +// field: observed and required for the floor, the two observed +// lengths for the coupling, the offending gap's index and value, +// and the cover count with the total. +// +// The original's CTS-10 composes the guard with the stateful +// CoverScheduler (new, enqueue_real, tick) and lets the result +// depend on the scheduler's random draws; a stateful scheduler +// is outside the pure-guard form, so the port's green test +// asserts the constants and exercises the guard's whole path +// with a fixed window instead - the composition the original +// itself calls "the path", with the randomness named and left +// with the scheduler. The canonical ladder is the ring's +// [1000, 5000, 30000, 300000], ported as in the burst guard. + +// Minimum number of emissions a window must contain before the +// cover-floor check applies. +pub const CTS_WINDOW_MIN_EMISSIONS: usize = 4; + +// Numerator of the minimum acceptable cover ratio. +pub const CTS_MIN_COVER_RATIO_NUM: usize = 1; + +// Denominator of the minimum acceptable cover ratio. +pub const CTS_MIN_COVER_RATIO_DEN: usize = 4; + +// The canonical gap ladder of the ring's scheduler. +pub const CTS_CANONICAL_GAPS_MS: [4]u64 = [4]u64{1000, 5000, 30000, 300000,}; + +// The kind of one emission on the wire. +pub enum EmissionKind { + Real, + Cover, +} + +// Verdict codes: Ok(()) and CoverStarvationError's variants, in declaration order. +pub enum CoverStarvationCode { + Ok, + WindowTooShort, + MismatchedGapLength, + NonCanonicalGap, + CoverFloorBreached, +} + +// The verdict of the starvation validation, with every error +// payload field. +pub struct CoverStarvationVerdict { + code: CoverStarvationCode, + observed: usize, + required: usize, + observed_gaps: usize, + observed_emissions: usize, + index: usize, + value_ms: u64, + cover_count: usize, + total: usize, +} + +// Whether the gap sits on the canonical ladder - the original's +// contains probe, answered by a scan. +pub fn is_canonical_gap(g: u64) -> bool { + if (g == 1000) { + return true; + } + if (g == 5000) { + return true; + } + if (g == 30000) { + return true; + } + if (g == 300000) { + return true; + } + return false; +} + +// [VERIFIED] Validate a contiguous wire window of cover-traffic +// emissions. Returns Ok only when every rule holds, checked in +// the original's order: the window floor, the gap-length +// coupling, the per-gap canonical membership, and the +// cover-floor cross-multiplication. +pub fn validate_window( + emissions: []const EmissionKind, + gaps_ms: []const u64, +) -> CoverStarvationVerdict { + if (emissions.len < CTS_WINDOW_MIN_EMISSIONS) { + return CoverStarvationVerdict { code: CoverStarvationCode.WindowTooShort, observed: emissions.len, required: CTS_WINDOW_MIN_EMISSIONS, observed_gaps: 0, observed_emissions: 0, index: 0, value_ms: 0, cover_count: 0, total: 0, }; + } + if (gaps_ms.len + 1 != emissions.len) { + return CoverStarvationVerdict { code: CoverStarvationCode.MismatchedGapLength, observed: 0, required: 0, observed_gaps: gaps_ms.len, observed_emissions: emissions.len, index: 0, value_ms: 0, cover_count: 0, total: 0, }; + } + for i in 0..gaps_ms.len { + if (!is_canonical_gap(gaps_ms[i])) { + return CoverStarvationVerdict { code: CoverStarvationCode.NonCanonicalGap, observed: 0, required: 0, observed_gaps: 0, observed_emissions: 0, index: i, value_ms: gaps_ms[i], cover_count: 0, total: 0, }; + } + } + var cover_count: usize = 0; + for i in 0..emissions.len { + if (emissions[i] == EmissionKind.Cover) { + cover_count = cover_count + 1; + } + } + if (cover_count * CTS_MIN_COVER_RATIO_DEN < emissions.len * CTS_MIN_COVER_RATIO_NUM) { + return CoverStarvationVerdict { code: CoverStarvationCode.CoverFloorBreached, observed: 0, required: 0, observed_gaps: 0, observed_emissions: 0, index: 0, value_ms: 0, cover_count: cover_count, total: emissions.len, }; + } + return CoverStarvationVerdict { code: CoverStarvationCode.Ok, observed: 0, required: 0, observed_gaps: 0, observed_emissions: 0, index: 0, value_ms: 0, cover_count: 0, total: 0, }; +} + +// **CTS-01** - the uniform-cadence balanced window accepted: +// real, cover, real, cover on one-second gaps - two covers of +// four, exactly at the floor. +test cts01_valid_uniform_window_accepted { + const em: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Real, + EmissionKind.Cover, + EmissionKind.Real, + EmissionKind.Cover, + }; + const g: [3]u64 = [3]u64{1000, 1000, 1000,}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.Ok); +} + +// **CTS-02** - a cadence skip rejected: one fifteen-hundred- +// millisecond gap off the ladder, pinpointed at index one. +test cts02_one_off_canonical_gap_rejected { + const em: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Cover, + EmissionKind.Real, + EmissionKind.Cover, + EmissionKind.Real, + }; + const g: [3]u64 = [3]u64{1000, 1500, 1000,}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.NonCanonicalGap); + assert(v.index == 1); + assert(v.value_ms == 1500); +} + +// **CTS-03** - an empty window rejected: no signal. +test cts03_empty_window_rejected { + const em: [0]EmissionKind = [0]EmissionKind{}; + const g: [0]u64 = [0]u64{}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.WindowTooShort); + assert(v.observed == 0); + assert(v.required == 4); +} + +// **CTS-04** - an all-real window rejected: the cover share is +// zero of four, under the quarter floor. +test cts04_all_real_window_breaches_cover_floor { + const em: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + }; + const g: [3]u64 = [3]u64{1000, 1000, 1000,}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.CoverFloorBreached); + assert(v.cover_count == 0); + assert(v.total == 4); +} + +// **CTS-05** - an all-cover window accepted: the floor +// saturated at four of four. +test cts05_all_cover_window_accepted { + const em: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Cover, + EmissionKind.Cover, + EmissionKind.Cover, + EmissionKind.Cover, + }; + const g: [3]u64 = [3]u64{5000, 5000, 5000,}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.Ok); +} + +// **CTS-06** - the floor boundary accepted: one cover of four, +// exactly the quarter. +test cts06_floor_boundary_accepted { + const em: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Cover, + EmissionKind.Real, + }; + const g: [3]u64 = [3]u64{30000, 30000, 30000,}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.Ok); +} + +// **CTS-07** - one below the floor rejected: one cover of eight, +// where the quarter demands two. +test cts07_below_floor_rejected { + const em: [8]EmissionKind = [8]EmissionKind{ + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Cover, + }; + const g: [7]u64 = [7]u64{1000, 1000, 1000, 1000, 1000, 1000, 1000,}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.CoverFloorBreached); + assert(v.cover_count == 1); + assert(v.total == 8); +} + +// **CTS-08** - a mismatched gap length rejected: four emissions +// need three gaps, four supplied. +test cts08_mismatched_gap_length_rejected { + const em: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Cover, + EmissionKind.Cover, + EmissionKind.Cover, + EmissionKind.Cover, + }; + const g: [4]u64 = [4]u64{1000, 1000, 1000, 1000,}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.MismatchedGapLength); + assert(v.observed_gaps == 4); + assert(v.observed_emissions == 4); +} + +// **CTS-09** - a non-canonical gap at the last position: the +// index points at the final gap. +test cts09_non_canonical_gap_index_correct { + const em: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Cover, + EmissionKind.Cover, + EmissionKind.Cover, + EmissionKind.Cover, + }; + const g: [3]u64 = [3]u64{1000, 5000, 42000,}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.NonCanonicalGap); + assert(v.index == 2); + assert(v.value_ms == 42000); +} + +// **CTS-10** - green: the constants are public and the guard's +// whole path composes over a fixed window - the stateful +// CoverScheduler of the original's composition is outside the +// pure-guard form, its randomness named and left with the +// scheduler. +test cts10_green_constants_and_composition { + assert(CTS_WINDOW_MIN_EMISSIONS == 4); + assert(CTS_MIN_COVER_RATIO_NUM == 1); + assert(CTS_MIN_COVER_RATIO_DEN == 4); + const em: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Real, + EmissionKind.Cover, + EmissionKind.Real, + EmissionKind.Cover, + }; + const g: [3]u64 = [3]u64{CTS_CANONICAL_GAPS_MS[0], CTS_CANONICAL_GAPS_MS[0], CTS_CANONICAL_GAPS_MS[0],}; + const v = validate_window(&em, &g); + assert(v.code == CoverStarvationCode.Ok); +} + +// The helper has a direct test (coverage: Untested 0). +test is_canonical_gap_answers { + assert(is_canonical_gap(1000)); + assert(is_canonical_gap(5000)); + assert(is_canonical_gap(30000)); + assert(is_canonical_gap(300000)); + assert(!is_canonical_gap(1500)); + assert(!is_canonical_gap(42000)); + assert(!is_canonical_gap(0)); +} + +// Rule order is the original's: the window floor before the gap +// coupling, the coupling before the canonical scan, and the +// canonical scan before the cover floor. +test rule_order_matches_original { + const short_mismatched: [2]EmissionKind = [2]EmissionKind{ + EmissionKind.Cover, + EmissionKind.Cover, + }; + const g2: [5]u64 = [5]u64{1000, 1000, 1000, 1000, 1000,}; + const v1 = validate_window(&short_mismatched, &g2); + assert(v1.code == CoverStarvationCode.WindowTooShort); + + const mismatched_offgrid: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Cover, + EmissionKind.Cover, + EmissionKind.Cover, + EmissionKind.Cover, + }; + const g4: [4]u64 = [4]u64{1000, 1500, 1000, 1000,}; + const v2 = validate_window(&mismatched_offgrid, &g4); + assert(v2.code == CoverStarvationCode.MismatchedGapLength); + + const offgrid_thin: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + }; + const g3: [3]u64 = [3]u64{1000, 1500, 1000,}; + const v3 = validate_window(&offgrid_thin, &g3); + assert(v3.code == CoverStarvationCode.NonCanonicalGap); + assert(v3.index == 1); + + const thin_only: [4]EmissionKind = [4]EmissionKind{ + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + EmissionKind.Real, + }; + const gok: [3]u64 = [3]u64{1000, 1000, 1000,}; + const v4 = validate_window(&thin_only, &gok); + assert(v4.code == CoverStarvationCode.CoverFloorBreached); + assert(v4.cover_count == 0); +} From 0d4fe82f2806d743ba2b2f5d3d22b1feaa5e48ec Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Sun, 18 Oct 2026 01:43:58 +0700 Subject: [PATCH 25/26] spec(port): port the cover-traffic timing correlation guard (Refs #8887) Landed from #9828 as part of one combined PR (the Actions queue could not carry 28 single runs while master kept moving under the ledger rows). Co-Authored-By: Claude Opus 5.5 --- ...over_traffic_timing_correlation_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + ...cover_traffic_timing_correlation_guard.t27 | 327 ++++++++++++++++++ 3 files changed, 356 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_timing_correlation_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_timing_correlation_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_timing_correlation_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_timing_correlation_guard.json new file mode 100644 index 0000000000..6c7202d2ea --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_timing_correlation_guard.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:9584114c62a3b9901a7634ee13bc97ebc1f0d535a4f6486e2be5588e96f44e5a", + "gen_hash_rust": "sha256:0c8a1ebc5d8ebb3aa2ff783c2195367e8a08da0f4a781673bde5681bc2f12392", + "gen_hash_verilog": "sha256:b76fd685573fe6a8895718df316e85af4fabf339fe7f1e3a5c2581db3578dc96", + "gen_hash_zig": "sha256:849b312808ab1db82265057ed9370622a730eed2b781545cbeca1df2ec312e89", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_timing_correlation_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T03:34:37Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:848d7d491685f090a57ba335d59356a5f6bedaa5cea58775b3b979bbe62fe1f1", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_timing_correlation_guard.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 11, + "total": 11, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index fed016be70..837b311262 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -887,6 +887,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_length_distribution_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_payload_size_variance_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_starvation.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_timing_correlation_guard.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/agent.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-cli/src/cmd/commit.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_timing_correlation_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_timing_correlation_guard.t27 new file mode 100644 index 0000000000..759b0df765 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_timing_correlation_guard.t27 @@ -0,0 +1,327 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_timing_correlation_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_timing_correlation_guard.rs +// (CR-CHAT-07 - Cover traffic timing correlation guard, Wave-79 +// Lane B, R-CHAT-10). +// +// Cover and real timing must not correlate: cover always right +// after real is a recognizable pair, cover that only appears +// with real traffic makes its presence a signal, and cover +// inter-arrival mirroring real inter-arrival leaks the real +// pattern. The rules, in the original's order: the window +// ceiling, the presence of both trains (no real, no cover +// answers its own error), then per emission the consecutive- +// real probe and the cover-between-reals floor (at least one +// cover between any two reals), then over the whole window the +// always-follows rejection - when every cover of the window +// sits directly after a real, the pattern is correlated. +// +// The original returns Result<(), CoverTimingError>. This port +// carries the verdict as a named struct (owner decision D5): +// Ok first, then the six variants in the original's +// declaration order - ConsecutiveReal, InsufficientCover, +// CoverAlwaysFollowsReal, WindowTooLarge, NoRealTraffic, +// NoCoverTraffic - none of which carries a payload, so the +// code is the whole verdict. +// +// The original's CTCG-02 ignores its result entirely; the +// verified outcome is CoverAlwaysFollowsReal - the single +// cover of real, cover, real sits directly after a real, so +// every cover of the window follows one, and the port pins +// that outcome with the discrepancy named. The original +// also declares two of its counters twice (a duplicate its +// build tolerated); the port declares each once, the +// semantics identical. + +// Minimum cover emissions between two real emissions. +pub const CTCG_MIN_COVER_BETWEEN_REAL: usize = 1; + +// Maximum emissions in a window. +pub const CTCG_MAX_WINDOW: usize = 256; + +// Whether an emission is real or cover. +pub enum EmissionType { + Real, + Cover, +} + +// An emission event. +pub struct TimingEmission { + timestamp_ms: u64, + kind: EmissionType, +} + +// Verdict codes: Ok(()) and CoverTimingError's variants, in declaration order. +pub enum CoverTimingCode { + Ok, + ConsecutiveReal, + InsufficientCover, + CoverAlwaysFollowsReal, + WindowTooLarge, + NoRealTraffic, + NoCoverTraffic, +} + +// The verdict of the cover timing validation. No variant of the +// original's error carries a payload, so the code is the whole +// verdict. +pub struct CoverTimingVerdict { + code: CoverTimingCode, +} + +// [VERIFIED] Validate that cover and real traffic timing are +// not correlated. Returns Ok only when every rule holds, +// checked in the original's order: the window ceiling, the +// presence of both trains, then per emission the consecutive- +// real probe and the cover floor, then the whole-window +// always-follows rejection. +pub fn validate_cover_timing(emissions: []const TimingEmission) -> CoverTimingVerdict { + if (emissions.len > CTCG_MAX_WINDOW) { + return CoverTimingVerdict { code: CoverTimingCode.WindowTooLarge, }; + } + var has_real: bool = false; + var has_cover: bool = false; + for i in 0..emissions.len { + if (emissions[i].kind == EmissionType.Real) { + has_real = true; + } + if (emissions[i].kind == EmissionType.Cover) { + has_cover = true; + } + } + if (!has_real) { + return CoverTimingVerdict { code: CoverTimingCode.NoRealTraffic, }; + } + if (!has_cover) { + return CoverTimingVerdict { code: CoverTimingCode.NoCoverTraffic, }; + } + var consecutive_real: usize = 0; + var cover_since_last_real: usize = 0; + var cover_count: usize = 0; + var total_real: usize = 0; + var cover_after_real: usize = 0; + for i in 0..emissions.len { + if (emissions[i].kind == EmissionType.Real) { + if (consecutive_real > 0) { + return CoverTimingVerdict { code: CoverTimingCode.ConsecutiveReal, }; + } + if (total_real > 0) { + if (cover_since_last_real < CTCG_MIN_COVER_BETWEEN_REAL) { + return CoverTimingVerdict { code: CoverTimingCode.InsufficientCover, }; + } + } + consecutive_real = 1; + cover_since_last_real = 0; + total_real = total_real + 1; + } else { + cover_since_last_real = cover_since_last_real + 1; + cover_count = cover_count + 1; + if (i > 0) { + if (emissions[i - 1].kind == EmissionType.Real) { + cover_after_real = cover_after_real + 1; + } + } + consecutive_real = 0; + } + } + if (cover_count > 0) { + if (cover_after_real == cover_count) { + return CoverTimingVerdict { code: CoverTimingCode.CoverAlwaysFollowsReal, }; + } + } + return CoverTimingVerdict { code: CoverTimingCode.Ok, }; +} + +// **CTCG-01** - two consecutive reals rejected: no cover +// between them. +test ctcg_01_consecutive_real_rejected { + const seq: [4]TimingEmission = [4]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 400, kind: EmissionType.Cover, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.ConsecutiveReal); +} + +// **CTCG-02** - the original ignores this result; the verified +// outcome is CoverAlwaysFollowsReal: the single cover sits +// directly after a real, so every cover of the window follows +// one - the floor of one cover between reals itself passes, +// the always-follows rule is what answers. The port pins the +// verified outcome and names the discrepancy. +test ctcg_02_insufficient_cover_exercised { + const seq: [3]TimingEmission = [3]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Real, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.CoverAlwaysFollowsReal); +} + +// **CTCG-03** - cover always follows real rejected: every cover +// of the window sits directly after a real. +test ctcg_03_cover_follows_real_rejected { + const seq: [4]TimingEmission = [4]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 400, kind: EmissionType.Cover, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.CoverAlwaysFollowsReal); +} + +// **CTCG-04** - the window ceiling rejected: 257 emissions of a +// maximum 256, built in a loop like the original's, the kinds +// alternating. The batch rule fires before any emission is +// examined. +test ctcg_04_window_large_rejected { + var seq: [257]TimingEmission; + for i in 0..257 { + var kind: EmissionType = EmissionType.Cover; + if (i % 2 == 0) { + kind = EmissionType.Real; + } + seq[i] = TimingEmission { timestamp_ms: @as(u64, i) * 100, kind: kind, }; + } + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.WindowTooLarge); +} + +// **CTCG-05** - a window without real traffic rejected. +test ctcg_05_no_real_rejected { + const seq: [3]TimingEmission = [3]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Cover, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.NoRealTraffic); +} + +// **CTCG-06** - a window without cover traffic rejected: one +// real, nothing else. +test ctcg_06_no_cover_rejected { + const seq: [1]TimingEmission = [1]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Real, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.NoCoverTraffic); +} + +// **CTCG-07** - the valid sequence accepted: cover, real, +// cover, cover, real, then three covers - every real separated, +// not every cover after a real. +test ctcg_07_valid_accepted { + const seq: [8]TimingEmission = [8]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 400, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 500, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 600, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 700, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 800, kind: EmissionType.Cover, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.Ok); +} + +// **CTCG-08** - cover before the first real accepted: two +// covers open the window, so the later covers after reals do +// not make the whole pattern always-follows. +test ctcg_08_cover_first_accepted { + const seq: [5]TimingEmission = [5]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 400, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 500, kind: EmissionType.Cover, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.Ok); +} + +// **CTCG-09** - the interleaved sequence accepted: three reals, +// each with covers between, and covers not all after reals. +test ctcg_09_interleaved_accepted { + const seq: [9]TimingEmission = [9]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 400, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 500, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 600, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 700, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 800, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 900, kind: EmissionType.Cover, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.Ok); +} + +// **CTCG-10** - the minimal valid sequence accepted: six +// emissions, two reals, every gap covered. +test ctcg_10_minimal_accepted { + const seq: [6]TimingEmission = [6]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 400, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 500, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 600, kind: EmissionType.Cover, }, + }; + const v = validate_cover_timing(&seq); + assert(v.code == CoverTimingCode.Ok); +} + +// Rule order is the original's: the window ceiling before any +// presence check, the no-real before the no-cover probe, the +// presence checks before the per-emission rules, the +// consecutive-real before the cover floor, and the per-emission +// rules before the always-follows rejection. +test rule_order_matches_original { + var flooded: [257]TimingEmission; + for i in 0..257 { + flooded[i] = TimingEmission { timestamp_ms: @as(u64, i), kind: EmissionType.Cover, }; + } + const v1 = validate_cover_timing(&flooded); + assert(v1.code == CoverTimingCode.WindowTooLarge); + + const no_train: [3]TimingEmission = [3]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Cover, }, + }; + const v2 = validate_cover_timing(&no_train); + assert(v2.code == CoverTimingCode.NoRealTraffic); + + const twins: [3]TimingEmission = [3]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Cover, }, + }; + const v3 = validate_cover_timing(&twins); + assert(v3.code == CoverTimingCode.ConsecutiveReal); + + const back_to_back: [4]TimingEmission = [4]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 400, kind: EmissionType.Real, }, + }; + const v4 = validate_cover_timing(&back_to_back); + assert(v4.code == CoverTimingCode.ConsecutiveReal); + + const paired: [4]TimingEmission = [4]TimingEmission{ + TimingEmission { timestamp_ms: 100, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 200, kind: EmissionType.Cover, }, + TimingEmission { timestamp_ms: 300, kind: EmissionType.Real, }, + TimingEmission { timestamp_ms: 400, kind: EmissionType.Cover, }, + }; + const v5 = validate_cover_timing(&paired); + assert(v5.code == CoverTimingCode.CoverAlwaysFollowsReal); +} From f3d55bd19f417373746f85d69b634751c6f1b7cb Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 02:26:25 +0700 Subject: [PATCH 26/26] spec(port): name the emission guard's interval type apart (types ratchet, #2774) cover_traffic_emission_pattern_randomness_guard.t27 (from #9811) declared `IntervalObservation`, a name master already defines in cover_traffic_inter_arrival_autocorrelation_guard.t27. Two definitions of one type name leave its domain size undetermined, so `tri types ratchet` went red (ledger 109, observed 110). Rename it EmissionIntervalObservation in this file only and reseal; the ratchet is CLEAN at 109 again. Refs #10096 Co-Authored-By: Claude Opus 5.5 --- ...fic_emission_pattern_randomness_guard.json | 14 +- ...ffic_emission_pattern_randomness_guard.t27 | 180 +++++++++--------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard.json index f23e72088c..acda714ed1 100644 --- a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard.json +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard.json @@ -1,17 +1,17 @@ { - "built_by": "t27c-bootstrap@0.5.2+01d0a635f", + "built_by": "t27c-bootstrap@0.5.2+0d4fe82f2", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:f384e714b1f61a205e6e86b746a33b48bcaf20cca826e256aa1efb4d68eee37c", - "gen_hash_rust": "sha256:44c0930818553b0ef664d227740af5b2838f3e02439e39bd21bed8526e90dcbc", - "gen_hash_verilog": "sha256:053f4b1f3849ba4940c452ae750fffe435bf2840c4c039bb82fa5295d3b3878b", - "gen_hash_zig": "sha256:55dc70f098def20eca600da4f70b1867e99a17b9746af45089d10bdbf3ed1d18", + "gen_hash_c": "sha256:9f9389f923c9505976484080394e0c106a20fb067c9618d82d5a024a892f481b", + "gen_hash_rust": "sha256:172d0058e7f4c2e2eb8e42edd92a41f094336b6051dd2a65fb09815940c62df9", + "gen_hash_verilog": "sha256:abe001fcc2acc8c46c87ff3a8b1c7eb603058a4aee4534fd611f98a6ecf0fccb", + "gen_hash_zig": "sha256:04e21c3940deabb2e93a7fff587a28c6a3954d24576a6219c3effa06ef5e661f", "module": "port::trios::crates::trios-chat::rings::CR-CHAT-07::src::cover_traffic_emission_pattern_randomness_guard", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-17T01:35:29Z", + "sealed_at": "2026-10-17T19:25:41Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:dd90be7e7b346e8a203eaf692c85817774df85767b1af801b7f1500a28aa1a4d", + "spec_hash": "sha256:e453a7ab6ab48a705c38c7f9fde06d3f1381d9f8c2a939987f5ee898cd0f6f0e", "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27", "tests": { "failed": 0, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27 index 09110347e8..fa99027d9a 100644 --- a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27 +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-07/src/cover_traffic_emission_pattern_randomness_guard.t27 @@ -48,8 +48,8 @@ pub const CEPR_MAX_INTERVAL: u64 = 300000; // Maximum intervals per batch. pub const CEPR_MAX_INTERVALS: usize = 4096; -// An inter-emission interval observation. -pub struct IntervalObservation { +// An inter-emission interval observation (named apart from the inter-arrival guard's IntervalObservation, #2774). +pub struct EmissionIntervalObservation { interval_ms: u64, } @@ -84,7 +84,7 @@ pub struct PatternRandomnessVerdict { // ceiling, then the entropy floor and the runs floor over the // caller's statistics. pub fn validate_pattern_randomness( - observations: []const IntervalObservation, + observations: []const EmissionIntervalObservation, entropy: f64, runs: usize, ) -> PatternRandomnessVerdict { @@ -126,9 +126,9 @@ pub const CEPR_INTERVALS_100: [100]u64 = [100]u64{500, 2031, 534, 2093, 568, 215 // **CEPR-01** - low entropy rejected: eight identical intervals // measure entropy zero against the floor of two bits. test cepr_01_low_entropy_rejected { - var os: [8]IntervalObservation; + var os: [8]EmissionIntervalObservation; for i in 0..8 { - os[i] = IntervalObservation { interval_ms: 1000, }; + os[i] = EmissionIntervalObservation { interval_ms: 1000, }; } const v = validate_pattern_randomness(&os, 0.0, 1); assert(v.code == PatternRandomnessCode.LowEntropy); @@ -139,10 +139,10 @@ test cepr_01_low_entropy_rejected { // **CEPR-02** - too few rejected: three intervals of a required // eight. test cepr_02_too_few_rejected { - const os: [3]IntervalObservation = [3]IntervalObservation{ - IntervalObservation { interval_ms: 1000, }, - IntervalObservation { interval_ms: 2000, }, - IntervalObservation { interval_ms: 1500, }, + const os: [3]EmissionIntervalObservation = [3]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 1000, }, + EmissionIntervalObservation { interval_ms: 2000, }, + EmissionIntervalObservation { interval_ms: 1500, }, }; const v = validate_pattern_randomness(&os, 3.0, 3); assert(v.code == PatternRandomnessCode.TooFew); @@ -153,16 +153,16 @@ test cepr_02_too_few_rejected { // **CEPR-03** - a zero interval rejected at the appended // position. test cepr_03_zero_interval_rejected { - const os: [9]IntervalObservation = [9]IntervalObservation{ - IntervalObservation { interval_ms: 1000, }, - IntervalObservation { interval_ms: 2500, }, - IntervalObservation { interval_ms: 800, }, - IntervalObservation { interval_ms: 3200, }, - IntervalObservation { interval_ms: 1500, }, - IntervalObservation { interval_ms: 500, }, - IntervalObservation { interval_ms: 2800, }, - IntervalObservation { interval_ms: 1200, }, - IntervalObservation { interval_ms: 0, }, + const os: [9]EmissionIntervalObservation = [9]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 1000, }, + EmissionIntervalObservation { interval_ms: 2500, }, + EmissionIntervalObservation { interval_ms: 800, }, + EmissionIntervalObservation { interval_ms: 3200, }, + EmissionIntervalObservation { interval_ms: 1500, }, + EmissionIntervalObservation { interval_ms: 500, }, + EmissionIntervalObservation { interval_ms: 2800, }, + EmissionIntervalObservation { interval_ms: 1200, }, + EmissionIntervalObservation { interval_ms: 0, }, }; const v = validate_pattern_randomness(&os, 3.0, 8); assert(v.code == PatternRandomnessCode.ZeroInterval); @@ -173,16 +173,16 @@ test cepr_03_zero_interval_rejected { // 300001 milliseconds of a maximum 300000, at the appended // position. test cepr_04_interval_too_large_rejected { - const os: [9]IntervalObservation = [9]IntervalObservation{ - IntervalObservation { interval_ms: 1000, }, - IntervalObservation { interval_ms: 2500, }, - IntervalObservation { interval_ms: 800, }, - IntervalObservation { interval_ms: 3200, }, - IntervalObservation { interval_ms: 1500, }, - IntervalObservation { interval_ms: 500, }, - IntervalObservation { interval_ms: 2800, }, - IntervalObservation { interval_ms: 1200, }, - IntervalObservation { interval_ms: 300001, }, + const os: [9]EmissionIntervalObservation = [9]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 1000, }, + EmissionIntervalObservation { interval_ms: 2500, }, + EmissionIntervalObservation { interval_ms: 800, }, + EmissionIntervalObservation { interval_ms: 3200, }, + EmissionIntervalObservation { interval_ms: 1500, }, + EmissionIntervalObservation { interval_ms: 500, }, + EmissionIntervalObservation { interval_ms: 2800, }, + EmissionIntervalObservation { interval_ms: 1200, }, + EmissionIntervalObservation { interval_ms: 300001, }, }; const v = validate_pattern_randomness(&os, 3.0, 8); assert(v.code == PatternRandomnessCode.IntervalTooLarge); @@ -198,13 +198,13 @@ test cepr_04_interval_too_large_rejected { // caller's true measurements and the entropy floor answers // first. test cepr_05_too_few_runs_rejected { - var os: [20]IntervalObservation; + var os: [20]EmissionIntervalObservation; for i in 0..20 { var iv: u64 = 100; if (i >= 15) { iv = 5000; } - os[i] = IntervalObservation { interval_ms: iv, }; + os[i] = EmissionIntervalObservation { interval_ms: iv, }; } const v = validate_pattern_randomness(&os, 0.8112781244591328, 2); assert(v.code == PatternRandomnessCode.LowEntropy); @@ -215,9 +215,9 @@ test cepr_05_too_few_runs_rejected { // walking (i mod 3000) + 100. The batch rule fires before any // interval is examined. test cepr_06_too_many_rejected { - var os: [4097]IntervalObservation; + var os: [4097]EmissionIntervalObservation; for i in 0..4097 { - os[i] = IntervalObservation { interval_ms: CEPR_INTERVALS_4097[i], }; + os[i] = EmissionIntervalObservation { interval_ms: CEPR_INTERVALS_4097[i], }; } const v = validate_pattern_randomness(&os, 6.0, 2000); assert(v.code == PatternRandomnessCode.TooMany); @@ -228,15 +228,15 @@ test cepr_06_too_many_rejected { // **CEPR-07** - the varied batch accepted: entropy three bits, // seven runs of a required two. test cepr_07_varied_accepted { - const os: [8]IntervalObservation = [8]IntervalObservation{ - IntervalObservation { interval_ms: 1000, }, - IntervalObservation { interval_ms: 2500, }, - IntervalObservation { interval_ms: 800, }, - IntervalObservation { interval_ms: 3200, }, - IntervalObservation { interval_ms: 1500, }, - IntervalObservation { interval_ms: 500, }, - IntervalObservation { interval_ms: 2800, }, - IntervalObservation { interval_ms: 1200, }, + const os: [8]EmissionIntervalObservation = [8]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 1000, }, + EmissionIntervalObservation { interval_ms: 2500, }, + EmissionIntervalObservation { interval_ms: 800, }, + EmissionIntervalObservation { interval_ms: 3200, }, + EmissionIntervalObservation { interval_ms: 1500, }, + EmissionIntervalObservation { interval_ms: 500, }, + EmissionIntervalObservation { interval_ms: 2800, }, + EmissionIntervalObservation { interval_ms: 1200, }, }; const v = validate_pattern_randomness(&os, 3.0, 7); assert(v.code == PatternRandomnessCode.Ok); @@ -245,15 +245,15 @@ test cepr_07_varied_accepted { // **CEPR-08** - the exact minimum accepted: eight intervals, // entropy three bits, eight runs. test cepr_08_exact_minimum_accepted { - const os: [8]IntervalObservation = [8]IntervalObservation{ - IntervalObservation { interval_ms: 500, }, - IntervalObservation { interval_ms: 3000, }, - IntervalObservation { interval_ms: 800, }, - IntervalObservation { interval_ms: 2500, }, - IntervalObservation { interval_ms: 1200, }, - IntervalObservation { interval_ms: 1800, }, - IntervalObservation { interval_ms: 600, }, - IntervalObservation { interval_ms: 2200, }, + const os: [8]EmissionIntervalObservation = [8]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 500, }, + EmissionIntervalObservation { interval_ms: 3000, }, + EmissionIntervalObservation { interval_ms: 800, }, + EmissionIntervalObservation { interval_ms: 2500, }, + EmissionIntervalObservation { interval_ms: 1200, }, + EmissionIntervalObservation { interval_ms: 1800, }, + EmissionIntervalObservation { interval_ms: 600, }, + EmissionIntervalObservation { interval_ms: 2200, }, }; const v = validate_pattern_randomness(&os, 3.0, 8); assert(v.code == PatternRandomnessCode.Ok); @@ -263,16 +263,16 @@ test cepr_08_exact_minimum_accepted { // plus exactly the 300000-millisecond ceiling - entropy 2.197 // over the floor, two runs at the required two. test cepr_09_boundary_interval_accepted { - const os: [9]IntervalObservation = [9]IntervalObservation{ - IntervalObservation { interval_ms: 1000, }, - IntervalObservation { interval_ms: 2500, }, - IntervalObservation { interval_ms: 800, }, - IntervalObservation { interval_ms: 3200, }, - IntervalObservation { interval_ms: 1500, }, - IntervalObservation { interval_ms: 500, }, - IntervalObservation { interval_ms: 2800, }, - IntervalObservation { interval_ms: 1200, }, - IntervalObservation { interval_ms: 300000, }, + const os: [9]EmissionIntervalObservation = [9]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 1000, }, + EmissionIntervalObservation { interval_ms: 2500, }, + EmissionIntervalObservation { interval_ms: 800, }, + EmissionIntervalObservation { interval_ms: 3200, }, + EmissionIntervalObservation { interval_ms: 1500, }, + EmissionIntervalObservation { interval_ms: 500, }, + EmissionIntervalObservation { interval_ms: 2800, }, + EmissionIntervalObservation { interval_ms: 1200, }, + EmissionIntervalObservation { interval_ms: 300000, }, }; const v = validate_pattern_randomness(&os, 2.197159723424149, 2); assert(v.code == PatternRandomnessCode.Ok); @@ -282,9 +282,9 @@ test cepr_09_boundary_interval_accepted { // alternating intervals, entropy 6.12 bits, a hundred runs of a // required twenty-five. test cepr_10_large_batch_accepted { - var os: [100]IntervalObservation; + var os: [100]EmissionIntervalObservation; for i in 0..100 { - os[i] = IntervalObservation { interval_ms: CEPR_INTERVALS_100[i], }; + os[i] = EmissionIntervalObservation { interval_ms: CEPR_INTERVALS_100[i], }; } const v = validate_pattern_randomness(&os, 6.121209564709828, 100); assert(v.code == PatternRandomnessCode.Ok); @@ -295,52 +295,52 @@ test cepr_10_large_batch_accepted { // rules before the entropy floor, and the entropy floor before // the runs floor. test rule_order_matches_original { - const short_zero: [3]IntervalObservation = [3]IntervalObservation{ - IntervalObservation { interval_ms: 0, }, - IntervalObservation { interval_ms: 2000, }, - IntervalObservation { interval_ms: 1500, }, + const short_zero: [3]EmissionIntervalObservation = [3]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 0, }, + EmissionIntervalObservation { interval_ms: 2000, }, + EmissionIntervalObservation { interval_ms: 1500, }, }; const v1 = validate_pattern_randomness(&short_zero, 3.0, 3); assert(v1.code == PatternRandomnessCode.TooFew); - const zero_huge: [8]IntervalObservation = [8]IntervalObservation{ - IntervalObservation { interval_ms: 1000, }, - IntervalObservation { interval_ms: 2500, }, - IntervalObservation { interval_ms: 800, }, - IntervalObservation { interval_ms: 3200, }, - IntervalObservation { interval_ms: 1500, }, - IntervalObservation { interval_ms: 500, }, - IntervalObservation { interval_ms: 2800, }, - IntervalObservation { interval_ms: 400000, }, + const zero_huge: [8]EmissionIntervalObservation = [8]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 1000, }, + EmissionIntervalObservation { interval_ms: 2500, }, + EmissionIntervalObservation { interval_ms: 800, }, + EmissionIntervalObservation { interval_ms: 3200, }, + EmissionIntervalObservation { interval_ms: 1500, }, + EmissionIntervalObservation { interval_ms: 500, }, + EmissionIntervalObservation { interval_ms: 2800, }, + EmissionIntervalObservation { interval_ms: 400000, }, }; const v2 = validate_pattern_randomness(&zero_huge, 3.0, 7); assert(v2.code == PatternRandomnessCode.IntervalTooLarge); assert(v2.idx == 7); - var huge_low: [8]IntervalObservation; + var huge_low: [8]EmissionIntervalObservation; for i in 0..8 { - huge_low[i] = IntervalObservation { interval_ms: 1000, }; + huge_low[i] = EmissionIntervalObservation { interval_ms: 1000, }; } - huge_low[7] = IntervalObservation { interval_ms: 400000, }; + huge_low[7] = EmissionIntervalObservation { interval_ms: 400000, }; const v3 = validate_pattern_randomness(&huge_low, 3.0, 2); assert(v3.code == PatternRandomnessCode.IntervalTooLarge); - var low_runs: [8]IntervalObservation; + var low_runs: [8]EmissionIntervalObservation; for i in 0..8 { - low_runs[i] = IntervalObservation { interval_ms: 1000, }; + low_runs[i] = EmissionIntervalObservation { interval_ms: 1000, }; } const v4 = validate_pattern_randomness(&low_runs, 1.5, 1); assert(v4.code == PatternRandomnessCode.LowEntropy); - const varied_runs: [8]IntervalObservation = [8]IntervalObservation{ - IntervalObservation { interval_ms: 1000, }, - IntervalObservation { interval_ms: 2500, }, - IntervalObservation { interval_ms: 800, }, - IntervalObservation { interval_ms: 3200, }, - IntervalObservation { interval_ms: 1500, }, - IntervalObservation { interval_ms: 500, }, - IntervalObservation { interval_ms: 2800, }, - IntervalObservation { interval_ms: 1200, }, + const varied_runs: [8]EmissionIntervalObservation = [8]EmissionIntervalObservation{ + EmissionIntervalObservation { interval_ms: 1000, }, + EmissionIntervalObservation { interval_ms: 2500, }, + EmissionIntervalObservation { interval_ms: 800, }, + EmissionIntervalObservation { interval_ms: 3200, }, + EmissionIntervalObservation { interval_ms: 1500, }, + EmissionIntervalObservation { interval_ms: 500, }, + EmissionIntervalObservation { interval_ms: 2800, }, + EmissionIntervalObservation { interval_ms: 1200, }, }; const v5 = validate_pattern_randomness(&varied_runs, 3.0, 1); assert(v5.code == PatternRandomnessCode.TooFewRuns);