Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/developer/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/codex32/checksums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)


Expand Down
2 changes: 1 addition & 1 deletion src/codex32/correction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions tests/test_bip93.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
]
Expand All @@ -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)
Expand Down
18 changes: 10 additions & 8 deletions tests/test_crc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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),
)


Expand Down
Loading