Skip to content

Commit 5558e62

Browse files
committed
Merge branch 'tpatja-better-btc-addr-validation'
2 parents 78be255 + 407f10e commit 5558e62

File tree

2 files changed

+61
-14
lines changed

2 files changed

+61
-14
lines changed

tests/test_btc_address.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,32 @@
44
from validators import btc_address, ValidationFailure
55

66

7-
@pytest.mark.parametrize(('address',), [
8-
('17nuNm4QpgKuDvWy7Jh2AZ2nzZpMyKSKzT',),
9-
('3Cwgr2g7vsi1bXDUkpEnVoRLA9w4FZfC69',),
10-
])
11-
def test_returns_true_on_valid_mac_address(address):
12-
assert btc_address(address)
7+
@pytest.mark.parametrize(
8+
'value',
9+
[
10+
# P2PKH (Pay-to-PubkeyHash) type
11+
'1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2',
12+
# P2SH (Pay to script hash) type
13+
'3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy',
14+
# Bech32/segwit type
15+
'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq',
16+
'bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9',
17+
],
18+
)
19+
def test_returns_true_on_valid_btc_address(value):
20+
assert btc_address(value)
1321

1422

15-
@pytest.mark.parametrize(('address',), [
16-
('ff3Cwgr2g7vsi1bXDUkpEnVoRLA9w4FZfC69',),
17-
('b3Cgwgr2g7vsi1bXyjyDUkphEnVoRLA9w4FZfC69',),
18-
])
19-
def test_returns_failed_validation_on_invalid_mac_address(address):
20-
assert isinstance(btc_address(address), ValidationFailure)
23+
@pytest.mark.parametrize(
24+
'value',
25+
[
26+
'ff3Cwgr2g7vsi1bXDUkpEnVoRLA9w4FZfC69',
27+
'b3Cgwgr2g7vsi1bXyjyDUkphEnVoRLA9w4FZfC69',
28+
# incorrect header
29+
'1BvBMsEYstWetqTFn5Au4m4GFg7xJaNVN2',
30+
# incorrect checksum
31+
'3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLz',
32+
],
33+
)
34+
def test_returns_failed_validation_on_invalid_btc_address(value):
35+
assert isinstance(btc_address(value), ValidationFailure)

validators/btc_address.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
import re
2+
from hashlib import sha256
23

34
from .utils import validator
45

5-
pattern = re.compile(r"^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$")
6+
segwit_pattern = re.compile(
7+
r'^(bc|tc)[0-3][02-9ac-hj-np-z]{14,74}$')
8+
9+
10+
def validate_segwit_address(addr):
11+
return segwit_pattern.match(addr)
12+
13+
14+
def decode_base58(addr):
15+
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
16+
return sum([
17+
(58 ** e) * alphabet.index(i)
18+
for e, i in enumerate(addr[::-1])
19+
])
20+
21+
22+
def validate_old_btc_address(addr):
23+
"Validate P2PKH and P2SH type address"
24+
if not len(addr) in range(25, 35):
25+
return False
26+
decoded_bytes = decode_base58(addr).to_bytes(25, "big")
27+
header = decoded_bytes[:-4]
28+
checksum = decoded_bytes[-4:]
29+
return checksum == sha256(sha256(header).digest()).digest()[:4]
630

731

832
@validator
@@ -13,11 +37,19 @@ def btc_address(value):
1337
If the value is valid bitcoin address this function returns ``True``,
1438
otherwise :class:`~validators.utils.ValidationFailure`.
1539
40+
Full validation is implemented for P2PKH and P2SH addresses.
41+
For segwit addresses a regexp is used to provide a reasonable estimate
42+
on whether the address is valid.
43+
1644
Examples::
1745
1846
>>> btc_address('3Cwgr2g7vsi1bXDUkpEnVoRLA9w4FZfC69')
1947
True
2048
2149
:param value: Bitcoin address string to validate
2250
"""
23-
return pattern.match(value)
51+
if not value or not isinstance(value, str):
52+
return False
53+
if value[:2] in ("bc", "tb"):
54+
return validate_segwit_address(value)
55+
return validate_old_btc_address(value)

0 commit comments

Comments
 (0)