From 3c726f90918a450aecad31a5707d1c5a94bffb0e Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sun, 29 Sep 2024 11:59:54 +0000 Subject: [PATCH] BCH cashaddr: low-level support --- mmgen/proto/bch/cashaddr.py | 99 ++++++++++++++++++++++++++++++ test/unit_tests_d/ut_cashaddr.py | 101 +++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+) create mode 100755 mmgen/proto/bch/cashaddr.py create mode 100755 test/unit_tests_d/ut_cashaddr.py diff --git a/mmgen/proto/bch/cashaddr.py b/mmgen/proto/bch/cashaddr.py new file mode 100755 index 00000000..d41b48a8 --- /dev/null +++ b/mmgen/proto/bch/cashaddr.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2024 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen-wallet +# https://gitlab.com/mmgen/mmgen-wallet + +""" +proto.bch.cashaddr: Bitcoin Cash cashaddr implementation for the MMGen Project +""" + +# Specification: https://upgradespecs.bitcoincashnode.org/cashaddr + +import re +from collections import namedtuple + +b32_matrix = """ + q p z r y 9 x 8 + g f 2 t v d w 0 + s 3 j n 5 4 k h + c e 6 m u a 7 l +""" + +b32a = re.sub(r'\s', '', b32_matrix) + +cashaddr_addr_types = { + 'p2pkh': 0, + 'p2sh': 1, + 'token_pubkey': 2, + 'token_script': 3, + 'unknown': 15, +} +addr_types_rev = {v:k for k,v in cashaddr_addr_types.items()} + +data_sizes = (160, 192, 224, 256, 320, 384, 448, 512) + +def PolyMod(v): + c = 1 + for d in v: + c0 = c >> 35 + c = ((c & 0x07ffffffff) << 5) ^ d + if (c0 & 1): c ^= 0x98f2bc8e61 + if (c0 & 2): c ^= 0x79b76d99e2 + if (c0 & 4): c ^= 0xf33e5fb3c4 + if (c0 & 8): c ^= 0xae2eabe2a8 + if (c0 & 16): c ^= 0x1e4f43e470 + return c ^ 1 + +def parse_ver_byte(ver): + assert not (ver >> 7), 'invalid version byte: most-significant bit must be zero' + t = namedtuple('parsed_version_byte', ['addr_type', 'bitlen']) + return t(addr_types_rev[ver >> 3], data_sizes[ver & 7]) + +def make_ver_byte(addr_type, bitlen): + assert addr_type in addr_types_rev, f'{addr_type}: invalid addr type' + return (addr_type << 3) | data_sizes.index(bitlen) + +def bin2vec(data_bin): + assert not len(data_bin) % 5, f'{len(data_bin)}: data length not a multiple of 5' + return [int(data_bin[i*5:(i*5)+5], 2) for i in range(len(data_bin) // 5)] + +def make_polymod_vec(pfx, payload_vec): + return ([ord(c) & 31 for c in pfx] + [0] + payload_vec) + +def cashaddr_parse_addr(addr): + t = namedtuple('parsed_cashaddr', ['pfx', 'payload']) + return t(*addr.split(':', 1)) + +def cashaddr_encode_addr(addr_type, size, pfx, data): + t = namedtuple('encoded_cashaddr', ['addr', 'pfx', 'payload']) + payload_bin = ( + '{:08b}'.format(make_ver_byte(addr_type, size * 8)) + + '{:0{w}b}'.format(int.from_bytes(data), w=len(data) * 8) + ) + payload_vec = bin2vec(payload_bin + '0' * (-len(payload_bin) % 5)) + chksum_vec = bin2vec('{:040b}'.format(PolyMod(make_polymod_vec(pfx, payload_vec + [0] * 8)))) + payload = ''.join(b32a[i] for i in payload_vec + chksum_vec) + return t(f'{pfx}:{payload}', pfx, payload) + +def cashaddr_decode_addr(addr): + t = namedtuple('decoded_cashaddr', ['pfx', 'payload', 'addr_type', 'bytes', 'chksum']) + a = cashaddr_parse_addr(addr.lower()) + data_bin = ''.join(f'{b32a.index(c):05b}' for c in a.payload) + vi = parse_ver_byte(int(data_bin[:8], 2)) + assert len(data_bin) >= vi.bitlen + 48, 'cashaddr data length too short!' + data = int(data_bin[8:8+vi.bitlen], 2).to_bytes(vi.bitlen // 8) + chksum = int(data_bin[-40:], 2).to_bytes(5) + pad_bin = data_bin[8+vi.bitlen:-40] + assert not pad_bin or pad_bin in '0000', f'{pad_bin}: invalid cashaddr data' + if chksum_chk := PolyMod(make_polymod_vec(a.pfx, [b32a.index(c) for c in a.payload])) != 0: + raise ValueError( + 'checksum check failed\n' + f' address: {addr}\n' + f' result: 0x{chksum_chk:x}\n' + f' checksum: 0x{chksum.hex()}') + return t(a.pfx, a.payload, vi.addr_type, data, chksum) diff --git a/test/unit_tests_d/ut_cashaddr.py b/test/unit_tests_d/ut_cashaddr.py new file mode 100755 index 00000000..304ba770 --- /dev/null +++ b/test/unit_tests_d/ut_cashaddr.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +""" +test.unit_tests_d.ut_cashaddr: unit test for the BCH cashaddr module +""" + +altcoin_dep = True + +from collections import namedtuple + +from mmgen.proto.bch.cashaddr import cashaddr_parse_addr, cashaddr_decode_addr, cashaddr_encode_addr + +from ..include.common import vmsg + +# Source: https://upgradespecs.bitcoincashnode.org/cashaddr +vec_data = """ +F5BF48B397DAE70BE82B3CCA4793F8EB2B6CDAC9 +20 0 bitcoincash:qr6m7j9njldwwzlg9v7v53unlr4jkmx6eylep8ekg2 +20 1 bchtest:pr6m7j9njldwwzlg9v7v53unlr4jkmx6eyvwc0uz5t +20 1 pref:pr6m7j9njldwwzlg9v7v53unlr4jkmx6ey65nvtks5 +20 15 prefix:0r6m7j9njldwwzlg9v7v53unlr4jkmx6ey3qnjwsrf + +7ADBF6C17084BC86C1706827B41A56F5CA32865925E946EA +24 0 bitcoincash:q9adhakpwzztepkpwp5z0dq62m6u5v5xtyj7j3h2ws4mr9g0 +24 1 bchtest:p9adhakpwzztepkpwp5z0dq62m6u5v5xtyj7j3h2u94tsynr +24 1 pref:p9adhakpwzztepkpwp5z0dq62m6u5v5xtyj7j3h2khlwwk5v +24 15 prefix:09adhakpwzztepkpwp5z0dq62m6u5v5xtyj7j3h2p29kc2lp + +3A84F9CF51AAE98A3BB3A78BF16A6183790B18719126325BFC0C075B +28 0 bitcoincash:qgagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkcw59jxxuz +28 1 bchtest:pgagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkcvs7md7wt +28 1 pref:pgagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkcrsr6gzkn +28 15 prefix:0gagf7w02x4wnz3mkwnchut2vxphjzccwxgjvvjmlsxqwkc5djw8s9g + +3173EF6623C6B48FFD1A3DCC0CC6489B0A07BB47A37F47CFEF4FE69DE825C060 +32 0 bitcoincash:qvch8mmxy0rtfrlarg7ucrxxfzds5pamg73h7370aa87d80gyhqxq5nlegake +32 1 bchtest:pvch8mmxy0rtfrlarg7ucrxxfzds5pamg73h7370aa87d80gyhqxq7fqng6m6 +32 1 pref:pvch8mmxy0rtfrlarg7ucrxxfzds5pamg73h7370aa87d80gyhqxq4k9m7qf9 +32 15 prefix:0vch8mmxy0rtfrlarg7ucrxxfzds5pamg73h7370aa87d80gyhqxqsh6jgp6w + +C07138323E00FA4FC122D3B85B9628EA810B3F381706385E289B0B25631197D194B5C238BEB136FB +40 0 bitcoincash:qnq8zwpj8cq05n7pytfmskuk9r4gzzel8qtsvwz79zdskftrzxtar994cgutavfklv39gr3uvz +40 1 bchtest:pnq8zwpj8cq05n7pytfmskuk9r4gzzel8qtsvwz79zdskftrzxtar994cgutavfklvmgm6ynej +40 1 pref:pnq8zwpj8cq05n7pytfmskuk9r4gzzel8qtsvwz79zdskftrzxtar994cgutavfklv0vx5z0w3 +40 15 prefix:0nq8zwpj8cq05n7pytfmskuk9r4gzzel8qtsvwz79zdskftrzxtar994cgutavfklvwsvctzqy + +E361CA9A7F99107C17A622E047E3745D3E19CF804ED63C5C40C6BA763696B98241223D8CE62AD48D863F4CB18C930E4C +48 0 bitcoincash:qh3krj5607v3qlqh5c3wq3lrw3wnuxw0sp8dv0zugrrt5a3kj6ucysfz8kxwv2k53krr7n933jfsunqex2w82sl +48 1 bchtest:ph3krj5607v3qlqh5c3wq3lrw3wnuxw0sp8dv0zugrrt5a3kj6ucysfz8kxwv2k53krr7n933jfsunqnzf7mt6x +48 1 pref:ph3krj5607v3qlqh5c3wq3lrw3wnuxw0sp8dv0zugrrt5a3kj6ucysfz8kxwv2k53krr7n933jfsunqjntdfcwg +48 15 prefix:0h3krj5607v3qlqh5c3wq3lrw3wnuxw0sp8dv0zugrrt5a3kj6ucysfz8kxwv2k53krr7n933jfsunqakcssnmn + +D9FA7C4C6EF56DC4FF423BAAE6D495DBFF663D034A72D1DC7D52CBFE7D1E6858F9D523AC0A7A5C34077638E4DD1A701BD017842789982041 +56 0 bitcoincash:qmvl5lzvdm6km38lgga64ek5jhdl7e3aqd9895wu04fvhlnare5937w4ywkq57juxsrhvw8ym5d8qx7sz7zz0zvcypqscw8jd03f +56 1 bchtest:pmvl5lzvdm6km38lgga64ek5jhdl7e3aqd9895wu04fvhlnare5937w4ywkq57juxsrhvw8ym5d8qx7sz7zz0zvcypqs6kgdsg2g +56 1 pref:pmvl5lzvdm6km38lgga64ek5jhdl7e3aqd9895wu04fvhlnare5937w4ywkq57juxsrhvw8ym5d8qx7sz7zz0zvcypqsammyqffl +56 15 prefix:0mvl5lzvdm6km38lgga64ek5jhdl7e3aqd9895wu04fvhlnare5937w4ywkq57juxsrhvw8ym5d8qx7sz7zz0zvcypqsgjrqpnw8 + +D0F346310D5513D9E01E299978624BA883E6BDA8F4C60883C10F28C2967E67EC77ECC7EEEAEAFC6DA89FAD72D11AC961E164678B868AEEEC5F2C1DA08884175B +64 0 bitcoincash:qlg0x333p4238k0qrc5ej7rzfw5g8e4a4r6vvzyrcy8j3s5k0en7calvclhw46hudk5flttj6ydvjc0pv3nchp52amk97tqa5zygg96mtky5sv5w +64 1 bchtest:plg0x333p4238k0qrc5ej7rzfw5g8e4a4r6vvzyrcy8j3s5k0en7calvclhw46hudk5flttj6ydvjc0pv3nchp52amk97tqa5zygg96mc773cwez +64 1 pref:plg0x333p4238k0qrc5ej7rzfw5g8e4a4r6vvzyrcy8j3s5k0en7calvclhw46hudk5flttj6ydvjc0pv3nchp52amk97tqa5zygg96mg7pj3lh8 +64 15 prefix:0lg0x333p4238k0qrc5ej7rzfw5g8e4a4r6vvzyrcy8j3s5k0en7calvclhw46hudk5flttj6ydvjc0pv3nchp52amk97tqa5zygg96ms92w6845 +""" + +class unit_tests: + + @property + def vectors(self): + t = namedtuple('vectors', ['size', 'type', 'addr', 'data']) + def gen(): + for a in vec_data.splitlines(): + if a: + d = a.split() + if len(d) == 1: + data = d[0].lower() + else: + yield t(int(d[0]), int(d[1]), d[2], data) + return list(gen()) + + def encode(self, name, ut, desc='low-level address encoding'): + data = None + for v in self.vectors: + if not data or data != v.data: + data = v.data + vmsg(f'\n{data}') + vmsg(f' {v.addr}') + ret = cashaddr_encode_addr(v.type, v.size, cashaddr_parse_addr(v.addr).pfx, bytes.fromhex(v.data)) + assert ret.addr == v.addr + return True + + def decode(self, name, ut, desc='low-level address decoding'): + data = None + for v in self.vectors: + if not data or data != v.data: + data = v.data + vmsg(f'\n{data}') + vmsg(f' {v.addr}') + ret = cashaddr_decode_addr(v.addr) + assert ret.bytes.hex() == v.data + return True