diff --git a/src/trezor/crypto/cashaddr.py b/src/trezor/crypto/cashaddr.py new file mode 100644 index 0000000000..b7a7c4879d --- /dev/null +++ b/src/trezor/crypto/cashaddr.py @@ -0,0 +1,102 @@ +# Copyright (c) 2017 Pieter Wuille +# Copyright (c) 2018 Oskar Hladky +# Copyright (c) 2018 Pavol Rusnak +# +# 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. + +CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' + + +def cashaddr_polymod(values): + generator = [0x98f2bc8e61, 0x79b76d99e2, 0xf33e5fb3c4, 0xae2eabe2a8, 0x1e4f43e470] + chk = 1 + for value in values: + top = chk >> 35 + chk = ((chk & 0x07ffffffff) << 5) ^ value + for i in range(5): + chk ^= generator[i] if (top & (1 << i)) else 0 + return chk ^ 1 + + +def prefix_expand(prefix): + return [ord(x) & 0x1f for x in prefix] + [0] + + +def calculate_checksum(prefix, payload): + poly = cashaddr_polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0]) + out = list() + for i in range(8): + out.append((poly >> 5 * (7 - i)) & 0x1f) + return out + + +def verify_checksum(prefix, payload): + return cashaddr_polymod(prefix_expand(prefix) + payload) == 0 + + +def b32decode(inputs): + out = list() + for letter in inputs: + out.append(CHARSET.find(letter)) + return out + + +def b32encode(inputs): + out = '' + for char_code in inputs: + out += CHARSET[char_code] + return out + + +def convertbits(data, frombits, tobits, pad=True): + 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: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def encode(prefix, version, payload): + payload = bytes([version]) + payload + payload = convertbits(payload, 8, 5) + checksum = calculate_checksum(prefix, payload) + return prefix + ':' + b32encode(payload + checksum) + + +def decode(prefix, addr): + addr = addr.lower() + decoded = b32decode(addr) + if not verify_checksum(prefix, decoded): + raise ValueError('Bad cashaddr checksum') + data = bytes(convertbits(decoded, 5, 8)) + return data[0], data[1:-6] diff --git a/tests/test_trezor.crypto.cashaddr.py b/tests/test_trezor.crypto.cashaddr.py new file mode 100644 index 0000000000..88ffee43ea --- /dev/null +++ b/tests/test_trezor.crypto.cashaddr.py @@ -0,0 +1,81 @@ +# Copyright (c) 2017 Pieter Wuille +# Copyright (c) 2018 Pavol Rusnak +# +# 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 tests for cashaddr adresses""" + +from common import * +from trezor.crypto import base58, cashaddr + + +VALID_CHECKSUM = [ + "prefix:x64nx6hz", + "p:gpf8m4h7", + "bitcoincash:qpzry9x8gf2tvdw0s3jn54khce6mua7lcw20ayyn", + "bchtest:testnetaddress4d6njnut", + "bchreg:555555555555555555555555555555555555555555555udxmlmrz", +] + +VALID_ADDRESS = [ + ("1BpEi6DfDAUFd7GtittLSdBeYJvcoaVggu", "bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a"), + ("1KXrWXciRDZUpQwQmuM1DbwsKDLYAYsVLR", "bitcoincash:qr95sy3j9xwd2ap32xkykttr4cvcu7as4y0qverfuy"), + ("16w1D5WRVKJuZUsSRzdLp9w3YGcgoxDXb", "bitcoincash:qqq3728yw0y47sqn6l2na30mcw6zm78dzqre909m2r"), + ("3CWFddi6m4ndiGyKqzYvsFYagqDLPVMTzC", "bitcoincash:ppm2qsznhks23z7629mms6s4cwef74vcwvn0h829pq"), + ("3LDsS579y7sruadqu11beEJoTjdFiFCdX4", "bitcoincash:pr95sy3j9xwd2ap32xkykttr4cvcu7as4yc93ky28e"), + ("31nwvkZwyPdgzjBJZXfDmSWsC4ZLKpYyUw", "bitcoincash:pqq3728yw0y47sqn6l2na30mcw6zm78dzq5ucqzc37") +] + + +class TestCryptoCashAddr(unittest.TestCase): + + def test_valid_checksum(self): + for test in VALID_CHECKSUM: + prefix, addr = test.split(':') + cashaddr.decode(prefix, addr) + + def test_invalid_checksum(self): + for test in VALID_CHECKSUM: + test += 'xxx' + prefix, addr = test.split(':') + with self.assertRaises(ValueError): + cashaddr.decode(prefix, addr) + + def test_valid_address(self): + # b58 -> cashaddr + for b58, ca in VALID_ADDRESS: + data = base58.decode_check(b58) + version = data[0] + if version == 5: + version = 8 + enc = cashaddr.encode('bitcoincash', version, data[1:]) + self.assertEqual(ca, enc) + # cashaddr -> base58 + for b58, ca in VALID_ADDRESS: + prefix, addr = ca.split(':') + version, data = cashaddr.decode(prefix, addr) + if version == 8: + version = 5 + enc = base58.encode_check(bytes([version]) + data) + self.assertEqual(b58, enc) + + +if __name__ == "__main__": + unittest.main()