python-stdnum branch master updated. 1.9-12-g54c3650
[
Date Prev][
Date Next]
[
Thread Prev][
Thread Next]
python-stdnum branch master updated. 1.9-12-g54c3650
- From: Commits of the python-stdnum project <python-stdnum-commits [at] lists.arthurdejong.org>
- To: python-stdnum-commits [at] lists.arthurdejong.org
- Reply-to: python-stdnum-users [at] lists.arthurdejong.org
- Subject: python-stdnum branch master updated. 1.9-12-g54c3650
- Date: Sun, 30 Sep 2018 23:48:54 +0200 (CEST)
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "python-stdnum".
The branch, master has been updated
via 54c36507220a9f39a14fade446d779315e37a270 (commit)
from 510ee93e0117eac927d17e213fa014df84774c48 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
https://arthurdejong.org/git/python-stdnum/commit/?id=54c36507220a9f39a14fade446d779315e37a270
commit 54c36507220a9f39a14fade446d779315e37a270
Author: Arthur de Jong <arthur@arthurdejong.org>
Date: Sun Sep 30 15:46:44 2018 +0200
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 https://github.com/arthurdejong/python-stdnum/issues/80
diff --git a/stdnum/bitcoin.py b/stdnum/bitcoin.py
new file mode 100644
index 0000000..9e85512
--- /dev/null
+++ b/stdnum/bitcoin.py
@@ -0,0 +1,159 @@
+# bitcoin.py - functions for handling Bitcoin addresses
+#
+# Copyright (C) 2018 Arthur de Jong
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 USA
+
+"""Bitcoin address.
+
+A Bitcoin address is an identifier that is used as destination in a Bitcoin
+transaction. It is based on a hash of the public portion of a keypair.
+
+There are currently three address formats in use:
+
+* P2PKH: pay to pubkey hash
+* P2SH: pay to script hash
+* Bech32
+
+More information:
+
+* https://en.bitcoin.it/wiki/Address
+
+>>> validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD')
+'1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD'
+>>> validate('BC1QARDV855YJNGSPVXUTTQ897AQCA3LXJU2Y69JCE')
+'bc1qardv855yjngspvxuttq897aqca3lxju2y69jce'
+>>> validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfX')
+Traceback (most recent call last):
+ ...
+InvalidChecksum: ...
+"""
+
+import hashlib
+import struct
+from functools import reduce
+
+from stdnum.exceptions import *
+from stdnum.util import clean
+
+
+def compact(number):
+ """Convert the number to the minimal representation. This strips the
+ number of any valid separators and removes surrounding whitespace."""
+ number = clean(number, ' ').strip()
+ if number[:3].lower() == 'bc1':
+ number = number.lower()
+ return number
+
+
+# Base58 encoding character set as used in Bitcoin addresses
+_base58_alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
+
+
+def b58decode(s):
+ """Decode a Base58 encoded string to a bytestring."""
+ value = reduce(lambda a, c: a * 58 + _base58_alphabet.index(c), s, 0)
+ result = b''
+ while value >= 256:
+ value, mod = divmod(value, 256)
+ result = struct.pack('B', mod) + result
+ result = struct.pack('B', value) + result
+ return struct.pack('B', 0) * (len(s) - len(s.lstrip('1'))) + result
+
+
+# Bech32 character set as used in Bitcoin addresses
+_bech32_alphabet = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
+
+
+# The Bech32 generator tests and values for checksum calculation
+_bech32_generator = (
+ (1 << 0, 0x3b6a57b2), (1 << 1, 0x26508e6d), (1 << 2, 0x1ea119fa),
+ (1 << 3, 0x3d4233dd), (1 << 4, 0x2a1462b3))
+
+
+def bech32_checksum(values):
+ """Calculate the Bech32 checksum."""
+ chk = 1
+ for value in values:
+ top = chk >> 25
+ chk = (chk & 0x1ffffff) << 5 | value
+ for t, v in _bech32_generator:
+ chk ^= v if top & t else 0
+ return chk
+
+
+def b32decode(data):
+ """Decode a list of Base32 values to a bytestring."""
+ acc, bits = 0, 0
+ result = b''
+ for value in data:
+ acc = ((acc << 5) | value) & 0xfff
+ bits += 5
+ if bits >= 8:
+ bits -= 8
+ result = result + struct.pack('B', (acc >> bits) & 0xff)
+ if bits >= 5 or acc & ((1 << bits) - 1):
+ raise InvalidComponent()
+ return result
+
+
+def _expand_hrp(hrp):
+ """Convert the human-readable part to format for checksum calculation."""
+ return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
+
+
+def validate(number):
+ """Check if the number provided is valid. This checks the length and
+ check digit."""
+ number = compact(number)
+ if number.startswith('1') or number.startswith('3'):
+ # P2PKH (pay to pubkey hash) or P2SH (pay to script hash) address
+ if not all(x in _base58_alphabet for x in number):
+ raise InvalidFormat()
+ address = b58decode(number)
+ if len(address) != 25:
+ raise InvalidLength()
+ if hashlib.sha256(hashlib.sha256(address[:-4]).digest()).digest()[:4]
!= address[-4:]:
+ raise InvalidChecksum()
+ elif number.startswith('bc1'):
+ # Bech32 type address
+ if not all(x in _bech32_alphabet for x in number[3:]):
+ raise InvalidFormat()
+ if len(number) < 11 or len(number) > 90:
+ raise InvalidLength()
+ data = [_bech32_alphabet.index(x) for x in number[3:]]
+ if bech32_checksum(_expand_hrp('bc') + data) != 1:
+ raise InvalidChecksum()
+ witness_version = data[0]
+ witness_program = b32decode(data[1:-6])
+ if witness_version > 16:
+ raise InvalidComponent()
+ if len(witness_program) < 2 or len(witness_program) > 40:
+ raise InvalidLength()
+ if witness_version == 0 and len(witness_program) not in (20, 32):
+ raise InvalidLength()
+ else:
+ raise InvalidComponent()
+ return number
+
+
+def is_valid(number):
+ """Check if the number provided is valid. This checks the length and
+ check digit."""
+ try:
+ return bool(validate(number))
+ except ValidationError:
+ return False
diff --git a/tests/test_bitcoin.doctest b/tests/test_bitcoin.doctest
new file mode 100644
index 0000000..85ad66d
--- /dev/null
+++ b/tests/test_bitcoin.doctest
@@ -0,0 +1,119 @@
+test_bitcoin.doctest - more detailed doctests for stdnum.bitcoin module
+
+Copyright (C) 2018 Arthur de Jong
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301 USA
+
+
+This file contains more detailed doctests for the stdnum.bitcoin module. It
+tries to test more corner cases and detailed functionality that is not really
+useful as module documentation.
+
+>>> from stdnum import bitcoin
+
+
+These are found and constructed P2PKH addresses (P2SH addresses are basically
+the same because they follow the same validation, except that the first digit
+is a 3).
+
+>>> bitcoin.validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD')
+'1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD'
+>>> bitcoin.validate('1NEDqZPvTWRaoho48qXuLLsrYomMXPABfA') # mangled digit
+Traceback (most recent call last):
+ ...
+InvalidChecksum: ...
+>>> bitcoin.validate('1NEDqZPvTWRaoho48qXu==srYomMXPABfD') # invalid digit
+Traceback (most recent call last):
+ ...
+InvalidFormat: ...
+>>> bitcoin.validate('1111111111111111111114oLvT2') # constructed but valid
+'1111111111111111111114oLvT2'
+>>> bitcoin.validate('111111111111111111aGQAo') # invalid binary length
+Traceback (most recent call last):
+ ...
+InvalidLength: ...
+
+
+Bech32 are more recent but also supported. Uppercase addresses will be
+automatically lowercased.
+
+>>> bitcoin.validate('BC1QARDV855YJNGSPVXUTTQ897AQCA3LXJU2Y69JCE')
+'bc1qardv855yjngspvxuttq897aqca3lxju2y69jce'
+>>> bitcoin.validate('bc1qardv855yjngspvxuttq897aqca3lxju2y69jZZ') # some
digits changed
+Traceback (most recent call last):
+ ...
+InvalidChecksum: ...
+>>> bitcoin.validate('bc1qardv855yjngspvxuttq897aqca3lxju2y69j11') #
non-bech32 characters
+Traceback (most recent call last):
+ ...
+InvalidFormat: ...
+>>> bitcoin.validate('bc1pc54a7w') # too short but valid checksum
+Traceback (most recent call last):
+ ...
+InvalidLength: ...
+>>> bitcoin.validate('bc1qv93xxeqnnq0uz') # too short for witness version
+Traceback (most recent call last):
+ ...
+InvalidLength: ...
+>>> bitcoin.validate('bc1lv93xxer9venks6t2ddkx6mn0wpchyum5rtc42k') # invalid
witness version
+Traceback (most recent call last):
+ ...
+InvalidComponent: ...
+>>>
bitcoin.validate('bc1pv93xxer9venks6t2ddkx6mn0wpchyum5w4m8w7re0fq5ys6yg4rywjzfff95cn2wfumys6cj')
# too long witness program
+Traceback (most recent call last):
+ ...
+InvalidLength: ...
+>>> bitcoin.validate('bc1ppzry7g5z8k') # invalid Base32 padding
+Traceback (most recent call last):
+ ...
+InvalidComponent: ...
+
+
+Test for unknown address type.
+
+>>> bitcoin.validate('gzXESMi1caU4L4CWEV96kQMkn5TKLsMzuX')
+Traceback (most recent call last):
+ ...
+InvalidComponent: ...
+
+
+These have been found online and should all be valid numbers.
+
+>>> numbers = '''
+...
+... 16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM
+... 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2
+... 1G5Tjyznf4hmWoStygje9h2u1Y7rFBjtmS
+... 1NEDqZPvTWRaoho48qXuLLsrYomMXPABfD
+... 1NXYoJ5xU91Jp83XfVMHwwTUyZFK64BoAD
+... 1P2c1W3x1TCUFvyDmVyVmUxrRqFtuF2w6
+... 39y1UjCMmxzMYtt4S4wii9e3xmfHngKncL
+... 3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy
+... 3KwLBFMtU9Wtn9Yys3imuU2hs2oSDsfZY4
+... BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4
+... BC1SW50QA3JX3S
+... bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx
+... bc1q362mcakh9p0zr380s4uhhz26263yjep36c8se8
+... bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq
+... bc1qardv855yjngspvxuttq897aqca3lxju2y69jce
+... bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9
+... bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3
+... bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
+... bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj
+...
+... '''
+>>> [x for x in numbers.splitlines() if x and not bitcoin.is_valid(x)]
+[]
-----------------------------------------------------------------------
Summary of changes:
stdnum/bitcoin.py | 159 +++++++++++++++++++++++++++++++++++++++++++++
tests/test_bitcoin.doctest | 119 +++++++++++++++++++++++++++++++++
2 files changed, 278 insertions(+)
create mode 100644 stdnum/bitcoin.py
create mode 100644 tests/test_bitcoin.doctest
hooks/post-receive
--
python-stdnum
--
To unsubscribe send an email to
python-stdnum-commits-unsubscribe@lists.arthurdejong.org or see
https://lists.arthurdejong.org/python-stdnum-commits/
- python-stdnum branch master updated. 1.9-12-g54c3650,
Commits of the python-stdnum project