diff --git a/src/validators/crypto_addresses/btc_address.py b/src/validators/crypto_addresses/btc_address.py index f781f3b9..c404614c 100644 --- a/src/validators/crypto_addresses/btc_address.py +++ b/src/validators/crypto_addresses/btc_address.py @@ -9,16 +9,20 @@ def _decode_base58(addr: str): - """Decode base58.""" + """Decode base58, preserving each leading one as a zero byte.""" alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - return sum((58**enm) * alphabet.index(idx) for enm, idx in enumerate(addr[::-1])) + decoded = sum((58**enm) * alphabet.index(idx) for enm, idx in enumerate(addr[::-1])) + zero_bytes = len(addr) - len(addr.lstrip("1")) + return b"\0" * zero_bytes + decoded.to_bytes((decoded.bit_length() + 7) // 8, "big") def _validate_old_btc_address(addr: str): """Validate P2PKH and P2SH type address.""" if len(addr) not in range(25, 35): return False - decoded_bytes = _decode_base58(addr).to_bytes(25, "big") + decoded_bytes = _decode_base58(addr) + if len(decoded_bytes) != 25: + return False header, checksum = decoded_bytes[:-4], decoded_bytes[-4:] return checksum == sha256(sha256(header).digest()).digest()[:4] diff --git a/tests/crypto_addresses/test_btc_address.py b/tests/crypto_addresses/test_btc_address.py index 96647eab..8fcd5714 100644 --- a/tests/crypto_addresses/test_btc_address.py +++ b/tests/crypto_addresses/test_btc_address.py @@ -12,6 +12,10 @@ [ # P2PKH (Pay-to-PubkeyHash) type "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", + # P2PKH with zero bytes in the payload, encoded as leading ones + "1111111111111111111114oLvT2", + # Legacy testnet P2PKH + "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn", # P2SH (Pay to script hash) type "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", # Bech32/segwit type @@ -41,3 +45,18 @@ def test_returns_true_on_valid_btc_address(value: str): def test_returns_failed_validation_on_invalid_btc_address(value: str): """Test returns failed validation on invalid btc address.""" assert isinstance(btc_address(value), ValidationError) + + +@pytest.mark.parametrize( + "value", + [ + # Removing the version byte's leading one must not be silently padded. + "BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", + # Both remain within the existing character-count bounds. + "111111111111111111114oLvT2", + "11111111111111111111114oLvT2", + ], +) +def test_rejects_changed_leading_zero_bytes(value: str): + """Leading ones encode bytes and cannot be added or removed.""" + assert isinstance(btc_address(value), ValidationError)