8000 Bitcoin address · yyht/python-stdnum@54c3650 · GitHub
[go: up one dir, main page]

Skip to content

Commit 54c3650

Browse files
committed
Bitcoin address
This adds validation of Bitcoin addresses. No check is done that the addresses actually exist but only that they are syntactically correct. Closes arthurdejong#80
1 parent 510ee93 commit 54c3650

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed

stdnum/bitcoin.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# bitcoin.py - functions for handling Bitcoin addresses
2+
#
3+
# Copyright (C) 2018 Arthur de Jong
4+
#
5+
# This library is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation; either
8+
# version 2.1 of the License, or (at your option) any later version.
9+
#
10+
# This library is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with this library; if not, write to the Free Software
17+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18+
# 02110-1301 USA
19+
20+
"""Bitcoin address.
21+
22+
A Bitcoin address is an identifier that is used as destination in a Bitcoin
23+
transaction. It is based on a hash of the public portion of a keypair.
24+
25+
There are currently three address formats in use:
26+
27+
* P2PKH: pay to pubkey hash
28+
* P2SH: pay to script hash
29+
* Bech32
30+
31+
More information:
32+
33+
* https://en.bitcoin.it/wiki/Address
34+
35+
>>> validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD')
36+
'1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD'
37+
>>> validate('BC1QARDV855YJNGSPVXUTTQ897AQCA3LXJU2Y69JCE')
38+< 8000 /span>
'bc1qardv855yjngspvxuttq897aqca3lxju2y69jce'
39+
>>> validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfX')
40+
Traceback (most recent call last):
41+
...
42+
InvalidChecksum: ...
43+
"""
44+
45+
import hashlib
46+
import struct
47+
from functools import reduce
48+
49+
from stdnum.exceptions import *
50+
from stdnum.util import clean
51+
52+
53+
def compact(number):
54+
"""Convert the number to the minimal representation. This strips the
55+
number of any valid separators and removes surrounding whitespace."""
56+
number = clean(number, ' ').strip()
57+
if number[:3].lower() == 'bc1':
58+
number = number.lower()
59+
return number
60+
61+
62+
# Base58 encoding character set as used in Bitcoin addresses
63+
_base58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
64+
65+
66+
def b58decode(s):
67+
"""Decode a Base58 encoded string to a bytestring."""
68+
value = reduce(lambda a, c: a * 58 + _base58_alphabet.index(c), s, 0)
69+
result = b''
70+
while value >= 256:
71+
value, mod = divmod(value, 256)
72+
result = struct.pack('B', mod) + result
73+
result = struct.pack('B', value) + result
74+
return struct.pack('B', 0) * (len(s) - len(s.lstrip('1'))) + result
75+
76+
77+
# Bech32 character set as used in Bitcoin addresses
78+
_bech32_alphabet = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
79+
80+
81+
# The Bech32 generator tests and values for checksum calculation
82+
_bech32_generator = (
83+
(1 << 0, 0x3b6a57b2), (1 << 1, 0x26508e6d), (1 << 2, 0x1ea119fa),
84+
(1 << 3, 0x3d4233dd), (1 << 4, 0x2a1462b3))
85+
86+
87+
def bech32_checksum(values):
88+
"""Calculate the Bech32 checksum."""
89+
chk = 1
90+
for value in values:
91+
top = chk >> 25
92+
chk = (chk & 0x1ffffff) << 5 | value
93+
for t, v in _bech32_generator:
94+
chk ^= v if top & t else 0
95+
return chk
96+
97+
98+
def b32decode(data):
99+
"""Decode a list of Base32 values to a bytestring."""
100+
acc, bits = 0, 0
101+
result = b''
102+
for value in data:
103+
acc = ((acc << 5) | value) & 0xfff
104+
bits += 5
105+
if bits >= 8:
106+
bits -= 8
107+
result = result + struct.pack('B', (acc >> bits) & 0xff)
108+
if bits >= 5 or acc & ((1 << bits) - 1):
109+
raise InvalidComponent()
110+
return result
111+
112+
113+
def _expand_hrp(hrp):
114+
"""Convert the human-readable part to format for checksum calculation."""
115+
return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
116+
117+
118+
def validate(number):
119+
"""Check if the number provided is valid. This checks the length and
120+
check digit."""
121+
number = compact(number)
122+
if number.startswith('1') or number.startswith('3'):
123+
# P2PKH (pay to pubkey hash) or P2SH (pay to script hash) address
124+
if not all(x in _base58_alphabet for x in number):
125+
raise InvalidFormat()
126+
address = b58decode(number)
127+
if len(address) != 25:
128+
raise InvalidLength()
129+
if hashlib.sha256(hashlib.sha256(address[:-4]).digest()).digest()[:4] != address[-4:]:
130+
raise InvalidChecksum()
131+
elif number.startswith('bc1'):
132+
# Bech32 type address
133+
if not all(x in _bech32_alphabet for x in number[3:]):
134+
raise InvalidFormat()
135+
if len(number) < 11 or len(number) > 90:
136+
raise InvalidLength()
137+
data = [_bech32_alphabet.index(x) for x in number[3:]]
138+
if bech32_checksum(_expand_hrp('bc') + data) != 1:
139+
raise InvalidChecksum()
140+
witness_version = data[0]
141+
witness_program = b32decode(data[1:-6])
142+
if witness_version > 16:
143+
raise InvalidComponent()
144+
if len(witness_program) < 2 or len(witness_program) > 40:
145+
raise InvalidLength()
146+
if witness_version == 0 and len(witness_program) not in (20, 32):
147+
raise InvalidLength()
148+
else:
149+
raise InvalidComponent()
150+
return number
151+
152+
153+
def is_valid(number):
154+
"""Check if the number provided is valid. This checks the length and
155+
check digit."""
156+
try:
157+
return bool(validate(number))
158+
except ValidationError:
159+
return False

