diff --git a/docs/developer/api.md b/docs/developer/api.md index 51c9fb5c..0289bd65 100644 --- a/docs/developer/api.md +++ b/docs/developer/api.md @@ -554,12 +554,19 @@ The reproducible bit convention matters as much as the polynomial name: - the generator integers encode the lower polynomial coefficients (`1` for CRC1 and `0b11` for CRC2–CRC4); the leading `x^p` coefficient is implicit; - `p` zero bits are appended before reading the result; -- the final XOR/residue constant is zero; +- the final XOR is all ones for CRC2–CRC4 and zero for CRC1; - the `p` result bits are emitted register-most-significant bit first and become the otherwise discarded payload bits. Representative all-zero seed outputs for lengths 16 through 20 bytes are -`2, 6, 1, 2, 0`, corresponding to `2, 4, 1, 3, 0` padding bits. +`1, 9, 1, 5, 0`, corresponding to `2, 4, 1, 3, 0` padding bits. + +The all-ones XOR matches the spare-bit CRC of the codex32 key-expressions draft. +Without it, a 24-byte seed extended with zero bits to 64 bytes would keep valid +padding, since both sizes use CRC3. With it, the extended padding never +matches, because the 320 added bits are not a multiple of CRC3's period of 7. +CRC1 stays without an XOR because an XOR there would give every all-zero +28-byte seed valid padding. The polynomials appear in the [Koopman CRC catalogue](https://users.ece.cmu.edu/~koopman/crc/index.html), including its [CRC-3](https://users.ece.cmu.edu/~koopman/crc/crc3.html) and diff --git a/src/codex32/checksums.py b/src/codex32/checksums.py index 6061d1e7..07121919 100644 --- a/src/codex32/checksums.py +++ b/src/codex32/checksums.py @@ -84,11 +84,11 @@ def create(self, values: list[int] | tuple[int, ...]) -> list[int]: None, # ``_Checksum`` consumes input bits most-significant bit first. With its # implicit leading term, these generator values spell x+1, x^2+x+1, - # x^3+x+1, and x^4+x+1 respectively. + # x^3+x+1, and x^4+x+1 respectively. CRC2-CRC4 end with an all-ones XOR. _Checksum("CRC1", (1,), 1, 0), - _Checksum("CRC2", (3,), 2, 0), - _Checksum("CRC3", (3,), 3, 0), - _Checksum("CRC4", (3,), 4, 0), + _Checksum("CRC2", (3,), 2, 0b11), + _Checksum("CRC3", (3,), 3, 0b111), + _Checksum("CRC4", (3,), 4, 0b1111), ) diff --git a/src/codex32/correction.py b/src/codex32/correction.py index 39fc2627..13e5fc97 100644 --- a/src/codex32/correction.py +++ b/src/codex32/correction.py @@ -802,7 +802,7 @@ def _best( tied = [item for item in tied if len(item.artifact.text) in (48, 74)] hamming = min(item.addend_hamming_weight for item in tied) tied = [item for item in tied if item.addend_hamming_weight == hamming] - hints: tuple[Callable[[CorrectionCandidate], bool | None], ...] = ((lambda item: item.crc_padding_match),) + hints: tuple[Callable[[CorrectionCandidate], bool | None], ...] = (lambda item: item.crc_padding_match,) if fingerprint_match is not None: hints += (fingerprint_match,) for hint in hints: diff --git a/tests/test_bip93.py b/tests/test_bip93.py index a57b1f05..65ae35fc 100644 --- a/tests/test_bip93.py +++ b/tests/test_bip93.py @@ -60,10 +60,10 @@ def test_vector_2_public_api_derives_and_recovers() -> None: def test_vector_3_factory_and_public_derivation() -> None: seed = bytes.fromhex(VECTOR_3["secret_hex"]) - secret = MasterSeed.from_seed(seed, identifier="cash", threshold=3) - assert secret.text == VECTOR_3["secret_s"] + generated = MasterSeed.from_seed(seed, identifier="cash", threshold=3) + assert generated.text == VECTOR_3["secret_s_alternate_3"] basis = [ - secret, + parse_codex32(VECTOR_3["secret_s"]), parse_codex32(VECTOR_3["share_a"]), parse_codex32(VECTOR_3["share_c"]), ] @@ -78,7 +78,7 @@ def test_vector_3_factory_and_public_derivation() -> None: def test_vector_4_all_arbitrary_parsed_padding() -> None: seed = bytes.fromhex(VECTOR_4["secret_hex"]) generated = MasterSeed.from_seed(seed, identifier="leet") - assert generated.text == VECTOR_4["secret_s_alternate_8"] + assert generated.text == VECTOR_4["secret_s_alternate_7"] for pad_value in range(16): alternate = parse_codex32(VECTOR_4[f"secret_s_alternate_{pad_value}"]) assert isinstance(alternate, MasterSeed) diff --git a/tests/test_crc.py b/tests/test_crc.py index cd0ace8b..2de7f149 100644 --- a/tests/test_crc.py +++ b/tests/test_crc.py @@ -17,9 +17,9 @@ ( (bytes(range(20)), 0, 0), (bytes(range(18)), 1, 0), - (bytes(range(16)), 2, 2), - (bytes(range(19)), 3, 7), - (bytes(range(17)), 4, 11), + (bytes(range(16)), 2, 1), + (bytes(range(19)), 3, 0), + (bytes(range(17)), 4, 4), ), ) def test_frozen_crc_padding_values(data: bytes, padding_bits: int, expected: int) -> None: @@ -31,12 +31,14 @@ def test_frozen_crc_padding_values(data: bytes, padding_bits: int, expected: int def test_compact_crc_definitions_are_unchanged() -> None: assert _CRC[0] is None assert tuple( - (checksum.kind, checksum.generators, checksum.length) for checksum in _CRC[1:] if checksum is not None + (checksum.kind, checksum.generators, checksum.length, checksum.constant) + for checksum in _CRC[1:] + if checksum is not None ) == ( - ("CRC1", (1,), 1), - ("CRC2", (3,), 2), - ("CRC3", (3,), 3), - ("CRC4", (3,), 4), + ("CRC1", (1,), 1, 0), + ("CRC2", (3,), 2, 0b11), + ("CRC3", (3,), 3, 0b111), + ("CRC4", (3,), 4, 0b1111), )