162 lines
5.3 KiB
Python
162 lines
5.3 KiB
Python
# 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 key pair.
|
|
|
|
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: ...
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import struct
|
|
from collections.abc import Iterable
|
|
from functools import reduce
|
|
|
|
from stdnum.exceptions import *
|
|
from stdnum.util import clean
|
|
|
|
|
|
def compact(number: str) -> str:
|
|
"""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: str) -> bytes:
|
|
"""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: Iterable[int]) -> int:
|
|
"""Calculate the Bech32 checksum."""
|
|
chk = 1
|
|
for value in values:
|
|
top = chk >> 25
|
|
chk = (chk & 0x1ffffff) << 5 | value
|
|
for test, val in _bech32_generator:
|
|
chk ^= val if top & test else 0
|
|
return chk
|
|
|
|
|
|
def b32decode(data: Iterable[int]) -> bytes:
|
|
"""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: str) -> list[int]:
|
|
"""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: str) -> str:
|
|
"""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: str) -> bool:
|
|
"""Check if the number provided is valid. This checks the length and
|
|
check digit."""
|
|
try:
|
|
return bool(validate(number))
|
|
except ValidationError:
|
|
return False
|