From 403473e6cb94f548f6c3a303e72f2784e0384078 Mon Sep 17 00:00:00 2001 From: Maciej Myszkiewicz Date: Sun, 11 Oct 2026 10:09:04 +0200 Subject: [PATCH 1/2] test: Reed-Solomon cannot see that the derandomiser was skipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pipeline is sync, derandomise, Reed-Solomon, and the ordering has been wrong in this repository's own prose once already. The reason it matters is not obvious and is now a test rather than a sentence. RS(255,223) is linear over GF(2^8), so the sum of two codewords is a codeword, and over GF(2) that sum is XOR. The 255-octet randomiser sequence of CCSDS 131.0-B-5 10.4 is itself a codeword. A randomised frame is therefore `codeword XOR sequence` — a codeword, exactly, with zero syndromes. So a block that has not been derandomised decodes clean, reports zero corrections, and disagrees with the frame it came from in all 255 of its 255 symbols. The error-correcting code is blind to the one mistake somebody reordering this pipeline would make. The test asserts all three: the sequence decodes as a codeword, arbitrary bytes of the same length do not, and a randomised real frame comes back Ok(0) and wrong everywhere. --- crates/xtce-gs-link/src/rs.rs | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/crates/xtce-gs-link/src/rs.rs b/crates/xtce-gs-link/src/rs.rs index 0ed6533..4e69fec 100644 --- a/crates/xtce-gs-link/src/rs.rs +++ b/crates/xtce-gs-link/src/rs.rs @@ -1450,4 +1450,57 @@ mod tests { } } } + /// Reed-Solomon cannot tell you that you forgot to derandomise. + /// + /// The pipeline is `synchronise → derandomise → Reed-Solomon`, and the temptation is to + /// put the error-correcting code first, on the theory that a check this strong would + /// notice a block that had not been derandomised yet. It does not, and the reason is + /// worth stating once where somebody reordering the pipeline will read it. + /// + /// The randomiser sequence of CCSDS 131.0-B-5 §10.4 is 255 octets. An RS(255,223) + /// codeword is 255 symbols over GF(2^8), and this code is linear: the sum of two + /// codewords is a codeword, and over GF(2) that sum is XOR. The sequence happens to be a + /// codeword itself — so a randomised frame, which is `codeword XOR sequence`, is also a + /// codeword. Exactly. Zero syndromes, zero corrections, and a block that disagrees with + /// the frame it came from in every single symbol. + /// + /// So the only thing standing between a misordered pipeline and 255 bytes of confident + /// nonsense is the order itself. + #[test] + fn reed_solomon_cannot_see_that_the_derandomiser_was_skipped() { + let rs = ReedSolomon::ccsds(32, 1).expect("RS(255,223) is a CCSDS code"); + + // The premise: the randomiser sequence is a codeword. + let mut sequence = crate::derand::sequence(); + assert_eq!(rs.decode(&mut sequence), Ok(0)); + + // And arbitrary bytes of the same length are not, so that is not a vacuous check. + let mut arbitrary = [0u8; CODEWORD_SYMBOLS]; + let mut state = 1u32; + for byte in &mut arbitrary { + state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345); + *byte = (state >> 16) as u8; + } + assert_eq!(rs.decode(&mut arbitrary), Err(RsError::Uncorrectable)); + + // The consequence: a real frame, encoded and then randomised, decodes clean. + let frame: Vec = (0..223u32) + .map(|i| (i.wrapping_mul(37) ^ 0xA5) as u8) + .collect(); + let mut encoded = Vec::new(); + rs.encode(&frame, &mut encoded) + .expect("223 data symbols is the code's data length"); + + let mut skipped_the_derandomiser = encoded.clone(); + crate::derand::randomize(&mut skipped_the_derandomiser); + assert_eq!(rs.decode(&mut skipped_the_derandomiser), Ok(0)); + + // Clean, and wrong in every symbol. + let differing = encoded + .iter() + .zip(&skipped_the_derandomiser) + .filter(|(sent, got)| sent != got) + .count(); + assert_eq!(differing, CODEWORD_SYMBOLS); + } } From e8013158f132d643ec0c5c42a2a9208db2959194 Mon Sep 17 00:00:00 2001 From: Maciej Myszkiewicz Date: Sun, 11 Oct 2026 10:13:26 +0200 Subject: [PATCH 2/2] test: the shorter CCSDS code does not escape it either E=16 and E=32 take consecutive roots from the same generator, so RS(255,223) is a subcode of RS(255,239) and the trap holds for both. Asserted rather than reasoned about, and the frame length now comes from the code instead of being 223 twice. --- crates/xtce-gs-link/src/rs.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/xtce-gs-link/src/rs.rs b/crates/xtce-gs-link/src/rs.rs index 4e69fec..68d3b69 100644 --- a/crates/xtce-gs-link/src/rs.rs +++ b/crates/xtce-gs-link/src/rs.rs @@ -1468,7 +1468,17 @@ mod tests { /// nonsense is the order itself. #[test] fn reed_solomon_cannot_see_that_the_derandomiser_was_skipped() { - let rs = ReedSolomon::ccsds(32, 1).expect("RS(255,223) is a CCSDS code"); + // Both codes CCSDS 131.0-B-5 defines, because the first question anybody asks is + // whether the shorter one escapes it. It does not: E=16 and E=32 take consecutive + // roots from the same generator, so RS(255,223) is a subcode of RS(255,239) and a + // codeword of the longer code is a codeword of the shorter one. + for parity in [16, 32] { + check_the_randomiser_is_invisible(parity); + } + } + + fn check_the_randomiser_is_invisible(parity: usize) { + let rs = ReedSolomon::ccsds(parity, 1).expect("a CCSDS code"); // The premise: the randomiser sequence is a codeword. let mut sequence = crate::derand::sequence(); @@ -1484,12 +1494,12 @@ mod tests { assert_eq!(rs.decode(&mut arbitrary), Err(RsError::Uncorrectable)); // The consequence: a real frame, encoded and then randomised, decodes clean. - let frame: Vec = (0..223u32) + let frame: Vec = (0..rs.data_length() as u32) .map(|i| (i.wrapping_mul(37) ^ 0xA5) as u8) .collect(); let mut encoded = Vec::new(); rs.encode(&frame, &mut encoded) - .expect("223 data symbols is the code's data length"); + .expect("the frame is exactly the code's data length"); let mut skipped_the_derandomiser = encoded.clone(); crate::derand::randomize(&mut skipped_the_derandomiser);