lists.arthurdejong.org
RSS feed

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



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/