diff --git a/mmgen/addr.py b/mmgen/addr.py index 41caf305..07e52976 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -42,6 +42,7 @@ class AddrGenerator(MMGenObject): gen_methods = { 'p2pkh': AddrGeneratorP2PKH, 'segwit': AddrGeneratorSegwit, + 'bech32': AddrGeneratorBech32, 'ethereum': AddrGeneratorEthereum, 'zcash_z': AddrGeneratorZcashZ, 'monero': AddrGeneratorMonero} @@ -68,6 +69,15 @@ class AddrGeneratorSegwit(AddrGenerator): assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit' return HexStr(g.proto.pubhex2redeem_script(pubhex)) +class AddrGeneratorBech32(AddrGenerator): + def to_addr(self,pubhex): + assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit' + from mmgen.protocol import hash160 + return CoinAddr(g.proto.pubhash2bech32addr(hash160(pubhex))) + + def to_segwit_redeem_script(self,pubhex): + raise NotImplementedError,'Segwit redeem script not supported by this address type' + class AddrGeneratorEthereum(AddrGenerator): def to_addr(self,pubhex): assert type(pubhex) == PubKey diff --git a/mmgen/bech32.py b/mmgen/bech32.py new file mode 100644 index 00000000..555e8c93 --- /dev/null +++ b/mmgen/bech32.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# +# Source URL: +# https://github.com/bitcoin/bitcoin/blob/v0.16.0/test/functional/test_framework/segwit_addr.py +# +# Copyright (c) 2017 Pieter Wuille +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +# +# Unaltered except for the following changes by the MMGen Project: +# 'python3' changed to 'python' in the hashbang +# leading spaces converted to tabs +# +"""Reference implementation for Bech32 and segwit addresses.""" + +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_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 + +def bech32_create_checksum(hrp, data): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + +def bech32_encode(hrp, data): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + +def bech32_decode(bech): + """Validate a Bech32 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) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + if not bech32_verify_checksum(hrp, data): + return (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: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + return (data[0], decoded) + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) + if decode(hrp, ret) == (None, None): + return None + return ret diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py index dfed5503..fa50c731 100755 --- a/mmgen/main_addrimport.py +++ b/mmgen/main_addrimport.py @@ -74,7 +74,7 @@ cmd_args = opts.init(opts_data) def import_mmgen_list(infile): al = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile) - if al.al_id.mmtype == 'S': + if al.al_id.mmtype in ('S','B'): from mmgen.tx import segwit_is_active if not segwit_is_active(): rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses') diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index 0ebdf80a..fcb340b6 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -120,7 +120,7 @@ opts_data = lambda: { -q, --quiet Produce quieter output -r, --usr-randchars=n Get 'n' characters of additional randomness from user (min={g.min_urandchars}, max={g.max_urandchars}) --t, --type=t Specify address type (valid options: 'compressed','segwit','zcash_z') +-t, --type=t Specify address type (valid options: 'compressed','segwit','bech32','zcash_z') -v, --verbose Produce more verbose output """.format(g=g), 'notes': """ diff --git a/mmgen/obj.py b/mmgen/obj.py index ada0152b..e5978c9d 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -394,7 +394,9 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject): proto = g.proto.get_protocol_by_chain(chain) vn = proto.addr_ver_num - if self.addr_fmt == 'p2sh' and 'p2sh2' in vn: + if self.addr_fmt == 'bech32': + return self[:len(proto.bech32_hrp)] == proto.bech32_hrp + elif self.addr_fmt == 'p2sh' and 'p2sh2' in vn: return pfx_ok(vn['p2sh'][1]) or pfx_ok(vn['p2sh2'][1]) else: return pfx_ok(vn[self.addr_fmt][1]) @@ -705,6 +707,12 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject): 'gen_method':'segwit', 'addr_fmt':'p2sh', 'desc':'Segwit P2SH-P2WPKH address' }, + 'B': { 'name':'bech32', + 'pubkey_type':'std', + 'compressed':True, + 'gen_method':'bech32', + 'addr_fmt':'bech32', + 'desc':'Native Segwit (Bech32) address' }, 'E': { 'name':'ethereum', 'pubkey_type':'std', 'compressed':False, diff --git a/mmgen/opts.py b/mmgen/opts.py index d79d5272..9f3a8785 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -302,6 +302,9 @@ def init(opts_f,add_opts=[],opt_filter=None): g.rpc_user = rt.rpc_user g.rpc_password = rt.rpc_password + if g.regtest and hasattr(g.proto,'bech32_hrp_rt'): + g.proto.bech32_hrp = g.proto.bech32_hrp_rt + # Check user-set opts without modifying them if not check_opts(uopts): sys.exit(1) diff --git a/mmgen/protocol.py b/mmgen/protocol.py index a2332d51..7107ec50 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -25,6 +25,7 @@ from binascii import unhexlify from mmgen.util import msg,pmsg,Msg,pdie from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt,B2XAmt from mmgen.globalvars import g +import mmgen.bech32 as bech32 def hash160(hexnum): # take hex, return hex - OP_HASH160 return hashlib.new('ripemd160',hashlib.sha256(unhexlify(hexnum)).digest()).hexdigest() @@ -67,7 +68,7 @@ class BitcoinProtocol(MMGenObject): daemon_name = 'bitcoind' addr_ver_num = { 'p2pkh': ('00','1'), 'p2sh': ('05','3') } wif_ver_num = { 'std': '80' } - mmtypes = ('L','C','S') + mmtypes = ('L','C','S','B') dfl_mmtype = 'L' data_subdir = '' rpc_port = 8332 @@ -91,6 +92,8 @@ class BitcoinProtocol(MMGenObject): # but OP_1 through OP_16 are encoded as 0x51 though 0x60 (81 to 96 in decimal). witness_vernum_hex = '00' witness_vernum = int(witness_vernum_hex,16) + bech32_addr_width = 40 + bech32_hrp = 'bc' @staticmethod def get_protocol_by_chain(chain): @@ -129,6 +132,19 @@ class BitcoinProtocol(MMGenObject): @classmethod def verify_addr(cls,addr,hex_width,return_dict=False): + + if 'B' in cls.mmtypes and addr[:len(cls.bech32_hrp)] == cls.bech32_hrp: + ret = bech32.decode(cls.bech32_hrp,addr) + if ret[0] != cls.witness_vernum: + msg('{}: Invalid witness version number'.format(ret[0])) + elif ret[1]: + return { + 'hex': ''.join([chr(b) for b in ret[1]]).encode('hex'), + 'format': 'bech32', + 'width': cls.bech32_addr_width, + } if return_dict else True + return False + for addr_fmt in cls.addr_ver_num: ver_num,pfx = cls.addr_ver_num[addr_fmt] if type(pfx) == tuple: @@ -173,6 +189,11 @@ class BitcoinProtocol(MMGenObject): def pubhex2segwitaddr(cls,pubhex): return cls.pubhash2addr(hash160(cls.pubhex2redeem_script(pubhex)),p2sh=True) + @classmethod + def pubhash2bech32addr(cls,pubhash): + d = [ord(b) for b in pubhash.decode('hex')] + return bech32.bech32_encode(cls.bech32_hrp,[cls.witness_vernum]+bech32.convertbits(d,8,5)) + class BitcoinTestnetProtocol(BitcoinProtocol): addr_ver_num = { 'p2pkh': ('6f',('m','n')), 'p2sh': ('c4','2') } wif_ver_num = { 'std': 'ef' } @@ -180,6 +201,8 @@ class BitcoinTestnetProtocol(BitcoinProtocol): daemon_data_subdir = 'testnet3' rpc_port = 18332 addr_width = 35 + bech32_hrp = 'tb' + bech32_hrp_rt = 'bcrt' class BitcoinCashProtocol(BitcoinProtocol): # TODO: assumes MSWin user installs in custom dir 'Bitcoin_ABC' diff --git a/mmgen/tx.py b/mmgen/tx.py index cd036373..5125dbfe 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -135,6 +135,8 @@ def scriptPubKey2addr(s): return g.proto.pubhash2addr(s[6:-4],p2sh=False) elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87': return g.proto.pubhash2addr(s[4:-2],p2sh=True) + elif len(s) == 44 and s[:4] == g.proto.witness_vernum_hex + '14': + return g.proto.pubhash2bech32addr(s[4:]) else: raise NotImplementedError,'Unknown scriptPubKey ({})'.format(s) @@ -372,7 +374,7 @@ class MMGenTX(MMGenObject): return self.add_comment(self) def has_segwit_inputs(self): - return any(i.mmid and i.mmid.mmtype == 'S' for i in self.inputs) + return any(i.mmid and i.mmid.mmtype in ('S','B') for i in self.inputs) def compare_size_and_estimated_size(self): vsize = self.estimate_size() @@ -410,6 +412,7 @@ class MMGenTX(MMGenObject): 'L': isize_common + sig_size + pubkey_size_uncompressed, # = 180 'C': isize_common + sig_size + pubkey_size_compressed, # = 148 'S': isize_common + 23, # = 64 + 'B': isize_common + 0 # = 41 } ret = sum(input_size[i.mmid.mmtype] for i in self.inputs if i.mmid) @@ -420,8 +423,8 @@ class MMGenTX(MMGenObject): def get_outputs_size(): # output bytes = amt: 8, byte_count: 1+, pk_script - # pk_script bytes: p2pkh: 25, p2sh: 23 - return sum({'p2pkh':34,'p2sh':32}[o.addr.addr_fmt] for o in self.outputs) + # pk_script bytes: p2pkh: 25, p2sh: 23, bech32: 22 + return sum({'p2pkh':34,'p2sh':32,'bech32':31}[o.addr.addr_fmt] for o in self.outputs) # https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki # The witness is a serialization of all witness data of the transaction. Each txin is @@ -434,7 +437,7 @@ class MMGenTX(MMGenObject): def get_witness_size(): if not self.has_segwit_inputs(): return 0 wf_size = 1 + 1 + sig_size + 1 + pubkey_size_compressed # vInt vInt sig vInt pubkey = 108 - return sum((1,wf_size)[bool(i.mmid) and i.mmid.mmtype == 'S'] for i in self.inputs) + return sum((1,wf_size)[bool(i.mmid) and i.mmid.mmtype in ('S','B')] for i in self.inputs) isize = get_inputs_size() osize = get_outputs_size() @@ -716,10 +719,10 @@ class MMGenTX(MMGenObject): assert type(ti['witness']) == list and len(ti['witness']) == 2, 'malformed witness' assert len(ti['witness'][1]) == 66, 'incorrect witness pubkey length' assert mmti.mmid, fs.format('witness-type','non-MMGen') - assert mmti.mmid.mmtype == 'S', fs.format('witness-type',mmti.mmid.mmtype) + assert mmti.mmid.mmtype in ('S','B'), fs.format('witness-type',mmti.mmid.mmtype) else: # non-witness if mmti.mmid: - assert mmti.mmid.mmtype != 'S', fs.format('signature in',mmti.mmid.mmtype) + assert mmti.mmid.mmtype not in ('S','B'), fs.format('signature in',mmti.mmid.mmtype) assert not 'witness' in ti, 'non-witness input has witness' # sig_size 72 (DER format), pubkey_size 'compressed':33, 'uncompressed':65 assert (200 < len(ti['scriptSig']) < 300), 'malformed scriptSig' # VERY rough check @@ -727,7 +730,7 @@ class MMGenTX(MMGenObject): return True def has_segwit_outputs(self): - return any(o.mmid and o.mmid.mmtype == 'S' for o in self.outputs) + return any(o.mmid and o.mmid.mmtype in ('S','B') for o in self.outputs) def is_in_mempool(self): return 'size' in g.rpch.getmempoolentry(self.coin_txid,on_fail='silent') diff --git a/scripts/test-release.sh b/scripts/test-release.sh index 8639daad..eac6290d 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -143,6 +143,7 @@ if [ "$MINGW" ]; then "$gentest_py --coin=btc 2 $rounds" "$gentest_py --coin=btc --type=compressed 2 $rounds" "$gentest_py --coin=btc --type=segwit 2 $rounds" + "$gentest_py --coin=btc --type=bech32 2 $rounds" "$gentest_py --coin=ltc 2 $rounds" "$gentest_py --coin=ltc --type=compressed 2 $rounds" "$gentest_py --coin=ltc --type=segwit 2 $rounds" @@ -156,6 +157,7 @@ else "$gentest_py --coin=btc 2 $rounds" "$gentest_py --coin=btc --type=compressed 2 $rounds" "$gentest_py --coin=btc --type=segwit 2 $rounds" + "$gentest_py --coin=btc --type=bech32 2 $rounds" "$gentest_py --coin=ltc 2 $rounds" "$gentest_py --coin=ltc --type=compressed 2 $rounds" "$gentest_py --coin=ltc --type=segwit 2 $rounds" @@ -218,6 +220,7 @@ t_btc=( "$test_py -On" "$test_py -On --segwit dfl_wallet main ref ref_other" "$test_py -On --segwit-random dfl_wallet main" + "$test_py -On --bech32 dfl_wallet main ref ref_other" "$tooltest_py rpc" "$python scripts/compute-file-chksum.py $REFDIR/*testnet.rawtx >/dev/null 2>&1") f_btc='You may stop the bitcoin (mainnet) daemon if you wish' @@ -228,6 +231,7 @@ t_btc_tn=( "$test_py -On --testnet=1" "$test_py -On --testnet=1 --segwit dfl_wallet main ref ref_other" "$test_py -On --testnet=1 --segwit-random dfl_wallet main" + "$test_py -On --testnet=1 --bech32 dfl_wallet main ref ref_other" "$tooltest_py --testnet=1 rpc") f_btc_tn='You may stop the bitcoin testnet daemon if you wish' @@ -309,6 +313,7 @@ t_gen=( "$gentest_py -q 2 $REFDIR/btcwallet.dump" "$gentest_py -q 1:2 $gen_rounds" "$gentest_py -q --type=segwit 1:2 $gen_rounds" + "$gentest_py -q --type=bech32 1:2 $gen_rounds" "$gentest_py -q --testnet=1 2 $REFDIR/btcwallet-testnet.dump" "$gentest_py -q --testnet=1 1:2 $gen_rounds" "$gentest_py -q --testnet=1 --type=segwit 1:2 $gen_rounds" diff --git a/setup.py b/setup.py index 7eb584ae..17d57fc2 100755 --- a/setup.py +++ b/setup.py @@ -110,6 +110,7 @@ setup( 'mmgen.__init__', 'mmgen.addr', 'mmgen.altcoin', + 'mmgen.bech32', 'mmgen.protocol', 'mmgen.color', 'mmgen.common', diff --git a/test/ref/98831F3A-B[1,31-33,500-501,1010-1011].addrs b/test/ref/98831F3A-B[1,31-33,500-501,1010-1011].addrs new file mode 100644 index 00000000..3ce9e2e9 --- /dev/null +++ b/test/ref/98831F3A-B[1,31-33,500-501,1010-1011].addrs @@ -0,0 +1,19 @@ +# MMGen address file +# +# This file is editable. +# Everything following a hash symbol '#' is a comment and ignored by MMGen. +# A text label of 32 characters or less may be added to the right of each +# address, and it will be appended to the tracking wallet label upon import. +# The label may contain any printable ASCII symbol. +# Address data checksum for 98831F3A-B[1,31-33,500-501,1010-1011]: 9D2A D4B6 5117 F02E +# Record this value to a secure location. +98831F3A BECH32 { + 1 bc1q8snv94j6959y3gkqv4gku0cm5mersnpucsvw5z + 31 bc1qvan6huak4z6wynjhc5hjdv8vd2xj6zrjpc6xks + 32 bc1q5nmmd6z2yavfwhl94t86n55k7vufvaz4xcvlud + 33 bc1qp7uv0f0cm944r5mddqvqq828fjkau2ny427djj + 500 bc1qj5l8yt9qysfx0xgsls8s0hqwjl59hq79agjzwp + 501 bc1qma70d8wttaslfyupc4ff0z9avuf57sdzdfe80s + 1010 bc1qqh6u944ta66gp85e2tzr9r5h9wfjfflyqn5rwl + 1011 bc1qu5249g7zajgp56v3fhjw2dt7g08zsx66hs2sd8 +} diff --git a/test/ref/98831F3A-B[1,31-33,500-501,1010-1011].testnet.addrs b/test/ref/98831F3A-B[1,31-33,500-501,1010-1011].testnet.addrs new file mode 100644 index 00000000..834bf180 --- /dev/null +++ b/test/ref/98831F3A-B[1,31-33,500-501,1010-1011].testnet.addrs @@ -0,0 +1,19 @@ +# MMGen address file +# +# This file is editable. +# Everything following a hash symbol '#' is a comment and ignored by MMGen. +# A text label of 32 characters or less may be added to the right of each +# address, and it will be appended to the tracking wallet label upon import. +# The label may contain any printable ASCII symbol. +# Address data checksum for 98831F3A-B[1,31-33,500-501,1010-1011]: BA07 0DD8 E2A6 2C5A +# Record this value to a secure location. +98831F3A BECH32 { + 1 tb1q8snv94j6959y3gkqv4gku0cm5mersnpujkha03 + 31 tb1qvan6huak4z6wynjhc5hjdv8vd2xj6zrjt7p4dr + 32 tb1q5nmmd6z2yavfwhl94t86n55k7vufvaz4v7hv87 + 33 tb1qp7uv0f0cm944r5mddqvqq828fjkau2nylv97fp + 500 tb1qj5l8yt9qysfx0xgsls8s0hqwjl59hq79hwf34j + 501 tb1qma70d8wttaslfyupc4ff0z9avuf57sdz80z55r + 1010 tb1qqh6u944ta66gp85e2tzr9r5h9wfjffly240s4v + 1011 tb1qu5249g7zajgp56v3fhjw2dt7g08zsx66ak3rk5 +} diff --git a/test/scrambletest.py b/test/scrambletest.py index 94057fdd..3baee027 100755 --- a/test/scrambletest.py +++ b/test/scrambletest.py @@ -60,6 +60,7 @@ test_data = OrderedDict([ ('btc', ('456d7f5f1c4bfe3b','(none)', '', '', '1MU7EdgqYy9JX35L25hR6CmXXcSEBDAwyv')), ('btc_compressed',('bf98a4af5464a4ef','compressed', '-C', 'COMPRESSED','1F97Jd89wwmu4ELadesAdGDzg3d8Y6j5iP')), ('btc_segwit', ('b56962d829ffc678','segwit', '-S', 'SEGWIT', '36TvVzU5mxSjJ3D9qKAmYzCV7iUqtTDezF')), +('btc_bech32', ('d09eea818f9ad17f','bech32', '-B', 'BECH32', 'bc1q8snv94j6959y3gkqv4gku0cm5mersnpucsvw5z')), ('bch', ('456d7f5f1c4bfe3b','(none)', '', '', '1MU7EdgqYy9JX35L25hR6CmXXcSEBDAwyv')), ('bch_compressed',('bf98a4af5464a4ef','compressed', '-C', 'COMPRESSED','1F97Jd89wwmu4ELadesAdGDzg3d8Y6j5iP')), ('ltc', ('b11f16632e63ba92','ltc:legacy', '-LTC','LTC', 'LMxB474SVfxeYdqxNrM1WZDZMnifteSMv1')), diff --git a/test/test.py b/test/test.py index add7a126..8d47cd60 100755 --- a/test/test.py +++ b/test/test.py @@ -111,6 +111,7 @@ opts_data = lambda: { 'options': """ -h, --help Print this help message --, --longhelp Print help message for long options (common options) +-B, --bech32 Generate and use Bech32 addresses -b, --buf-keypress Use buffered keypresses as with real human input -c, --print-cmdline Print the command line of each spawned command -C, --coverage Produce code coverage info using trace module @@ -161,12 +162,12 @@ txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[coin_sel] rtFundAmt = {'btc':'500','bch':'500','ltc':'5500'}[coin_sel] rtFee = { - 'btc': ('20s','10s','60s','0.0001','10s','20s'), + 'btc': ('20s','10s','60s','31s','10s','20s'), 'bch': ('20s','10s','60s','0.0001','10s','20s'), 'ltc': ('1000s','500s','1500s','0.05','400s','1000s') }[coin_sel] rtBals = { - 'btc': ('499.9999488','399.9998282','399.9998147','399.9996875','6.79000000','993.20958750','999.99958750'), + 'btc': ('499.9999488','399.9998282','399.9998147','399.9996877','13.00000000','986.99933647','999.99933647'), 'bch': ('499.9999416','399.9999124','399.99989','399.9997616','276.22339397','723.77626763','999.99966160'), 'ltc': ('5499.9971','5399.994085','5399.993545','5399.987145','13.00000000','10986.93714500','10999.93714500'), }[coin_sel] @@ -174,11 +175,13 @@ rtBobOp3 = {'btc':'S:2','bch':'L:3','ltc':'S:2'}[coin_sel] if opt.segwit and 'S' not in g.proto.mmtypes: die(1,'--segwit option incompatible with {}'.format(g.proto.__name__)) +if opt.bech32 and 'B' not in g.proto.mmtypes: + die(1,'--bech32 option incompatible with {}'.format(g.proto.__name__)) def randbool(): return hexlify(os.urandom(1))[1] in '12345678' def get_segwit_bool(): - return randbool() if opt.segwit_random else True if opt.segwit else False + return randbool() if opt.segwit_random else True if opt.segwit or opt.bech32 else False def disable_debug(): ds = os.getenv('MMGEN_DEBUG') if ds is not None: os.environ['MMGEN_DEBUG'] = '' @@ -316,6 +319,10 @@ cfgs = { 'btc': ('9914 6D10 2307 F348','7DBF 441F E188 8B37'), 'ltc': ('CC09 A190 B7DF B7CD','3676 4C49 14F8 1AD0'), }, + 'addrfile_bech32_chk': { + 'btc': ('C529 D686 31AA ACD4','13C5 493E 1CD7 2852'), + 'ltc': ('CC09 A190 B7DF B7CD','3676 4C49 14F8 1AD0'), + }, 'addrfile_compressed_chk': { 'btc': ('95EB 8CC0 7B3B 7856','629D FDE4 CDC0 F276'), 'ltc': ('35D5 8ECA 9A42 46C3','37E9 A36E 94A2 010F'), @@ -328,6 +335,10 @@ cfgs = { 'btc': ('C13B F717 D4E8 CF59','4DB5 BAF0 45B7 6E81'), 'ltc': ('054B 9794 55B4 5D82','C373 0074 DEE6 B70A'), }, + 'keyaddrfile_bech32_chk': { + 'btc': ('934F 1C33 6C06 B18C','D994 DF67 6E53 6F5F'), + 'ltc': ('054B 9794 55B4 5D82','C373 0074 DEE6 B70A'), + }, 'keyaddrfile_compressed_chk': { 'btc': ('E43A FA46 5751 720A','B995 A6CF D1CD FAD0'), 'ltc': ('7603 2FE3 2145 FFAD','3248 356A C707 4A41'), @@ -368,6 +379,9 @@ cfgs = { 'btc': ('91C4 0414 89E4 2089','3BA6 7494 8E2B 858D'), 'ltc': ('8F12 FA7B 9F12 594C','E79E F55B 1536 56F2'), }, + 'addrfile_bech32_chk': { + 'btc': ('2AA3 78DF B965 82EB','5B6A 4D12 820D BC3C'), + }, 'addrfile_compressed_chk': { 'btc': ('2615 8401 2E98 7ECA','DF38 22AB AAB0 124E'), 'ltc': ('197C C48C 3C37 AB0F','5072 15DA 1A90 5E99'), @@ -380,6 +394,9 @@ cfgs = { 'btc': ('C98B DF08 A3D5 204B','25F2 AEB6 AAAC 8BBE'), 'ltc': ('1829 7FE7 2567 CB91','1305 9007 E515 B66A'), }, + 'keyaddrfile_bech32_chk': { + 'btc': ('4A6B 3762 DF30 9368','A68A 53D4 874E 923C'), + }, 'keyaddrfile_compressed_chk': { 'btc': ('6D6D 3D35 04FD B9C3','B345 9CD8 9EAE 5489'), 'ltc': ('F5DA 9D60 6798 C4E9','F928 113B C9D7 9DF5'), @@ -420,6 +437,9 @@ cfgs = { 'btc': ('06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14'), 'ltc': ('63DF E42A 0827 21C3','1A3F 3016 2E2B F33A'), }, + 'addrfile_bech32_chk': { + 'btc': ('9D2A D4B6 5117 F02E','BA07 0DD8 E2A6 2C5A'), + }, 'addrfile_compressed_chk': { 'btc': ('A33C 4FDE F515 F5BC','5186 02C2 535E B7D5'), 'ltc': ('3FC0 8F03 C2D6 BD19','535E 5CDC 1CA7 08D5'), @@ -432,6 +452,9 @@ cfgs = { 'btc': ('A447 12C2 DD14 5A9B','0690 460D A600 D315'), 'ltc': ('E8A3 9F6E E164 A521','70ED 8557 5882 08A5'), }, + 'keyaddrfile_bech32_chk': { + 'btc': ('D0DD BDE3 87BE 15AE','2C7B 70E5 5F96 9B09'), + }, 'keyaddrfile_compressed_chk': { 'btc': ('420A 8EB5 A9E2 7814','3243 DD92 809E FE8D'), 'ltc': ('8D1C 781F EB7F 44BC','678E 8EF9 1396 B140'), @@ -443,6 +466,7 @@ cfgs = { 'ref_wallet': '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]), 'ref_addrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs', 'ref_segwitaddrfile':'98831F3A{}-S[1,31-33,500-501,1010-1011]{}.addrs', + 'ref_bech32addrfile':'98831F3A{}-B[1,31-33,500-501,1010-1011]{}.addrs', 'ref_keyaddrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.akeys.mmenc', 'ref_passwdfile': '98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws', 'ref_addrfile_chksum': { @@ -453,6 +477,9 @@ cfgs = { 'btc': ('06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14'), 'ltc': ('63DF E42A 0827 21C3','1A3F 3016 2E2B F33A'), }, + 'ref_bech32addrfile_chksum': { + 'btc': ('9D2A D4B6 5117 F02E','BA07 0DD8 E2A6 2C5A'), + }, 'ref_keyaddrfile_chksum': { 'btc': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2'), 'ltc': ('B804 978A 8796 3ED4','93A6 844C 8ECC BEF4'), @@ -621,6 +648,7 @@ cmd_group['ref'] = ( cmd_group['ref_other'] = ( ('ref_addrfile_chk', 'saved reference address file'), ('ref_segwitaddrfile_chk','saved reference address file (segwit)'), + ('ref_bech32addrfile_chk','saved reference address file (bech32)'), ('ref_keyaddrfile_chk','saved reference key-address file'), ('ref_passwdfile_chk', 'saved reference password file'), # Create the fake inputs: @@ -886,7 +914,8 @@ os.environ['MMGEN_NO_LICENSE'] = '1' os.environ['MMGEN_MIN_URANDCHARS'] = '3' os.environ['MMGEN_BOGUS_SEND'] = '1' -def get_segwit_arg(cfg): return ([],['--type','segwit'])[cfg['segwit']] +def get_segwit_arg(cfg): + return ['--type='+('segwit','bech32')[bool(opt.bech32)]] if cfg['segwit'] else [] # Tell spawned programs they're running in the test suite os.environ['MMGEN_TEST_SUITE'] = '1' @@ -1022,7 +1051,8 @@ def create_fake_unspent_entry(coinaddr,al_id=None,idx=None,lbl=None,non_mmgen=Fa spk_beg,spk_end = ( ('76a914','88ac'), ('a914','87'), - )[segwit and coinaddr.addr_fmt=='p2sh'] + (g.proto.witness_vernum_hex+'14','') + )[segwit and (coinaddr.addr_fmt=='p2sh') + 2*(coinaddr.addr_fmt=='bech32')] amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[coin_sel] return { 'account': '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) if non_mmgen \ @@ -1451,7 +1481,7 @@ class MMGenTestSuite(object): def addrgen(self,name,wf,pf=None,check_ref=False,ftype='addr',id_str=None,extra_args=[],mmtype=None): if ftype[:4] != 'pass' and not mmtype: - if cfg['segwit']: mmtype = 'segwit' + if cfg['segwit']: mmtype = ('segwit','bech32')[bool(opt.bech32)] cmd_pfx = (ftype,'pass')[ftype[:4]=='pass'] t = MMGenExpect(name,'mmgen-{}gen'.format(cmd_pfx), ['-d',cfg['tmpdir']] + @@ -1459,7 +1489,8 @@ class MMGenTestSuite(object): ([],['--type='+str(mmtype)])[bool(mmtype)] + ([],[wf])[bool(wf)] + ([],[id_str])[bool(id_str)] + - [cfg['{}_idx_list'.format(cmd_pfx)]],extra_desc=('','(segwit)')[mmtype=='segwit']) + [cfg['{}_idx_list'.format(cmd_pfx)]], + extra_desc='({})'.format(mmtype) if mmtype in ('segwit','bech32') else '') t.license() t.passphrase('MMGen wallet',cfg['wpasswd']) t.expect('Passphrase is OK') @@ -1487,7 +1518,7 @@ class MMGenTestSuite(object): self.addrgen(name,wf,pf=pf,check_ref=True) def refaddrgen_compressed(self,name,wf,pf): - if opt.segwit: + if opt.segwit or opt.bech32: msg('Skipping non-Segwit address generation'); return True self.addrgen(name,wf,pf=pf,check_ref=True,mmtype='compressed') @@ -1750,10 +1781,11 @@ class MMGenTestSuite(object): def keyaddrgen(self,name,wf,pf=None,check_ref=False,mmtype=None): if cfg['segwit'] and not mmtype: - mmtype = 'segwit' + mmtype = ('segwit','bech32')[bool(opt.bech32)] args = ['-d',cfg['tmpdir'],usr_rand_arg,wf,cfg['addr_idx_list']] t = MMGenExpect(name,'mmgen-keygen', - ([],['--type='+str(mmtype)])[bool(mmtype)] + args,extra_desc=('','(segwit)')[mmtype=='segwit']) + ([],['--type='+str(mmtype)])[bool(mmtype)] + args, + extra_desc='({})'.format(mmtype) if mmtype in ('segwit','bech32') else '') t.license() t.passphrase('MMGen wallet',cfg['wpasswd']) chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True) @@ -1773,7 +1805,7 @@ class MMGenTestSuite(object): self.keyaddrgen(name,wf,pf,check_ref=True) def refkeyaddrgen_compressed(self,name,wf,pf): - if opt.segwit: + if opt.segwit or opt.bech32: msg('Skipping non-Segwit key-address generation'); return True self.keyaddrgen(name,wf,pf,check_ref=True,mmtype='compressed') @@ -2088,7 +2120,8 @@ class MMGenTestSuite(object): af_fn = cfg[af_key].format(pfx or altcoin_pfx,'' if coin else tn_ext) af = os.path.join(ref_dir,(subdir or ref_subdir,'')[ftype=='passwd'],af_fn) coin_arg = [] if coin == None else ['--coin='+coin] - t = MMGenExpect(name,'mmgen-tool',coin_arg+[ftype.replace('segwit','')+'file_chksum',af]+add_args) + tool_cmd = ftype.replace('segwit','').replace('bech32','')+'file_chksum' + t = MMGenExpect(name,'mmgen-tool',coin_arg+[tool_cmd,af]+add_args) if ftype == 'keyaddr': w = 'key-address data' t.hash_preset(w,ref_kafile_hash_preset) @@ -2153,6 +2186,12 @@ class MMGenTestSuite(object): else: self.ref_addrfile_chk(name,ftype='segwitaddr') + def ref_bech32addrfile_chk(self,name): + if not 'B' in g.proto.mmtypes: + msg_r('Skipping {} (not supported)'.format(name)); ok() + else: + self.ref_addrfile_chk(name,ftype='bech32addr') + # def txcreate8(self,name,addrfile): # self.txcreate_common(name,sources=['8']) @@ -2279,7 +2318,7 @@ class MMGenTestSuite(object): def regtest_addrgen_alice(self,name): self.regtest_addrgen(name,'alice') def regtest_addrimport(self,name,user,sid=None,addr_range='1-5',num_addrs=5): - id_strs = { 'legacy':'', 'compressed':'-C', 'segwit':'-S' } + id_strs = { 'legacy':'', 'compressed':'-C', 'segwit':'-S', 'bech32':'-B' } if not sid: sid = self.regtest_user_sid(user) from mmgen.addr import MMGenAddrType for mmtype in g.proto.mmtypes: @@ -2512,7 +2551,7 @@ class MMGenTestSuite(object): amts = (a for a in (1.12345678,2.87654321,3.33443344,4.00990099,5.43214321)) outputs1 = ['{},{}'.format(a,amts.next()) for a in addrs] sid = self.regtest_user_sid('bob') - l1,l2 = (':S',':S') if g.proto.cap('segwit') else (':L',':L') + l1,l2 = (':S',':B') if g.proto.cap('segwit') else (':L',':L') outputs2 = [sid+':C:2,6.333', sid+':L:3,6.667',sid+l1+':4,0.123',sid+l2+':5'] return self.regtest_user_txdo(name,'bob',rtFee[5],outputs1+outputs2,'1-2')