From 616e2b084fd795632d9158f6ab8a332980a93eb5 Mon Sep 17 00:00:00 2001 From: dmitrii-f-t27 Date: Fri, 16 Oct 2026 09:42:59 -0300 Subject: [PATCH] spec(port): port the padding payload entropy minimum guard (Refs #8884) The twenty-third W2 target of the CR-CHAT-04 ring tracker: the payload entropy rules - the batch ceiling, then per record the zero-length probe, the length floor and ceiling, the entropy floor and the repeated-hash scan - as the D5 verdict with every payload field, the original's rule order, and all ten PPEM tests transcribed plus helper and rule-order tests. The original's Vec payload becomes its length in the record and the Shannon entropy over the bytes arrives as a number the caller computed - the fill-byte-rainness lane convention for statistics measured over byte buffers; no payload byte reaches the spec. The BTreeSet dedup becomes a scan over the visited records. Checks (conventions 0-14): t27b test 13 passed 0 failed; t27c test-report 13/13, 0 vacuous; gen-rust output compiles under rustc; coverage Untested 0; ASCII only; seal saved, all hashes MATCH; --- ...padding_payload_entropy_minimum_guard.json | 28 ++ docs/reports/t27b_expectations.json | 1 + .../padding_payload_entropy_minimum_guard.t27 | 305 ++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 .trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard.json create mode 100644 specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27 diff --git a/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard.json b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard.json new file mode 100644 index 0000000000..e253577a07 --- /dev/null +++ b/.trinity/seals/src_port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_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:87890c7ffd465adceacca323c30d5279583551cb8cb3cad31b1e6489d0bcb6bc", + "gen_hash_rust": "sha256:d1560e79d4ad438284274e2763c329d43d703f9ee12b87ad62fef4e934dfe808", + "gen_hash_verilog": "sha256:51e9cce2f4401bcce69d3502af6f968b590d65b5675300fca575af32b34d63d9", + "gen_hash_zig": "sha256:370872476e7a40802b33016fce49a99224465013f4d130e710b083d0b6ae7afd", + "module": "port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-16T12:41:31Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:64c82bb77b590befc7892383a699b459fba33b329727b600860eafca8e56a64c", + "spec_path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_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 c041cbab56..1908bf8111 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -853,6 +853,7 @@ {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-02/src/epoch_rollover_wraparound_guard.t27", "t27b": "pass"}, {"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/unmerged_leaves_bound_guard.t27", "t27b": "pass"}, + {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_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/cover_decoy_ratio_governor.t27", "t27b": "pass"}, {"path": "specs/port/trios/crates/trios-chat/rings/CR-CHAT-LAWS/src/lib.t27", "t27b": "pass"}, diff --git a/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27 b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27 new file mode 100644 index 0000000000..f8b1280fc8 --- /dev/null +++ b/specs/port/trios/crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.t27 @@ -0,0 +1,305 @@ +module port::trios::crates::trios-chat::rings::CR-CHAT-04::src::padding_payload_entropy_minimum_guard; + +// Port of gHashTag/trios crates/trios-chat/rings/CR-CHAT-04/src/padding_payload_entropy_minimum_guard.rs +// (CR-CHAT-04 - Padding payload entropy minimum guard, Wave-116 +// Lane B, requirement R-CHAT-4). +// +// Padded payloads must carry minimum entropy: all-zero padding or +// low-entropy runs are distinguishable from real ciphertext by a +// chi-squared probe, fingerprint the device by their pattern, and +// open a compression side channel. The rules, in the original's +// order: the batch ceiling, then per record the zero-length +// probe, the length floor and ceiling, the entropy floor and the +// repeated-hash scan. +// +// The original returns Result<(), PayloadEntropyError>. 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 - BelowMin, TooShort, TooLong, ZeroLength, DuplicateHash, +// TooMany - with every payload field: idx and the measured/min +// entropy for BelowMin, idx and the got/min or got/max lengths +// for the length rules, idx for the zero and duplicate rules, and +// got and max for the batch ceiling. +// +// The original's Vec payload becomes its length in the record, +// and the Shannon entropy over the bytes arrives as a number the +// caller computed - the fill-byte-rainness lane convention for +// statistics measured over byte buffers (no payload byte reaches +// the spec; the hash stays the fixed [32]u8 form). The original's +// BTreeSet dedup becomes a scan over the records already visited. + +// Minimum entropy in bits per byte. +pub const PPEM_MIN_ENTROPY: f64 = 3.0; + +// Minimum payload length. +pub const PPEM_MIN_LEN: usize = 32; + +// Maximum payload length. +pub const PPEM_MAX_LEN: usize = 16384; + +// Maximum payloads per batch. +pub const PPEM_MAX_PAYLOADS: usize = 1024; + +// Hash length for dedup. +pub const PPEM_HASH_LEN: usize = 32; + +// A padded payload record: the payload's length, the entropy the +// caller measured over its bytes, and its dedup hash. +pub struct PayloadRecord { + payload_len: usize, + entropy: f64, + payload_hash: [32]u8, +} + +// Verdict codes: Ok(()) and PayloadEntropyError's variants, in declaration order. +pub enum PayloadEntropyCode { + Ok, + BelowMin, + TooShort, + TooLong, + ZeroLength, + DuplicateHash, + TooMany, +} + +// The verdict of the payload entropy validation, with every error +// payload field. +pub struct PayloadEntropyVerdict { + code: PayloadEntropyCode, + idx: usize, + got: usize, + bound: usize, + got_f: f64, + bound_f: f64, +} + +// Whole-form equality over the fixed thirty-two hash bytes. +pub fn hashes_equal(a: [32]u8, b: [32]u8) -> bool { + for i in 0..32 { + if (a[i] != b[i]) { + return false; + } + } + return true; +} + +// Whether record i repeats an earlier hash - the original's +// BTreeSet insert probe, answered by a scan. +pub fn hash_seen_before(records: []const PayloadRecord, i: usize) -> bool { + for j in 0..i { + if (hashes_equal(records[j].payload_hash, records[i].payload_hash)) { + return true; + } + } + return false; +} + +// [VERIFIED] Validate padding payload entropy minimum. Returns Ok +// only when every rule holds, checked in the original's order: +// the batch ceiling, then per record the zero-length probe, the +// length floor and ceiling, the entropy floor and the repeated +// hash. +pub fn validate_payload_entropy(records: []const PayloadRecord) -> PayloadEntropyVerdict { + if (records.len > PPEM_MAX_PAYLOADS) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.TooMany, idx: 0, got: records.len, bound: PPEM_MAX_PAYLOADS, got_f: 0.0, bound_f: 0.0, }; + } + for i in 0..records.len { + if (records[i].payload_len == 0) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.ZeroLength, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + if (records[i].payload_len < PPEM_MIN_LEN) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.TooShort, idx: i, got: records[i].payload_len, bound: PPEM_MIN_LEN, got_f: 0.0, bound_f: 0.0, }; + } + if (records[i].payload_len > PPEM_MAX_LEN) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.TooLong, idx: i, got: records[i].payload_len, bound: PPEM_MAX_LEN, got_f: 0.0, bound_f: 0.0, }; + } + if (records[i].entropy < PPEM_MIN_ENTROPY) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.BelowMin, idx: i, got: 0, bound: 0, got_f: records[i].entropy, bound_f: PPEM_MIN_ENTROPY, }; + } + if (hash_seen_before(records, i)) { + return PayloadEntropyVerdict { code: PayloadEntropyCode.DuplicateHash, idx: i, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; + } + } + return PayloadEntropyVerdict { code: PayloadEntropyCode.Ok, idx: 0, got: 0, bound: 0, got_f: 0.0, bound_f: 0.0, }; +} + +// The dedup hashes of the fixtures, one byte repeated thirty-two +// times - the original's hash(byte) helper, entry by entry. +pub const HASH_01: [32]u8 = [32]u8{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,}; +pub const HASH_02: [32]u8 = [32]u8{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,}; + +// The leading hash bytes of the too-many fixture: the original's +// (i as u8).wrapping_add(1) over 0..=1024, one byte per record. +pub const PPEM_HASH_BYTES_1025: [1025]u8 = [1025]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, 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,}; + +// **PPEM-01** - below-min entropy rejected: the all-zero payload +// of the original measures entropy zero against the floor of +// three bits per byte. +test ppem_01_below_min_rejected { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.BelowMin); + assert(v.idx == 0); + assert(v.got_f == 0.0); + assert(v.bound_f == 3.0); +} + +// **PPEM-02** - too short rejected: sixteen bytes of a required +// thirty-two. +test ppem_02_too_short_rejected { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16, entropy: 4.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.TooShort); + assert(v.idx == 0); + assert(v.got == 16); + assert(v.bound == 32); +} + +// **PPEM-03** - too long rejected: 16385 bytes of a ceiling 16384. +test ppem_03_too_long_rejected { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16385, entropy: 8.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.TooLong); + assert(v.idx == 0); + assert(v.got == 16385); + assert(v.bound == 16384); +} + +// **PPEM-04** - zero length rejected. +test ppem_04_zero_length_rejected { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 0, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.ZeroLength); + assert(v.idx == 0); +} + +// **PPEM-05** - duplicate hash rejected: the second record repeats +// the first's hash under a different payload. +test ppem_05_duplicate_rejected { + const records: [2]PayloadRecord = [2]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.DuplicateHash); + assert(v.idx == 1); +} + +// **PPEM-06** - too many rejected: 1025 records of a maximum +// 1024, built in a loop like the original's, each carrying a +// sixty-four-byte payload (entropy six) and a distinct leading +// hash byte. The batch rule fires before any record is examined. +test ppem_06_too_many_rejected { + var records: [1025]PayloadRecord; + for i in 0..1025 { + var h: [32]u8 = [32]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,}; + h[0] = PPEM_HASH_BYTES_1025[i]; + records[i] = PayloadRecord { payload_len: 64, entropy: 6.0, payload_hash: h, }; + } + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.TooMany); + assert(v.got == 1025); + assert(v.bound == 1024); +} + +// **PPEM-07** - the valid batch accepted: two high-entropy +// payloads, distinct hashes. +test ppem_07_valid_accepted { + const records: [2]PayloadRecord = [2]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + PayloadRecord { payload_len: 1024, entropy: 8.0, payload_hash: HASH_02, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.Ok); +} + +// **PPEM-08** - an empty batch accepted. +test ppem_08_empty_accepted { + const records: [0]PayloadRecord = [0]PayloadRecord{}; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.Ok); +} + +// **PPEM-09** - the min-length boundary accepted: thirty-two +// bytes, entropy five. +test ppem_09_min_length_accepted { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 32, entropy: 5.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.Ok); +} + +// **PPEM-10** - the max-length boundary accepted: 16384 bytes, +// entropy eight. +test ppem_10_max_length_accepted { + const records: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16384, entropy: 8.0, payload_hash: HASH_01, }, + }; + const v = validate_payload_entropy(&records); + assert(v.code == PayloadEntropyCode.Ok); +} + +// The two helpers have direct tests (coverage: Untested 0). +test hashes_equal_answers { + assert(hashes_equal(HASH_01, HASH_01)); + assert(!hashes_equal(HASH_01, HASH_02)); +} + +test hash_seen_before_answers { + const records: [3]PayloadRecord = [3]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_02, }, + PayloadRecord { payload_len: 256, entropy: 8.0, payload_hash: HASH_01, }, + }; + assert(!hash_seen_before(&records, 0)); + assert(!hash_seen_before(&records, 1)); + assert(hash_seen_before(&records, 2)); +} + +// Rule order is the original's: the zero-length probe before the +// length floor, the length floor before the entropy floor, the +// length ceiling before the entropy floor, and the entropy floor +// before the duplicate scan. The entropy floor admits its exact +// boundary. +test rule_order_matches_original { + const zero_short: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 0, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v1 = validate_payload_entropy(&zero_short); + assert(v1.code == PayloadEntropyCode.ZeroLength); + + const short_low: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v2 = validate_payload_entropy(&short_low); + assert(v2.code == PayloadEntropyCode.TooShort); + + const long_low: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 16385, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v3 = validate_payload_entropy(&long_low); + assert(v3.code == PayloadEntropyCode.TooLong); + + const twins_low: [2]PayloadRecord = [2]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 0.0, payload_hash: HASH_01, }, + PayloadRecord { payload_len: 256, entropy: 0.0, payload_hash: HASH_01, }, + }; + const v4 = validate_payload_entropy(&twins_low); + assert(v4.code == PayloadEntropyCode.BelowMin); + assert(v4.idx == 0); + + const boundary: [1]PayloadRecord = [1]PayloadRecord{ + PayloadRecord { payload_len: 256, entropy: 3.0, payload_hash: HASH_01, }, + }; + const v5 = validate_payload_entropy(&boundary); + assert(v5.code == PayloadEntropyCode.Ok); +}