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
27 changes: 27 additions & 0 deletions codex32_terminal/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Python artifacts
__pycache__/
*.py[cod]
*.so
*.egg-info/
dist/
build/

# Virtual environments
venv/
.venv/
env/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Testing
.pytest_cache/
.coverage
htmlcov/

# OS
.DS_Store
Thumbs.db
87 changes: 86 additions & 1 deletion codex32_terminal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@ This folder contains a terminal-based MVP for validating Codex32 shares, recover

⚠️ Not an ECW yet (no error correction). The codex32 Python library does **not** provide substitution/erasure correction, so this tool does not attempt it. Error-correction should be implemented separately before advertising ECW behavior.

## Setup (Windows PowerShell)
## Setup

### macOS / Linux

```bash
cd codex32_terminal
python3 -m venv venv
source venv/bin/activate
pip install codex32 embit
```

### Windows PowerShell

From the repo root:

Expand All @@ -30,6 +41,13 @@ pip freeze > .\codex32_terminal\requirements.txt

### Box-by-box entry (default)

**macOS / Linux:**
```bash
source venv/bin/activate
python src/main.py
```

**Windows PowerShell:**
```powershell
.\codex32_terminal\venv\Scripts\Activate.ps1 ; python .\codex32_terminal\src\main.py
```
Expand All @@ -43,12 +61,70 @@ Features:

### Full-share paste mode

**macOS / Linux:**
```bash
source venv/bin/activate
python src/main.py --full
```

**Windows PowerShell:**
```powershell
.\codex32_terminal\venv\Scripts\Activate.ps1 ; python .\codex32_terminal\src\main.py --full
```

Paste full shares in sequence. For `k-of-n` shares, the tool will ask for additional shares until the threshold is met.

## Run Tests

Run all test files:

**macOS / Linux:**
```bash
cd codex32_terminal
source venv/bin/activate
python tests/test_vectors.py
python tests/test_share_recovery.py
python tests/test_validation.py
python tests/test_invalid_vectors.py
```

**Windows PowerShell:**
```powershell
.\codex32_terminal\venv\Scripts\Activate.ps1
python tests/test_vectors.py
python tests/test_share_recovery.py
python tests/test_validation.py
python tests/test_invalid_vectors.py
```

### Expected output

When all tests pass, you should see:

```
vector2: seed OK -> spice afford liquid stool forest agent choose draw clinic cram obvious enough
vector3: seed OK -> zoo ivory industry jar praise service talk skirt during october lounge absurd
test_vector2_share_recovery: PASS
test_vector3_share_recovery: PASS
test_vector3_different_share_combination: PASS

All share recovery tests passed!
test_invalid_checksum_rejected: PASS
test_wrong_length_rejected: PASS
test_empty_input_rejected: PASS
test_non_s_share_rejected_for_s_share_validation: PASS
test_sanitize_input: PASS
test_valid_share_accepted: PASS
test_case_insensitive_but_consistent: PASS

All validation tests passed!
test_invalid_checksum_vectors: PASS (3 vectors rejected)
test_bip93_invalid_vectors: PASS (4 vectors rejected)
test_corrupted_single_char: PASS (7 corruptions detected)

All invalid vector tests passed!
```

## Test vectors

Use BIP-93 test vectors to validate recovery:
Expand Down Expand Up @@ -96,6 +172,15 @@ Expected output:
- `tests/test_vectors.py`
- Manual harness for BIP-93 vectors 2/3

- `tests/test_share_recovery.py`
- Tests 2-of-n and 3-of-n share recovery with BIP-93 vectors

- `tests/test_validation.py`
- Tests input validation (checksum, length, empty input, sanitization)

- `tests/test_invalid_vectors.py`
- Tests rejection of BIP-93 invalid test vectors

### Implementation rationale

- **Validation** uses `codex32.Codex32String`, which enforces checksum + header correctness.
Expand Down
115 changes: 115 additions & 0 deletions codex32_terminal/tests/test_invalid_vectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Test rejection of BIP-93 invalid test vectors."""

from __future__ import annotations

import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))

from model import Codex32InputError, parse_codex32_share


# BIP-93 Invalid Test Vectors
# https://github.com/bitcoin/bips/blob/master/bip-0093.mediawiki#user-content-Invalid_test_vectors

INVALID_VECTORS = [
# Invalid checksum (single character errors)
{
"codex32": "ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlx",
"reason": "Invalid checksum - last char changed",
},
{
"codex32": "ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczla",
"reason": "Invalid checksum - last char changed",
},
# Threshold 1 is not valid (must be 0 or 2-9)
{
"codex32": "ms11testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw",
"reason": "Threshold '1' is invalid",
},
# Share index must be 's' when threshold is 0
{
"codex32": "ms10testaxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw",
"reason": "Share index must be 's' when k=0",
},
]

# These should fail checksum validation
CHECKSUM_FAIL_VECTORS = [
"ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlx",
"ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczla",
"MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVX",
]


def test_invalid_checksum_vectors():
"""Test that invalid checksum vectors are rejected."""
for vector in CHECKSUM_FAIL_VECTORS:
try:
# Use expected_len=None to skip length check, focus on checksum
parse_codex32_share(vector, expected_len=None)
raise AssertionError(f"Should have rejected: {vector}")
except Codex32InputError:
pass # Expected

print(f"test_invalid_checksum_vectors: PASS ({len(CHECKSUM_FAIL_VECTORS)} vectors rejected)")


def test_bip93_invalid_vectors():
"""Test all BIP-93 invalid test vectors are rejected."""
passed = 0
for vector in INVALID_VECTORS:
try:
parse_codex32_share(vector["codex32"], expected_len=None)
print(f"FAIL: Should have rejected - {vector['reason']}")
print(f" Input: {vector['codex32']}")
except Codex32InputError:
passed += 1

assert passed == len(INVALID_VECTORS), (
f"Only {passed}/{len(INVALID_VECTORS)} invalid vectors were rejected"
)
print(f"test_bip93_invalid_vectors: PASS ({passed} vectors rejected)")


def test_corrupted_single_char():
"""Test that single character corruption is detected."""
valid = "MS12NAMES6XQGUZTTXKEQNJSJZV4JV3NZ5K3KWGSPHUH6EVW"

# Test corruption at key positions:
# 0=HRP start, 5=threshold, 10=identifier, 20=payload middle,
# 30=payload, 40=near checksum, 47=last char (checksum)
positions_to_test = [0, 5, 10, 20, 30, 40, 47]
corruptions_detected = 0

for pos in positions_to_test:
# Change character at position
char_list = list(valid)
original_char = char_list[pos]
# Pick a different bech32 char
new_char = 'Q' if original_char != 'Q' else 'P'
char_list[pos] = new_char
corrupted = ''.join(char_list)

try:
parse_codex32_share(corrupted, expected_len=None)
except Codex32InputError:
corruptions_detected += 1

assert corruptions_detected == len(positions_to_test), (
f"Only detected {corruptions_detected}/{len(positions_to_test)} corruptions"
)
print(f"test_corrupted_single_char: PASS ({corruptions_detected} corruptions detected)")


def main():
test_invalid_checksum_vectors()
test_bip93_invalid_vectors()
test_corrupted_single_char()
print("\nAll invalid vector tests passed!")


if __name__ == "__main__":
main()
Loading