Browse Source

BCH cashaddr: low-level support

The MMGen Project 5 months ago
parent
commit
3c726f9091
2 changed files with 200 additions and 0 deletions
  1. 99 0
      mmgen/proto/bch/cashaddr.py
  2. 101 0
      test/unit_tests_d/ut_cashaddr.py

+ 99 - 0
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 <mmgen@tuta.io>
+# 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)

+ 101 - 0
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