Source code for bitcoincash.cashaddr

# Copyright (c) 2017 Pieter Wuille
# Copyright (c) 2017 Shammah Chancellor, Neil Booth
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Copyright (C) 2019 The python-bitcoincashlib developers
# This file is part of python-bitcoincashlib.
#
# It is subject to the license terms in the LICENSE file found in the top-level
# directory of this distribution.
#
# No part of python-bitcoinlib, including this file, may be copied, modified,
# propagated, or distributed except according to the terms contained in the
# LICENSE file.



_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

def _polymod(values):
    """Internal function that computes the cashaddr checksum."""
    c = 1
    for d in values:
        c0 = c >> 35
        c = ((c & 0x07ffffffff) << 5) ^ d
        if (c0 & 0x01):
            c ^= 0x98f2bc8e61
        if (c0 & 0x02):
            c ^= 0x79b76d99e2
        if (c0 & 0x04):
            c ^= 0xf33e5fb3c4
        if (c0 & 0x08):
            c ^= 0xae2eabe2a8
        if (c0 & 0x10):
            c ^= 0x1e4f43e470
    retval= c ^ 1
    return retval

def _prefix_expand(prefix):
    """Expand the prefix into values for checksum computation."""
    retval = bytearray(ord(x) & 0x1f for x in prefix)
    # Append null separator
    retval.append(0)
    return retval

def _create_checksum(prefix, data):
    """Compute the checksum values given prefix and data."""
    values = _prefix_expand(prefix) + data + bytes(8)
    polymod = _polymod(values)
    # Return the polymod expanded into eight 5-bit elements
    return bytes((polymod >> 5 * (7 - i)) & 31 for i in range(8))

def _convertbits(data, frombits, tobits, pad=True):
    """General power-of-2 base conversion."""
    acc = 0
    bits = 0
    ret = bytearray()
    maxv = (1 << tobits) - 1
    max_acc = (1 << (frombits + tobits - 1)) - 1
    for value in data:
        acc = ((acc << frombits) | value ) & max_acc
        bits += frombits
        while bits >= tobits:
            bits -= tobits
            ret.append((acc >> bits) & maxv)

    if pad and bits:
        ret.append((acc << (tobits - bits)) & maxv)

    return ret

def _pack_addr_data(kind, addr_hash):
    """Pack addr data with version byte"""
    version_byte = kind << 3

    offset = 1
    encoded_size = 0
    if len(addr_hash) >= 40:
        offset = 2
        encoded_size |= 0x04
    encoded_size |= (len(addr_hash) - 20 * offset) // (4 * offset)

    # invalid size?
    if ((len(addr_hash) - 20 * offset) % (4 * offset) != 0
            or not 0 <= encoded_size <= 7):
        raise ValueError('invalid address hash size {}'.format(addr_hash))

    version_byte |= encoded_size

    data = bytes([version_byte]) + addr_hash
    return _convertbits(data, 8, 5, True)


def _decode_payload(addr):
    """Validate a cashaddr string.

    Throws CashAddr.Error if it is invalid, otherwise returns the
    triple

       (prefix,  payload)

    without the checksum.
    """
    lower = addr.lower()
    if lower != addr and addr.upper() != addr:
        raise ValueError('mixed case in address: {}'.format(addr))

    parts = lower.split(':', 1)
    if len(parts) != 2:
        raise ValueError("address missing ':' separator: {}".format(addr))

    prefix, payload = parts
    if not prefix:
        raise ValueError('address prefix is missing: {}'.format(addr))
    if not all(33 <= ord(x) <= 126 for x in prefix):
        raise ValueError('invalid address prefix: {}'.format(prefix))
    if not (8 <= len(payload) <= 124):
        raise ValueError('address payload has invalid length: {}'
                         .format(len(addr)))
    try:
        data = bytes(_CHARSET.find(x) for x in payload)
    except ValueError:
        raise ValueError('invalid characters in address: {}'
                            .format(payload))

    if _polymod(_prefix_expand(prefix) + data):
        raise ValueError('invalid checksum in address: {}'.format(addr))

    if lower != addr:
        prefix = prefix.upper()

    # Drop the 40 bit checksum
    return prefix, data[:-8]

#
# External Interface
#

PUBKEY_TYPE = 0
SCRIPT_TYPE = 1

[docs]def decode(address): '''Given a cashaddr address, return a triple (prefix, kind, hash) ''' if not isinstance(address, str): raise TypeError('address must be a string') prefix, payload = _decode_payload(address) # Ensure there isn't extra padding extrabits = len(payload) * 5 % 8 if extrabits >= 5: raise ValueError('excess padding in address {}'.format(address)) # Ensure extrabits are zeros if payload[-1] & ((1 << extrabits) - 1): raise ValueError('non-zero padding in address {}'.format(address)) decoded = _convertbits(payload, 5, 8, False) version = decoded[0] addr_hash = bytes(decoded[1:]) size = (version & 0x03) * 4 + 20 # Double the size, if the 3rd bit is on. if version & 0x04: size <<= 1 if size != len(addr_hash): raise ValueError('address hash has length {} but expected {}' .format(len(addr_hash), size)) kind = version >> 3 if kind not in (SCRIPT_TYPE, PUBKEY_TYPE): raise ValueError('unrecognised address type {}'.format(kind)) return prefix, kind, addr_hash
[docs]def encode(prefix, kind, addr_hash): """Encode a cashaddr address without prefix and separator.""" if not isinstance(prefix, str): raise TypeError('prefix must be a string') if not isinstance(addr_hash, (bytes, bytearray)): raise TypeError('addr_hash must be binary bytes') if kind not in (SCRIPT_TYPE, PUBKEY_TYPE): raise ValueError('unrecognised address type {}'.format(kind)) payload = _pack_addr_data(kind, addr_hash) checksum = _create_checksum(prefix, payload) return ''.join([_CHARSET[d] for d in (payload + checksum)])
[docs]def encode_full(prefix, kind, addr_hash): """Encode a full cashaddr address, with prefix and separator.""" return ':'.join([prefix, encode(prefix, kind, addr_hash)])
[docs]class CashAddrData(bytes): """CashAddr-encoded data""" def __new__(cls, s): prefix, kind, addr_hash = decode(s) return cls.from_bytes(addr_hash, prefix, kind) def __init__(self, s): """Initialize from cashaddr-encoded string Note: subclasses put your initialization routines here, but ignore the argument - that's handled by __new__(), and .from_bytes() will call __init__() with None in place of the string. """
[docs] @classmethod def from_bytes(cls, data, prefix, kind): self = bytes.__new__(cls, data) self.prefix = prefix self.kind = kind return self
[docs] def to_bytes(self): """Convert to bytes instance Note that it's the data represented that is converted; the prefix and kind is not included. """ return b'' + self
def __str__(self): """Convert to string""" return encode_full(self.prefix, self.kind, b'' + self) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, str(self))
__all__ = ( 'CashAddrData', 'PUBKEY_TYPE', 'SCRIPT_TYPE', 'decode', 'encode', 'encode_full', )