diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard.json new file mode 100644 index 0000000000..19e6a1dbe8 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_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:8f61a351374d4a00c669c61f176b0b74c51f64fd93a316df6c8b490589cba95a", + "gen_hash_rust": "sha256:b864dbb042f38b3a78c8fe6243a593d51d287a16c1c9d70801f5dc048f9fc337", + "gen_hash_verilog": "sha256:f9e5656aad11599fea0ea8cb0f851e887f71b1ae27e832f65219e549a8816f62", + "gen_hash_zig": "sha256:24b90070ec9b4b213eb788eced8bf1b124cf88fae569eaa941c5aa953272055c", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T13:10:11Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:36bd058647fc50b62f90f5d45704656b55e70fb1e3e742ce2edb99239decc07f", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_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 cfbb74965e..5086d83da8 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -900,6 +900,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/padding_response_timing_fingerprint_guard.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"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27 new file mode 100644 index 0000000000..9d1ca03e48 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.t27 @@ -0,0 +1,476 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_response_timing_fingerprint_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_response_timing_fingerprint_guard.rs +// (CR-CHAT-04 - Padding response timing fingerprint guard, Wave-143 +// Lane A, requirement R-CHAT-4). +// +// Padded and unpadded responses must cost the same time: an +// observer timing the server reads off whether padding applied, +// infers short from fast, and builds a binary classifier for the +// message type. The rules, in the original's order: the batch +// ceiling, the empty batch (accepted before the sample floor, +// exactly as the original places it), the sample floor, then per +// sample the zero-id probe and the repeated-id scan, then over +// the whole batch the mean floor and the coefficient-of- +// variation ceiling - the standard deviation over the mean, +// scaled by a thousand and compared as the original's integer +// cross-multiplication. +// +// The original returns Result<(), TimingFingerprintError>. 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 - HighCv, TooFew, LowMean, Duplicate, +// ZeroId, TooMany - with every payload field: the measured and +// ceiling CV (x1000) for HighCv, got and min for TooFew, the +// got and min mean for LowMean, idx for the id rules, and got +// and max for the batch ceiling. +// +// The original's u64::isqrt becomes a bisection over the whole +// answer range - thirty-three halvings, exact for every u64, no +// float anywhere. The BTreeSet dedup becomes a scan over the +// samples already visited; the mean, variance and CV stay the +// original's integer u64 arithmetic, the length widening through +// the int cast. + +// Maximum CV numerator (x1000). +pub const PRTF_MAX_CV_NUM: u64 = 200; + +// Maximum CV denominator (x1000). +pub const PRTF_MAX_CV_DEN: u64 = 1000; + +// Minimum samples. +pub const PRTF_MIN_SAMPLES: usize = 8; + +// Minimum mean timing in microseconds. +pub const PRTF_MIN_MEAN_US: u64 = 50; + +// Maximum samples per batch. +pub const PRTF_MAX_SAMPLES: usize = 512; + +// Sample ID length. +pub const PRTF_SAMPLE_ID_LEN: usize = 16; + +// A timing sample record. +pub struct TimingSample { + sample_id: [16]u8, + time_us: u64, +} + +// Verdict codes: Ok(()) and TimingFingerprintError's variants, in declaration order. +pub enum TimingFingerprintCode { + Ok, + HighCv, + TooFew, + LowMean, + Duplicate, + ZeroId, + TooMany, +} + +// The verdict of the timing fingerprint validation, with every +// error payload field. +pub struct TimingFingerprintVerdict { + code: TimingFingerprintCode, + idx: usize, + got: usize, + bound: usize, + got64: u64, + bound64: u64, +} + +// Whole-form equality over the fixed sixteen id bytes. +pub fn ids16_equal(a: [16]u8, b: [16]u8) -> bool { + for i in 0..16 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// The all-zero sample id - the original's [0u8; 16] probe. +pub fn id16_zero(id: [16]u8) -> bool { + for i in 0..16 { + if (id[i] != 0) { + return false; + } + } + return true; +} + +// Whether sample i repeats an earlier id - the original's +// BTreeSet insert probe, answered by a scan. +pub fn id_seen_before(samples: []const TimingSample, i: usize) -> bool { + for j in 0..i { + if (ids16_equal(samples[j].sample_id, samples[i].sample_id)) { + return true; + } + } + return false; +} + +// The integer square root of a u64 - the original's u64::isqrt, +// answered by a bisection over the whole answer range. The +// largest root of any u64 is 4294967295, and thirty-three +// halvings settle the search exactly. +pub fn isqrt64(v: u64) -> u64 { + var lo: u64 = 0; + var hi: u64 = 4294967295; + var mid: u64 = 0; + for _ in 0..33 { + if (lo <= hi) { + mid = (lo + hi) / 2; + if (mid * mid <= v) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + } + return hi; +} + +// [VERIFIED] Validate padding response timing fingerprint. +// Returns Ok only when every rule holds, checked in the +// original's order: the batch ceiling, the empty batch (accepted), +// the sample floor, then per sample the zero id and the repeated +// id, then over the whole batch the mean floor and the CV +// ceiling. +pub fn validate_timing_fingerprint(samples: []const TimingSample) -> TimingFingerprintVerdict { + if (samples.len > PRTF_MAX_SAMPLES) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.TooMany, idx: 0, got: samples.len, bound: PRTF_MAX_SAMPLES, got64: 0, bound64: 0, }; + } + if (samples.len == 0) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.Ok, idx: 0, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (samples.len < PRTF_MIN_SAMPLES) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.TooFew, idx: 0, got: samples.len, bound: PRTF_MIN_SAMPLES, got64: 0, bound64: 0, }; + } + for i in 0..samples.len { + if (id16_zero(samples[i].sample_id)) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.ZeroId, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + if (id_seen_before(samples, i)) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.Duplicate, idx: i, got: 0, bound: 0, got64: 0, bound64: 0, }; + } + } + const len64: u64 = @as(u64, samples.len); + var sum: u64 = 0; + for i in 0..samples.len { + sum = sum + samples[i].time_us; + } + const mean: u64 = sum / len64; + if (mean < PRTF_MIN_MEAN_US) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.LowMean, idx: 0, got: 0, bound: 0, got64: mean, bound64: PRTF_MIN_MEAN_US, }; + } + var var_sum: u64 = 0; + var diff: u64 = 0; + for i in 0..samples.len { + if (samples[i].time_us > mean) { + diff = samples[i].time_us - mean; + } else { + diff = mean - samples[i].time_us; + } + var_sum = var_sum + diff * diff; + } + const std_dev_x1000: u64 = isqrt64(var_sum / len64) * 1000; + var cv_x1000: u64 = 18446744073709551615; + if (mean > 0) { + cv_x1000 = std_dev_x1000 / mean; + } + const max_cv_x1000: u64 = PRTF_MAX_CV_NUM * 1000 / PRTF_MAX_CV_DEN; + if (cv_x1000 > max_cv_x1000) { + return TimingFingerprintVerdict { code: TimingFingerprintCode.HighCv, idx: 0, got: 0, bound: 0, got64: cv_x1000, bound64: max_cv_x1000, }; + } + return TimingFingerprintVerdict { code: TimingFingerprintCode.Ok, idx: 0, got: 0, bound: 0, got64: 0, bound64: 0, }; +} + +// The sample ids of the fixtures, one byte repeated sixteen +// times - the original's sid(byte) helper, entry by entry. +pub const SID_01: [16]u8 = [16]u8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,}; +pub const SID_02: [16]u8 = [16]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; +pub const SID_03: [16]u8 = [16]u8{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,}; +pub const SID_04: [16]u8 = [16]u8{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,}; +pub const SID_05: [16]u8 = [16]u8{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,}; +pub const SID_06: [16]u8 = [16]u8{6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,}; +pub const SID_07: [16]u8 = [16]u8{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,}; +pub const SID_08: [16]u8 = [16]u8{8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,}; +pub const SID_09: [16]u8 = [16]u8{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,}; +pub const SID_10: [16]u8 = [16]u8{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,}; +pub const ZERO_ID16: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + +// The leading id bytes of the thirty-sample fixture. +pub const PRTF_ID_BYTES_30: [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,}; + +// 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..=513 only the last two of those eight bytes can carry a +// nonzero digit. +pub const PRTF_ID6_513: [513]u8 = [513]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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2,}; +pub const PRTF_ID7_513: [513]u8 = [513]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, 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, 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, 0, 1,}; + +// **PRTF-01** - high CV rejected: seven samples at a hundred +// microseconds and one at a hundred thousand - CV x1000 of 2624 +// over the ceiling of 200. +test prtf_01_high_cv_rejected { + const samples: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 100, }, + TimingSample { sample_id: SID_02, time_us: 100, }, + TimingSample { sample_id: SID_03, time_us: 100, }, + TimingSample { sample_id: SID_04, time_us: 100, }, + TimingSample { sample_id: SID_05, time_us: 100, }, + TimingSample { sample_id: SID_06, time_us: 100, }, + TimingSample { sample_id: SID_07, time_us: 100, }, + TimingSample { sample_id: SID_08, time_us: 100000, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.HighCv); + assert(v.got64 == 2624); + assert(v.bound64 == 200); +} + +// **PRTF-02** - too few rejected: seven samples of a required +// eight. +test prtf_02_too_few_rejected { + const samples: [7]TimingSample = [7]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_03, time_us: 1000, }, + TimingSample { sample_id: SID_04, time_us: 1000, }, + TimingSample { sample_id: SID_05, time_us: 1000, }, + TimingSample { sample_id: SID_06, time_us: 1000, }, + TimingSample { sample_id: SID_07, time_us: 1000, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.TooFew); + assert(v.got == 7); + assert(v.bound == 8); +} + +// **PRTF-03** - low mean rejected: ten samples at ten +// microseconds - a mean of ten under the floor of fifty. +test prtf_03_low_mean_rejected { + const samples: [10]TimingSample = [10]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 10, }, + TimingSample { sample_id: SID_02, time_us: 10, }, + TimingSample { sample_id: SID_03, time_us: 10, }, + TimingSample { sample_id: SID_04, time_us: 10, }, + TimingSample { sample_id: SID_05, time_us: 10, }, + TimingSample { sample_id: SID_06, time_us: 10, }, + TimingSample { sample_id: SID_07, time_us: 10, }, + TimingSample { sample_id: SID_08, time_us: 10, }, + TimingSample { sample_id: SID_09, time_us: 10, }, + TimingSample { sample_id: SID_10, time_us: 10, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.LowMean); + assert(v.got64 == 10); + assert(v.bound64 == 50); +} + +// **PRTF-04** - duplicate rejected: the second sample repeats +// the first's id. +test prtf_04_duplicate_rejected { + const samples: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_03, time_us: 1000, }, + TimingSample { sample_id: SID_04, time_us: 1000, }, + TimingSample { sample_id: SID_05, time_us: 1000, }, + TimingSample { sample_id: SID_06, time_us: 1000, }, + TimingSample { sample_id: SID_07, time_us: 1000, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Duplicate); + assert(v.idx == 1); +} + +// **PRTF-05** - zero id rejected under live timings. +test prtf_05_zero_id_rejected { + const samples: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: ZERO_ID16, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_03, time_us: 1000, }, + TimingSample { sample_id: SID_04, time_us: 1000, }, + TimingSample { sample_id: SID_05, time_us: 1000, }, + TimingSample { sample_id: SID_06, time_us: 1000, }, + TimingSample { sample_id: SID_07, time_us: 1000, }, + TimingSample { sample_id: SID_08, time_us: 1000, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.ZeroId); + assert(v.idx == 0); +} + +// **PRTF-06** - too many rejected: 513 samples of a maximum +// 512, 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 sample is examined. +test prtf_06_too_many_rejected { + var samples: [513]TimingSample; + for i in 0..513 { + var id: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + id[6] = PRTF_ID6_513[i]; + id[7] = PRTF_ID7_513[i]; + samples[i] = TimingSample { sample_id: id, time_us: 1000, }; + } + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.TooMany); + assert(v.got == 513); + assert(v.bound == 512); +} + +// **PRTF-07** - the uniform batch accepted: ten samples stepping +// by ten microseconds from a thousand - CV x1000 of 26. +test prtf_07_valid_accepted { + const samples: [10]TimingSample = [10]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1010, }, + TimingSample { sample_id: SID_03, time_us: 1020, }, + TimingSample { sample_id: SID_04, time_us: 1030, }, + TimingSample { sample_id: SID_05, time_us: 1040, }, + TimingSample { sample_id: SID_06, time_us: 1050, }, + TimingSample { sample_id: SID_07, time_us: 1060, }, + TimingSample { sample_id: SID_08, time_us: 1070, }, + TimingSample { sample_id: SID_09, time_us: 1080, }, + TimingSample { sample_id: SID_10, time_us: 1090, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Ok); +} + +// **PRTF-08** - an empty batch accepted, before the sample +// floor, exactly as the original places it. +test prtf_08_empty_accepted { + const samples: [0]TimingSample = [0]TimingSample{}; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Ok); +} + +// **PRTF-09** - the minimum-sample boundary accepted: eight +// samples stepping by five from five hundred. +test prtf_09_min_samples_accepted { + const samples: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 500, }, + TimingSample { sample_id: SID_02, time_us: 505, }, + TimingSample { sample_id: SID_03, time_us: 510, }, + TimingSample { sample_id: SID_04, time_us: 515, }, + TimingSample { sample_id: SID_05, time_us: 520, }, + TimingSample { sample_id: SID_06, time_us: 525, }, + TimingSample { sample_id: SID_07, time_us: 530, }, + TimingSample { sample_id: SID_08, time_us: 535, }, + }; + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Ok); +} + +// **PRTF-10** - many uniform high values accepted: thirty samples +// stepping by two from five thousand. +test prtf_10_many_uniform_accepted { + var samples: [30]TimingSample; + var t: u64 = 5000; + for i in 0..30 { + var id: [16]u8 = [16]u8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,}; + id[0] = PRTF_ID_BYTES_30[i]; + samples[i] = TimingSample { sample_id: id, time_us: t, }; + t = t + 2; + } + const v = validate_timing_fingerprint(&samples); + assert(v.code == TimingFingerprintCode.Ok); +} + +// The four helpers have direct tests (coverage: Untested 0). +test ids16_helpers_answer { + assert(ids16_equal(SID_01, SID_01)); + assert(!ids16_equal(SID_01, SID_02)); + assert(id16_zero(ZERO_ID16)); + assert(!id16_zero(SID_01)); + const samples: [3]TimingSample = [3]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_01, time_us: 1000, }, + }; + assert(!id_seen_before(&samples, 0)); + assert(!id_seen_before(&samples, 1)); + assert(id_seen_before(&samples, 2)); +} + +test isqrt64_answers { + assert(isqrt64(0) == 0); + assert(isqrt64(1) == 1); + assert(isqrt64(3) == 1); + assert(isqrt64(4) == 2); + assert(isqrt64(845) == 29); + assert(isqrt64(125) == 11); + assert(isqrt64(100) == 10); + assert(isqrt64(18446744073709551615) == 4294967295); +} + +// Rule order is the original's: the batch ceiling before any +// sample, the zero id before the duplicate, the duplicate before +// the mean floor, the mean floor before the CV ceiling - and the +// CV ceiling admitting its exact boundary. +test rule_order_matches_original { + var flooded: [513]TimingSample; + for i in 0..513 { + flooded[i] = TimingSample { sample_id: ZERO_ID16, time_us: 1000, }; + } + const v1 = validate_timing_fingerprint(&flooded); + assert(v1.code == TimingFingerprintCode.TooMany); + + const twin_zeros: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: ZERO_ID16, time_us: 1000, }, + TimingSample { sample_id: ZERO_ID16, time_us: 1000, }, + TimingSample { sample_id: SID_02, time_us: 1000, }, + TimingSample { sample_id: SID_03, time_us: 1000, }, + TimingSample { sample_id: SID_04, time_us: 1000, }, + TimingSample { sample_id: SID_05, time_us: 1000, }, + TimingSample { sample_id: SID_06, time_us: 1000, }, + TimingSample { sample_id: SID_07, time_us: 1000, }, + }; + const v2 = validate_timing_fingerprint(&twin_zeros); + assert(v2.code == TimingFingerprintCode.ZeroId); + assert(v2.idx == 0); + + const dup_low_mean: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 10, }, + TimingSample { sample_id: SID_01, time_us: 10, }, + TimingSample { sample_id: SID_02, time_us: 10, }, + TimingSample { sample_id: SID_03, time_us: 10, }, + TimingSample { sample_id: SID_04, time_us: 10, }, + TimingSample { sample_id: SID_05, time_us: 10, }, + TimingSample { sample_id: SID_06, time_us: 10, }, + TimingSample { sample_id: SID_07, time_us: 10, }, + }; + const v3 = validate_timing_fingerprint(&dup_low_mean); + assert(v3.code == TimingFingerprintCode.Duplicate); + assert(v3.idx == 1); + + const low_mean_high_cv: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 0, }, + TimingSample { sample_id: SID_02, time_us: 0, }, + TimingSample { sample_id: SID_03, time_us: 0, }, + TimingSample { sample_id: SID_04, time_us: 0, }, + TimingSample { sample_id: SID_05, time_us: 0, }, + TimingSample { sample_id: SID_06, time_us: 0, }, + TimingSample { sample_id: SID_07, time_us: 0, }, + TimingSample { sample_id: SID_08, time_us: 399, }, + }; + const v4 = validate_timing_fingerprint(&low_mean_high_cv); + assert(v4.code == TimingFingerprintCode.LowMean); + assert(v4.got64 == 49); + + const boundary_cv: [8]TimingSample = [8]TimingSample{ + TimingSample { sample_id: SID_01, time_us: 40, }, + TimingSample { sample_id: SID_02, time_us: 40, }, + TimingSample { sample_id: SID_03, time_us: 40, }, + TimingSample { sample_id: SID_04, time_us: 40, }, + TimingSample { sample_id: SID_05, time_us: 60, }, + TimingSample { sample_id: SID_06, time_us: 60, }, + TimingSample { sample_id: SID_07, time_us: 60, }, + TimingSample { sample_id: SID_08, time_us: 60, }, + }; + const v5 = validate_timing_fingerprint(&boundary_cv); + assert(v5.code == TimingFingerprintCode.Ok); +}