Skip to content

Commit

Permalink
Add address validation
Browse files Browse the repository at this point in the history
  • Loading branch information
emesik committed Oct 30, 2021
1 parent b77838d commit 2f7b2ad
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 179 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ docs/build/*
venv
build/
dist/
htmlcov/
*.egg-info/
75 changes: 0 additions & 75 deletions cardano/address.py

This file was deleted.

122 changes: 122 additions & 0 deletions cardano/address/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import base58

from ..consts import Era

from .shelley import SHELLEY_ADDR_RE
from . import bech32


def address(addr, wallet=None):
if isinstance(addr, Address):
return addr # already instatinated and should be of proper class
elif isinstance(addr, (bytes, bytearray)):
addr = addr.decode()
elif not isinstance(addr, str):
raise TypeError(
"address() argument must be str, bytes, bytearray or Address instance"
)
# validation
if SHELLEY_ADDR_RE.match(addr):
AddressClass = ShelleyAddress
elif addr.startswith("DdzFF"):
AddressClass = ByronAddress
elif addr.startswith("Ae2"):
AddressClass = IcarusAddress
else:
raise ValueError("String {} is not a valid Cardano address".format(addr))
return AddressClass(addr, wallet=wallet)


class Address(object):
"""
Cardano base address class. Does no validation, it is up to child classes.
Compares with ``str`` and ``bytes`` objects.
:param addr: the address as ``str`` or ``bytes`` or ``Address``
:param wallet: the ``Wallet`` object if address belongs to
"""

_address = ""
wallet = None

def __init__(self, addr, wallet=None):
self._address = addr
self.wallet = wallet or self.wallet
self._validate()

def _validate(self):
pass

def __repr__(self):
return str(self._address)

def __eq__(self, other):
if isinstance(other, Address):
return str(self) == str(other)
elif isinstance(other, str):
return str(self) == other
elif isinstance(other, bytes):
return str(self).encode() == other
return super(Address, self).__eq__(other)

def __hash__(self):
return hash(str(self))

def __format__(self, spec):
return format(str(self), spec)


class ByronAddress(Address):
era = Era.BYRON

def _validate(self):
if not self._address.startswith("DdzFF"):
raise ValueError("{:s} is not a valid Byron address".format(self._address))
data = base58.b58decode(self._address)


class IcarusAddress(ByronAddress):
def _validate(self):
if not self._address.startswith("Ae2"):
raise ValueError(
"{:s} is not a valid Icarus/Byron address".format(self._address)
)
data = base58.b58decode(self._address)


class ShelleyAddress(Address):
era = Era.SHELLEY

def _validate(self):
hrp, payload32 = bech32.bech32_decode(self._address)
if not payload32:
raise ValueError(
"{:s} is not a valid Shelley address".format(self._address)
)
payload = bech32.convertbits(payload32, 5, 8, False)
addr_typ, net_tag = (payload[0] & 0xF0) >> 4, payload[0] & 0xF
if addr_typ > 7 and addr_typ < 0xE:
raise ValueError(
"Shelley address {:s} is of wrong type (0x{:x})".format(
self._address, addr_typ
)
)
if net_tag not in (0, 1):
raise ValueError(
"Shelley address {:s} has unsupported net tag (0x{:x})".format(
self._address, net_tag
)
)
if net_tag == 0 and not hrp.endswith("_test"):
raise ValueError(
'Shelley address {:s} has TESTNET tag but the prefix doesn\'t end with "_test"'.format(
self._address
)
)
elif net_tag == 1 and hrp.endswith("_test"):
raise ValueError(
'Shelley address {:s} has MAINNET tag but the prefix ends with "_test"'.format(
self._address
)
)
93 changes: 93 additions & 0 deletions cardano/address/bech32.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright (c) 2017, 2020 Pieter Wuille
# Copyright (c) 2021 Michal Salaban
#
# Code taken from
# https://github.com/sipa/bech32/blob/45bbf67d3dcc00dc960041563fc2dddd32a5f903/ref/python/segwit_addr.py
# and pruned to match the needs of the cardano module.
#
#
#
# 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.

"""Reference implementation for Bech32/Bech32m and segwit addresses."""


from enum import Enum

CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"


def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum."""
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
chk = 1
for value in values:
top = chk >> 25
chk = (chk & 0x1FFFFFF) << 5 ^ value
for i in range(5):
chk ^= generator[i] if ((top >> i) & 1) else 0
return chk


def bech32_hrp_expand(hrp):
"""Expand the HRP into values for checksum computation."""
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]


def bech32_decode(bech):
"""Validate a Bech32/Bech32m string, and determine HRP and data."""
if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
bech.lower() != bech and bech.upper() != bech
):
return (None, None, None)
bech = bech.lower()
pos = bech.rfind("1")
if pos < 1 or pos + 7 > len(bech):
# Removed the 90 char limitation -- MS
return (None, None, None)
if not all(x in CHARSET for x in bech[pos + 1 :]):
return (None, None, None)
hrp = bech[:pos]
data = [CHARSET.find(x) for x in bech[pos + 1 :]]
if bech32_polymod(bech32_hrp_expand(hrp) + data) is None:
return (None, None, None)
return (hrp, data[:-6])


def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion."""
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad: # pragma: no cover
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret
53 changes: 53 additions & 0 deletions cardano/address/shelley.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import re

PREFIXES = [
"addr",
"addr_test",
"script",
"stake",
"stake_test"
# -- * Hashes
,
"addr_vkh",
"stake_vkh",
"addr_shared_vkh",
"stake_shared_vkh"
# -- * Keys for 1852H
,
"addr_vk",
"addr_sk",
"addr_xvk",
"addr_xsk",
"acct_vk",
"acct_sk",
"acct_xvk",
"acct_xsk",
"root_vk",
"root_sk",
"root_xvk",
"root_xsk",
"stake_vk",
"stake_sk",
"stake_xvk",
"stake_xsk"
# -- * Keys for 1854H
,
"addr_shared_vk",
"addr_shared_sk",
"addr_shared_xvk",
"addr_shared_xsk",
"acct_shared_vk",
"acct_shared_sk",
"acct_shared_xvk",
"acct_shared_xsk",
"root_shared_vk",
"root_shared_sk",
"root_shared_xvk",
"root_shared_xsk",
"stake_shared_vk",
"stake_shared_sk",
"stake_shared_xvk",
"stake_shared_xsk",
]

SHELLEY_ADDR_RE = re.compile("^(" + "|".join(PREFIXES) + ")")
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
base58
requests
Loading

0 comments on commit 2f7b2ad

Please sign in to comment.