tests/test_bitcoin.doctest

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
test_bitcoin.doctest - more detailed doctests for stdnum.bitcoin module
2+
3+
Copyright (C) 2018 Arthur de Jong
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18+
02110-1301 USA
19+
20+
21+
This file contains more detailed doctests for the stdnum.bitcoin module. It
22+
tries to test more corner cases and detailed functionality that is not really
23+
useful as module documentation.
24+
25+
>>> from stdnum import bitcoin
26+
27+
28+
These are found and constructed P2PKH addresses (P2SH addresses are basically
29+
the same because they follow the same validation, except that the first digit
30+
is a 3).
31+
32+
>>> bitcoin.validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD')
33+
'1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD'
34+
>>> bitcoin.validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfA') # mangled digit
35+
Traceback (most recent call last):
36+
...
37+
InvalidChecksum: ...
38+
>>> bitcoin.validate('1NEDqZPvTWRaoho48qXu==srYomMXPABfD') # invalid digit
39+
Traceback (most recent call last):
40+
...
41+
InvalidFormat: ...
42+
>>> bitcoin.validate('1111111111111111111114oLvT2') # constructed but valid
43+
'1111111111111111111114oLvT2'
44+
>>> bitcoin.validate('111111111111111111aGQAo') # invalid binary length
45+
Traceback (most recent call last):
46+
...
47+
InvalidLength: ...
48+
49+
50+
Bech32 are more recent but also supported. Uppercase addresses will be
51+
automatically lowercased.
52+
53+
>>> bitcoin.validate('BC1QARDV855YJNGSPVXUTTQ897AQCA3LXJU2Y69JCE')
54+
'bc1qardv855yjngspvxuttq897aqca3lxju2y69jce'
55+
>>> bitcoin.validate('bc1qardv855yjngspvxuttq897aqca3lxju2y69jZZ') # some digits changed
56+
Traceback (most recent call last):
57+
...
58+
InvalidChecksum: ...
59+
>>> bitcoin.validate('bc1qardv855yjngspvxuttq897aqca3lxju2y69j11') # non-bech32 characters
60+
Traceback (most recent call last):
61+
...
62+
InvalidFormat: ...
63+
>>> bitcoin.validate('bc1pc54a7w') # too short but valid checksum
64+
Traceback (most recent call last):
65+
...
66+
InvalidLength: ...
67+
>>> bitcoin.validate('bc1qv93xxeqnnq0uz') # too short for witness version
68+
Traceback (most recent call last):
69+
...
70+
InvalidLength: ...
71+
>>> bitcoin.validate('bc1lv93xxer9venks6t2ddkx6mn0wpchyum5rtc42k') # invalid witness version
72+
Traceback (most recent call last):
73+
...
74+
InvalidComponent: ...
75+
>>> bitcoin.validate('bc1pv93xxer9venks6t2ddkx6mn0wpchyum5w4m8w7re0fq5ys6yg4rywjzfff95cn2wfumys6cj') # too long witness program
76+
Traceback (most recent call last):
77+
...
78+
InvalidLength: ...
79+
>>> bitcoin.validate('bc1ppzry7g5z8k') # invalid Base32 padding
80+
Traceback (most recent call last):
81+
...
82+
InvalidComponent: ...
83+
84+
85+
Test for unknown address type.
86+
87+
>>> bitcoin.validate('gzXESMi1caU4L4CWEV96kQMkn5TKLsMzuX')
88+
Traceback (most recent call last):
89+
...
90+
InvalidComponent: ...
91+
92+
93+
These have been found online and should all be valid numbers.
94+
95+
>>> numbers = '''
96+
...
97+
... 16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM
98+
... 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
99+
... 1G5Tjyznf4hmWoStygje9h2u1Y7rFBjtmS
100+
... 1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD
101+
... 1NXYoJ5xU91Jp83XfVMHwwTUyZFK64BoAD
102+
... 1P2c1W3x1TCUFvyDmVyVmUxrRqFtuF2w6
103+
... 39y1UjCMmxzMYtt4S4wii9e3xmfHngKncL
104+
... 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy
105+
... 3KwLBFMtU9Wtn9Yys3imuU2hs2oSDsfZY4
106+
... BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4
107+
... BC1SW50QA3JX3S
108+
... bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx
109+
... bc1q362mcakh9p0zr380s4uhhz26263yjep36c8se8
110+
... bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
111+
... bc1qardv855yjngspvxuttq897aqca3lxju2y69jce
112+
... bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9
113+
... bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3
114+
... bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
115+
... bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj
116+
...
117+
... '''
118+
>>> [x for x in numbers.splitlines() if x and not bitcoin.is_valid(x)]
119+
[]

0 commit comments

Comments
 (0)
0