Linear Feedback Shift Registers for OCaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add ocaml-lfsr: CCSDS OID LFSR pseudo-noise generator

32-cell LFSR for OID frame randomization per CCSDS 132.0-B-3 Annex D.
Polynomial x^32+x^22+x^2+x+1, verified against CCSDS test vector.
Used for TRANSEC traffic flow confidentiality (CNSSP 12 §14.c).

Generic configurable API (up to 62-bit width) with CCSDS OID preset.

+147 -150
+1
.ocamlformat
··· 1 + version = 0.28.1
+5 -3
lib/lfsr.ml
··· 13 13 mutable state : int; 14 14 } 15 15 16 - let create ~taps ~seed ~width = 16 + let pp ppf t = Format.fprintf ppf "lfsr(%d, 0x%0*X)" t.width (t.width / 4) t.state 17 + 18 + let v ~taps ~seed ~width = 17 19 if width < 1 || width > 62 then 18 - invalid_arg (Printf.sprintf "Lfsr.create: width %d not in [1, 62]" width); 20 + invalid_arg (Printf.sprintf "Lfsr.v: width %d not in [1, 62]" width); 19 21 let mask = if width = 62 then max_int else (1 lsl width) - 1 in 20 22 { taps; width; mask; state = seed land mask } 21 23 ··· 35 37 36 38 Byte packing: MSB-first (first output bit goes to bit 7 of first byte). *) 37 39 let ccsds_oid () = 38 - create ~taps:0xC0000401 ~seed:0xFFFFFFFF ~width:32 40 + v ~taps:0xC0000401 ~seed:0xFFFFFFFF ~width:32 39 41 40 42 (* Parity of an int (1 if odd number of set bits, 0 if even). *) 41 43 let[@inline] parity x =
+7 -2
lib/lfsr.mli
··· 23 23 (** LFSR state. Mutable — each call to {!step}, {!next_byte}, or {!fill} 24 24 advances the internal state. *) 25 25 26 + (** {1 Pretty-printing} *) 27 + 28 + val pp : Format.formatter -> t -> unit 29 + (** Pretty-print the LFSR state (width, current register value in hex). *) 30 + 26 31 (** {1 Creation} *) 27 32 28 - val create : taps:int -> seed:int -> width:int -> t 29 - (** [create ~taps ~seed ~width] creates an LFSR with the given tap mask, 33 + val v : taps:int -> seed:int -> width:int -> t 34 + (** [v ~taps ~seed ~width] creates an LFSR with the given tap mask, 30 35 initial seed, and register width (number of cells, 1–62). 31 36 32 37 [taps] is a bitmask of the tap positions. The feedback bit is the XOR
+1 -145
test/test.ml
··· 1 - (*--------------------------------------------------------------------------- 2 - Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 - SPDX-License-Identifier: MIT 4 - ---------------------------------------------------------------------------*) 5 - 6 - let hex_of_bytes b = 7 - let buf = Buffer.create (Bytes.length b * 3) in 8 - Bytes.iteri 9 - (fun i c -> 10 - if i > 0 then Buffer.add_char buf ' '; 11 - Buffer.add_string buf (Printf.sprintf "%02X" (Char.code c))) 12 - b; 13 - Buffer.contents buf 14 - 15 - let bytes_of_hex s = 16 - let s = String.concat "" (String.split_on_char ' ' s) in 17 - let len = String.length s / 2 in 18 - let buf = Bytes.create len in 19 - for i = 0 to len - 1 do 20 - Bytes.set buf i (Char.chr (int_of_string ("0x" ^ String.sub s (i * 2) 2))) 21 - done; 22 - buf 23 - 24 - (* {1 CCSDS OID test vectors} 25 - 26 - From CCSDS 132.0-B-3 Annex D: first 20 bytes of OID data pattern. 27 - Fibonacci form, seed = all ones. *) 28 - 29 - let ccsds_oid_vector = 30 - "FF FF FF FF 6D B6 D8 61 45 1F 11 F1 97 16 72 3C BE 7E 00 B1" 31 - 32 - let test_ccsds_oid_first_20 () = 33 - let t = Lfsr.ccsds_oid () in 34 - let got = Lfsr.generate t 20 in 35 - let expected = bytes_of_hex ccsds_oid_vector in 36 - Alcotest.(check string) 37 - "CCSDS OID first 20 bytes" (Bytes.to_string expected) (Bytes.to_string got) 38 - 39 - let test_ccsds_oid_first_10 () = 40 - let t = Lfsr.ccsds_oid () in 41 - let got = Lfsr.generate t 10 in 42 - let expected = bytes_of_hex "FF FF FF FF 6D B6 D8 61 45 1F" in 43 - Alcotest.(check string) 44 - "CCSDS OID first 10 bytes" (Bytes.to_string expected) (Bytes.to_string got) 45 - 46 - (* The LFSR should NOT restart between calls — state is continuous. *) 47 - let test_ccsds_oid_continuous () = 48 - let t = Lfsr.ccsds_oid () in 49 - let first = Lfsr.generate t 10 in 50 - let second = Lfsr.generate t 10 in 51 - let t2 = Lfsr.ccsds_oid () in 52 - let all = Lfsr.generate t2 20 in 53 - let combined = Bytes.cat first second in 54 - Alcotest.(check string) 55 - "continuous across calls" (Bytes.to_string all) (Bytes.to_string combined) 56 - 57 - (* fill should produce same output as generate *) 58 - let test_fill_matches_generate () = 59 - let t1 = Lfsr.ccsds_oid () in 60 - let t2 = Lfsr.ccsds_oid () in 61 - let gen = Lfsr.generate t1 100 in 62 - let buf = Bytes.create 120 in 63 - Lfsr.fill t2 buf ~off:10 ~len:100; 64 - Alcotest.(check string) 65 - "fill matches generate" 66 - (Bytes.to_string gen) 67 - (Bytes.sub_string buf 10 100) 68 - 69 - (* {1 Generic LFSR tests} *) 70 - 71 - (* A 4-bit LFSR with polynomial x^4+x+1 (taps 0,1) has period 15. *) 72 - let test_4bit_period () = 73 - let t = Lfsr.create ~taps:0x3 ~seed:0xF ~width:4 in 74 - let initial = Lfsr.state t in 75 - let period = ref 0 in 76 - let found = ref false in 77 - for _ = 1 to 20 do 78 - ignore (Lfsr.step t); 79 - incr period; 80 - if (not !found) && Lfsr.state t = initial then found := true 81 - done; 82 - (* Maximal-length 4-bit LFSR has period 2^4-1 = 15 *) 83 - let t2 = Lfsr.create ~taps:0x3 ~seed:0xF ~width:4 in 84 - for _ = 1 to 15 do 85 - ignore (Lfsr.step t2) 86 - done; 87 - Alcotest.(check int) "period 15 returns to seed" initial (Lfsr.state t2) 88 - 89 - (* Width validation *) 90 - let test_invalid_width () = 91 - Alcotest.check_raises "width 0" (Invalid_argument "Lfsr.create: width 0 not in [1, 62]") 92 - (fun () -> ignore (Lfsr.create ~taps:1 ~seed:1 ~width:0)); 93 - Alcotest.check_raises "width 63" (Invalid_argument "Lfsr.create: width 63 not in [1, 62]") 94 - (fun () -> ignore (Lfsr.create ~taps:1 ~seed:1 ~width:63)) 95 - 96 - (* step returns only 0 or 1 *) 97 - let test_step_output_range () = 98 - let t = Lfsr.ccsds_oid () in 99 - for _ = 1 to 1000 do 100 - let bit = Lfsr.step t in 101 - if bit <> 0 && bit <> 1 then 102 - Alcotest.fail (Printf.sprintf "step returned %d, expected 0 or 1" bit) 103 - done 104 - 105 - (* next_byte returns 0-255 *) 106 - let test_next_byte_range () = 107 - let t = Lfsr.ccsds_oid () in 108 - for _ = 1 to 1000 do 109 - let b = Lfsr.next_byte t in 110 - if b < 0 || b > 255 then 111 - Alcotest.fail (Printf.sprintf "next_byte returned %d" b) 112 - done 113 - 114 - (* 32-bit LFSR with maximal polynomial has period 2^32-1. Verify state 115 - doesn't degenerate to zero (which would be an absorbing state). *) 116 - let test_no_zero_state () = 117 - let t = Lfsr.ccsds_oid () in 118 - for i = 1 to 100_000 do 119 - ignore (Lfsr.step t); 120 - if Lfsr.state t = 0 then 121 - Alcotest.fail (Printf.sprintf "state became zero at step %d" i) 122 - done 123 - 124 - let () = 125 - Alcotest.run "lfsr" 126 - [ 127 - ( "ccsds-oid", 128 - [ 129 - Alcotest.test_case "first 10 bytes" `Quick test_ccsds_oid_first_10; 130 - Alcotest.test_case "first 20 bytes" `Quick test_ccsds_oid_first_20; 131 - Alcotest.test_case "continuous state" `Quick test_ccsds_oid_continuous; 132 - ] ); 133 - ( "generic", 134 - [ 135 - Alcotest.test_case "4-bit period" `Quick test_4bit_period; 136 - Alcotest.test_case "invalid width" `Quick test_invalid_width; 137 - Alcotest.test_case "step range" `Quick test_step_output_range; 138 - Alcotest.test_case "byte range" `Quick test_next_byte_range; 139 - Alcotest.test_case "no zero state" `Quick test_no_zero_state; 140 - ] ); 141 - ( "fill", 142 - [ 143 - Alcotest.test_case "fill matches generate" `Quick test_fill_matches_generate; 144 - ] ); 145 - ] 1 + let () = Alcotest.run "lfsr" [ Test_lfsr.suite ]
+131
test/test_lfsr.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + let hex_of_bytes b = 7 + let buf = Buffer.create (Bytes.length b * 3) in 8 + Bytes.iteri 9 + (fun i c -> 10 + if i > 0 then Buffer.add_char buf ' '; 11 + Buffer.add_string buf (Printf.sprintf "%02X" (Char.code c))) 12 + b; 13 + Buffer.contents buf 14 + 15 + let bytes_of_hex s = 16 + let s = String.concat "" (String.split_on_char ' ' s) in 17 + let len = String.length s / 2 in 18 + let buf = Bytes.create len in 19 + for i = 0 to len - 1 do 20 + Bytes.set buf i (Char.chr (int_of_string ("0x" ^ String.sub s (i * 2) 2))) 21 + done; 22 + buf 23 + 24 + (* {1 CCSDS OID test vectors} 25 + 26 + From CCSDS 132.0-B-3 Annex D: first 20 bytes of OID data pattern. 27 + Fibonacci form, seed = all ones. *) 28 + 29 + let ccsds_oid_vector = 30 + "FF FF FF FF 6D B6 D8 61 45 1F 11 F1 97 16 72 3C BE 7E 00 B1" 31 + 32 + let first_20 () = 33 + let t = Lfsr.ccsds_oid () in 34 + let got = Lfsr.generate t 20 in 35 + let expected = bytes_of_hex ccsds_oid_vector in 36 + Alcotest.(check string) 37 + "CCSDS OID first 20 bytes" (Bytes.to_string expected) (Bytes.to_string got) 38 + 39 + let first_10 () = 40 + let t = Lfsr.ccsds_oid () in 41 + let got = Lfsr.generate t 10 in 42 + let expected = bytes_of_hex "FF FF FF FF 6D B6 D8 61 45 1F" in 43 + Alcotest.(check string) 44 + "CCSDS OID first 10 bytes" (Bytes.to_string expected) (Bytes.to_string got) 45 + 46 + (* The LFSR should NOT restart between calls — state is continuous. *) 47 + let continuous () = 48 + let t = Lfsr.ccsds_oid () in 49 + let first = Lfsr.generate t 10 in 50 + let second = Lfsr.generate t 10 in 51 + let t2 = Lfsr.ccsds_oid () in 52 + let all = Lfsr.generate t2 20 in 53 + let combined = Bytes.cat first second in 54 + Alcotest.(check string) 55 + "continuous across calls" (Bytes.to_string all) (Bytes.to_string combined) 56 + 57 + (* fill should produce same output as generate *) 58 + let fill_matches_generate () = 59 + let t1 = Lfsr.ccsds_oid () in 60 + let t2 = Lfsr.ccsds_oid () in 61 + let gen = Lfsr.generate t1 100 in 62 + let buf = Bytes.create 120 in 63 + Lfsr.fill t2 buf ~off:10 ~len:100; 64 + Alcotest.(check string) 65 + "fill matches generate" 66 + (Bytes.to_string gen) 67 + (Bytes.sub_string buf 10 100) 68 + 69 + (* {1 Generic LFSR tests} *) 70 + 71 + (* A 4-bit LFSR with polynomial x^4+x+1 (taps 0,1) has period 15. *) 72 + let period_4bit () = 73 + let t = Lfsr.v ~taps:0x3 ~seed:0xF ~width:4 in 74 + let initial = Lfsr.state t in 75 + (* Maximal-length 4-bit LFSR has period 2^4-1 = 15 *) 76 + let t2 = Lfsr.v ~taps:0x3 ~seed:0xF ~width:4 in 77 + for _ = 1 to 15 do 78 + ignore (Lfsr.step t2) 79 + done; 80 + Alcotest.(check int) "period 15 returns to seed" initial (Lfsr.state t2) 81 + 82 + (* Width validation *) 83 + let invalid_width () = 84 + Alcotest.check_raises "width 0" 85 + (Invalid_argument "Lfsr.v: width 0 not in [1, 62]") 86 + (fun () -> ignore (Lfsr.v ~taps:1 ~seed:1 ~width:0)); 87 + Alcotest.check_raises "width 63" 88 + (Invalid_argument "Lfsr.v: width 63 not in [1, 62]") 89 + (fun () -> ignore (Lfsr.v ~taps:1 ~seed:1 ~width:63)) 90 + 91 + (* step returns only 0 or 1 *) 92 + let step_range () = 93 + let t = Lfsr.ccsds_oid () in 94 + for _ = 1 to 1000 do 95 + let bit = Lfsr.step t in 96 + if bit <> 0 && bit <> 1 then 97 + Alcotest.fail (Printf.sprintf "step returned %d, expected 0 or 1" bit) 98 + done 99 + 100 + (* next_byte returns 0-255 *) 101 + let byte_range () = 102 + let t = Lfsr.ccsds_oid () in 103 + for _ = 1 to 1000 do 104 + let b = Lfsr.next_byte t in 105 + if b < 0 || b > 255 then 106 + Alcotest.fail (Printf.sprintf "next_byte returned %d" b) 107 + done 108 + 109 + (* 32-bit LFSR with maximal polynomial has period 2^32-1. Verify state 110 + doesn't degenerate to zero (which would be an absorbing state). *) 111 + let no_zero_state () = 112 + let t = Lfsr.ccsds_oid () in 113 + for i = 1 to 100_000 do 114 + ignore (Lfsr.step t); 115 + if Lfsr.state t = 0 then 116 + Alcotest.fail (Printf.sprintf "state became zero at step %d" i) 117 + done 118 + 119 + let suite = 120 + ( "lfsr", 121 + [ 122 + Alcotest.test_case "ccsds oid first 10" `Quick first_10; 123 + Alcotest.test_case "ccsds oid first 20" `Quick first_20; 124 + Alcotest.test_case "ccsds oid continuous" `Quick continuous; 125 + Alcotest.test_case "fill matches generate" `Quick fill_matches_generate; 126 + Alcotest.test_case "4-bit period" `Quick period_4bit; 127 + Alcotest.test_case "invalid width" `Quick invalid_width; 128 + Alcotest.test_case "step range" `Quick step_range; 129 + Alcotest.test_case "byte range" `Quick byte_range; 130 + Alcotest.test_case "no zero state" `Quick no_zero_state; 131 + ] )
+2
test/test_lfsr.mli
··· 1 + val suite : string * unit Alcotest.test_case list 2 + (** LFSR test suite. *)