From 715985d8c5a244a30bbf1f6104a58d0f029af1fb Mon Sep 17 00:00:00 2001 From: undeath Date: Wed, 11 Oct 2017 19:44:50 +0200 Subject: [PATCH 1/3] add support for non-segwit maker --- jmclient/jmclient/__init__.py | 2 +- jmclient/jmclient/maker.py | 6 ++--- jmclient/jmclient/wallet.py | 34 +++++++++++++++++++++++------ jmclient/jmclient/wallet_utils.py | 16 ++++++-------- jmclient/jmclient/yieldgenerator.py | 20 ++++++++--------- 5 files changed, 48 insertions(+), 30 deletions(-) diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index 8e980c6..e285696 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -18,7 +18,7 @@ from .slowaes import decryptData, encryptData from .taker import Taker from .wallet import (AbstractWallet, BitcoinCoreInterface, Wallet, BitcoinCoreWallet, estimate_tx_fee, WalletError, - create_wallet_file, SegwitWallet, Bip39Wallet) + create_wallet_file, SegwitWallet, Bip39Wallet, get_wallet_cls) from .configure import (load_program_config, get_p2pk_vbyte, jm_single, get_network, validate_address, get_irc_mchannels, get_blockchain_interface_instance, get_p2sh_vbyte, set_config) diff --git a/jmclient/jmclient/maker.py b/jmclient/jmclient/maker.py index 773675c..38543d0 100644 --- a/jmclient/jmclient/maker.py +++ b/jmclient/jmclient/maker.py @@ -69,8 +69,7 @@ class Maker(object): if res[0]['value'] < reqd_amt: reason = "commitment utxo too small: " + str(res[0]['value']) return reject(reason) - if res[0]['address'] != btc.pubkey_to_p2sh_p2wpkh_address(cr_dict['P'], - self.wallet.get_vbyte()): + if res[0]['address'] != self.wallet.pubkey_to_address(cr_dict['P']): reason = "Invalid podle pubkey: " + str(cr_dict['P']) return reject(reason) @@ -147,7 +146,8 @@ class Maker(object): times_seen_cj_addr = 0 times_seen_change_addr = 0 for outs in txd['outs']: - addr = btc.script_to_address(outs['script'], get_p2sh_vbyte()) + #FIXME: the type of address should be detected from the script (p2pkh/p2sh) + addr = self.wallet.script_to_address(outs['script']) if addr == cjaddr: times_seen_cj_addr += 1 if outs['value'] != amount: diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index a48d6af..cb220d8 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -101,6 +101,10 @@ class AbstractWallet(object): """ return None + @classmethod + def pubkey_to_address(cls, pubkey): + return None + def update_cache_index(self): pass @@ -214,12 +218,17 @@ class Wallet(AbstractWallet): """ return btc.sign(tx, i, priv) - def script_to_address(self, script): + @classmethod + def script_to_address(cls, script): """Return the address for a given output script, which will be p2pkh for the default Wallet object, and reading the correct network byte from the config. """ - return btc.script_to_address(script, get_p2pk_vbyte()) + return btc.script_to_address(script, cls.get_vbyte()) + + @classmethod + def pubkey_to_address(cls, pubkey): + return btc.pubkey_to_address(pubkey, cls.get_vbyte()) def read_wallet_file_data(self, filename, pwd=None, wallet_dir=None): self.path = None @@ -385,8 +394,8 @@ class Wallet(AbstractWallet): self.spent_utxos += removed_utxos.keys() return removed_utxos - - def get_vbyte(self): + @staticmethod + def get_vbyte(): return get_p2pk_vbyte() def add_new_utxos(self, tx, txid): @@ -467,7 +476,8 @@ class SegwitWallet(Bip39Wallet): root = btc.bip32_ckd(pre_root, testnet_flag + 2**31) return [btc.bip32_ckd(root, c + 2**31) for c in range(self.max_mix_depth)] - def get_vbyte(self): + @staticmethod + def get_vbyte(): return get_p2sh_vbyte() def get_txtype(self): @@ -484,13 +494,18 @@ class SegwitWallet(Bip39Wallet): pub = btc.privtopub(self.get_key(mixing_depth, forchange, i)) return btc.pubkey_to_p2sh_p2wpkh_address(pub, magicbyte=self.get_vbyte()) - def script_to_address(self, script): + @classmethod + def script_to_address(cls, script): """Return the address for a given output script, which will be p2sh-p2wpkh for the segwit (currently). The underlying witness is however invisible at this layer; so it's just a p2sh address. """ - return btc.script_to_address(script, get_p2sh_vbyte()) + return btc.script_to_address(script, cls.get_vbyte()) + + @classmethod + def pubkey_to_address(cls, pubkey): + return btc.pubkey_to_p2sh_p2wpkh_address(pubkey, cls.get_vbyte()) def sign(self, tx, i, priv, amount): """Sign a transaction; the amount field @@ -551,3 +566,8 @@ class BitcoinCoreWallet(AbstractWallet): #pragma: no cover if exc.code != -14: raise exc # Wrong passphrase, try again. + +def get_wallet_cls(): + if jm_single().config.get("POLICY", "segwit") == "true": + return SegwitWallet + return Wallet diff --git a/jmclient/jmclient/wallet_utils.py b/jmclient/jmclient/wallet_utils.py index cb16f3e..de24ce7 100644 --- a/jmclient/jmclient/wallet_utils.py +++ b/jmclient/jmclient/wallet_utils.py @@ -9,10 +9,10 @@ from datetime import datetime from mnemonic import Mnemonic from optparse import OptionParser import getpass -from jmclient import (get_network, Wallet, Bip39Wallet, podle, +from jmclient import (get_network, get_wallet_cls, Bip39Wallet, podle, encryptData, get_p2sh_vbyte, get_p2pk_vbyte, jm_single, mn_decode, mn_encode, BitcoinCoreInterface, - JsonRpcError, sync_wallet, WalletError, SegwitWallet) + JsonRpcError, sync_wallet, WalletError) from jmbase.support import get_password import jmclient.btc as btc @@ -815,8 +815,6 @@ def wallet_tool_main(wallet_root_path): """ parser = get_wallettool_parser() (options, args) = parser.parse_args() - walletclass = SegwitWallet if jm_single().config.get( - "POLICY", "segwit") == "true" else Wallet # if the index_cache stored in wallet.json is longer than the default # then set maxmixdepth to the length of index_cache maxmixdepth_configured = True @@ -840,15 +838,15 @@ def wallet_tool_main(wallet_root_path): seed = args[0] method = ('display' if len(args) == 1 else args[1].lower()) if not os.path.exists(os.path.join(wallet_root_path, seed)): - wallet = walletclass(seed, None, options.maxmixdepth, - options.gaplimit, extend_mixdepth= not maxmixdepth_configured, - storepassword=(method == 'importprivkey'), - wallet_dir=wallet_root_path) + wallet = get_wallet_cls()(seed, None, options.maxmixdepth, + options.gaplimit, extend_mixdepth= not maxmixdepth_configured, + storepassword=(method == 'importprivkey'), + wallet_dir=wallet_root_path) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") - wallet = walletclass(seed, pwd, + wallet = get_wallet_cls()(seed, pwd, options.maxmixdepth, options.gaplimit, extend_mixdepth=not maxmixdepth_configured, diff --git a/jmclient/jmclient/yieldgenerator.py b/jmclient/jmclient/yieldgenerator.py index df08851..72dc585 100644 --- a/jmclient/jmclient/yieldgenerator.py +++ b/jmclient/jmclient/yieldgenerator.py @@ -9,7 +9,7 @@ from twisted.python.log import startLogging from optparse import OptionParser from jmbase import get_password from jmclient import (Maker, jm_single, get_network, load_program_config, get_log, - SegwitWallet, sync_wallet, JMClientProtocolFactory, + get_wallet_cls, sync_wallet, JMClientProtocolFactory, start_reactor, calc_cj_fee, WalletError) jlog = get_log() @@ -87,13 +87,13 @@ class YieldGeneratorBasic(YieldGenerator): # print mix_balance max_mix = max(mix_balance, key=mix_balance.get) f = '0' - if self.ordertype == 'swreloffer': + if self.ordertype in ('reloffer', 'swreloffer'): f = self.cjfee_r #minimum size bumped if necessary such that you always profit #least 50% of the miner fee self.minsize = max(int(1.5 * self.txfee / float(self.cjfee_r)), self.minsize) - elif self.ordertype == 'swabsoffer': + elif self.ordertype in ('absoffer', 'swabsoffer'): f = str(self.txfee + self.cjfee_a) order = {'oid': 0, 'ordertype': self.ordertype, @@ -215,13 +215,13 @@ def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffe wallet_name = args[0] ordertype = options.ordertype txfee = options.txfee - if ordertype == 'swreloffer': + if ordertype in ('reloffer', 'swreloffer'): if options.cjfee != '': cjfee_r = options.cjfee # minimum size is such that you always net profit at least 20% #of the miner fee minsize = max(int(1.2 * txfee / float(cjfee_r)), options.minsize) - elif ordertype == 'swabsoffer': + elif ordertype in ('absoffer', 'swabsoffer'): if options.cjfee != '': cjfee_a = int(options.cjfee) minsize = options.minsize @@ -233,15 +233,15 @@ def ygmain(ygclass, txfee=1000, cjfee_a=200, cjfee_r=0.002, ordertype='swreloffe load_program_config() if not os.path.exists(os.path.join('wallets', wallet_name)): - wallet = SegwitWallet(wallet_name, None, max_mix_depth=MAX_MIX_DEPTH, - gaplimit=options.gaplimit) + wallet = get_wallet_cls()(wallet_name, None, max_mix_depth=MAX_MIX_DEPTH, + gaplimit=options.gaplimit) else: while True: try: pwd = get_password("Enter wallet decryption passphrase: ") - wallet = SegwitWallet(wallet_name, pwd, - max_mix_depth=MAX_MIX_DEPTH, - gaplimit=options.gaplimit) + wallet = get_wallet_cls()(wallet_name, pwd, + max_mix_depth=MAX_MIX_DEPTH, + gaplimit=options.gaplimit) except WalletError: print("Wrong password, try again.") continue From d2196ad68193966eae58602a39b754655bdbc9cd Mon Sep 17 00:00:00 2001 From: undeath Date: Wed, 25 Oct 2017 17:36:14 +0200 Subject: [PATCH 2/3] fix maker crash when verifying transaction with p2sh output in non-sw mode --- jmclient/jmclient/maker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jmclient/jmclient/maker.py b/jmclient/jmclient/maker.py index 38543d0..9ef1436 100644 --- a/jmclient/jmclient/maker.py +++ b/jmclient/jmclient/maker.py @@ -128,7 +128,9 @@ class Maker(object): utxos = offerinfo["utxos"] cjaddr = offerinfo["cjaddr"] + cjaddr_script = btc.address_to_script(cjaddr) changeaddr = offerinfo["changeaddr"] + changeaddr_script = btc.address_to_script(changeaddr) amount = offerinfo["amount"] cjfee = offerinfo["offer"]["cjfee"] txfee = offerinfo["offer"]["txfee"] @@ -146,13 +148,11 @@ class Maker(object): times_seen_cj_addr = 0 times_seen_change_addr = 0 for outs in txd['outs']: - #FIXME: the type of address should be detected from the script (p2pkh/p2sh) - addr = self.wallet.script_to_address(outs['script']) - if addr == cjaddr: + if outs['script'] == cjaddr_script: times_seen_cj_addr += 1 if outs['value'] != amount: return (False, 'Wrong cj_amount. I expect ' + str(amount)) - if addr == changeaddr: + if outs['script'] == changeaddr_script: times_seen_change_addr += 1 if outs['value'] != expected_change_value: return (False, 'wrong change, i expect ' + str( From 605eebe7453b14de9c0e710c9a3d03da753c47d8 Mon Sep 17 00:00:00 2001 From: undeath Date: Wed, 25 Oct 2017 17:37:25 +0200 Subject: [PATCH 3/3] add test cases for maker::verify_unsigned_tx --- jmclient/test/test_maker.py | 183 ++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 jmclient/test/test_maker.py diff --git a/jmclient/test/test_maker.py b/jmclient/test/test_maker.py new file mode 100644 index 0000000..6cdfbf7 --- /dev/null +++ b/jmclient/test/test_maker.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python + +from __future__ import print_function + +from jmclient import AbstractWallet, Maker, btc, get_p2sh_vbyte, get_p2pk_vbyte, \ + load_program_config, jm_single +import jmclient +from commontest import DummyBlockchainInterface + +import struct +import binascii +from itertools import chain +import pytest + + +class MockWallet(AbstractWallet): + pass + + +class OfflineMaker(Maker): + def try_to_create_my_orders(self): + self.sync_wait_loop.stop() + + +def construct_tx_offerlist(cjaddr, changeaddr, maker_utxos, maker_utxos_value, + cj_value, ordertype): + offer = { + 'cjfee': '0', + 'maxsize': cj_value*3, + 'minsize': 7500000, + 'oid': 0, + 'ordertype': ordertype, + 'txfee': 0 + } + + utxos = { utxo['outpoint']['hash'] + ':' + str(utxo['outpoint']['index']): + {'utxo': utxo, 'value': maker_utxos_value} for utxo in maker_utxos } + + offerlist = { + 'utxos': utxos, + 'cjaddr': cjaddr, + 'changeaddr': changeaddr, + 'amount': cj_value, + 'offer': offer + } + + return offerlist + + +def create_tx_inputs(count=1): + inp = [] + for i in xrange(count): + inp.append({'outpoint': {'hash': '0'*64, 'index': i}, + 'script': '', + 'sequence': 4294967295}) + return inp + + +def create_tx_outputs(*scripts_amount): + outp = [] + for script, amount in scripts_amount: + outp.append({'script': script, 'value': amount}) + return outp + + +def address_p2pkh_generator(): + return get_address_generator(b'\x76\xa9\x14', b'\x88\xac', get_p2pk_vbyte()) + + +def address_p2sh_generator(): + return get_address_generator(b'\xa9\x14', b'\x87', get_p2sh_vbyte()) + + +def get_address_generator(script_pre, script_post, vbyte): + counter = 0 + while True: + script = script_pre + struct.pack('=LQQ', 0, 0, counter) + script_post + addr = btc.script_to_address(script, vbyte) + yield addr, binascii.hexlify(script) + counter += 1 + + +def create_tx_and_offerlist(cj_addr, changeaddr, other_output_scripts, + cj_script=None, cj_change_script=None, offertype='swreloffer'): + assert len(other_output_scripts) % 2 == 0, "bug in test" + other_participant_count = len(other_output_scripts) // 2 + + cj_value = 100000000 + maker_total_value = cj_value*3 + + if cj_script is None: + cj_script = btc.address_to_script(cj_addr) + if cj_change_script is None: + changeaddr = btc.address_to_script(cj_change_addr) + + inputs = create_tx_inputs(3) + outputs = create_tx_outputs( + (cj_script, cj_value), + (cj_change_script, maker_total_value - cj_value), # cjfee=0, txfee=0 + *((script, cj_value + (i%2)*(50000000+i)) \ + for i, script in enumerate(other_output_scripts)) + ) + + maker_utxos = [inputs[0]] + + tx = btc.deserialize(btc.mktx(inputs, outputs)) + offerlist = construct_tx_offerlist(cj_addr, changeaddr, maker_utxos, + maker_total_value, cj_value, offertype) + + return tx, offerlist + + +def test_verify_unsigned_tx_sw_valid(setup_env_nodeps): + jm_single().config.set("POLICY", "segwit", "true") + + p2sh_gen = address_p2sh_generator() + p2pkh_gen = address_p2pkh_generator() + + wallet = MockWallet() + maker = OfflineMaker(wallet) + + cj_addr, cj_script = next(p2sh_gen) + changeaddr, cj_change_script = next(p2sh_gen) + + # test standard cj + tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, + [next(p2sh_gen)[1] for s in xrange(4)], cj_script, cj_change_script) + + assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "standard sw cj" + + # test cj with mixed outputs + tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, + list(chain((next(p2sh_gen)[1] for s in xrange(3)), + (next(p2pkh_gen)[1] for s in xrange(1)))), + cj_script, cj_change_script) + + assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "sw cj with p2pkh output" + + # test cj with only p2pkh outputs + tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, + [next(p2pkh_gen)[1] for s in xrange(4)], cj_script, cj_change_script) + + assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "sw cj with only p2pkh outputs" + + +def test_verify_unsigned_tx_nonsw_valid(setup_env_nodeps): + jm_single().config.set("POLICY", "segwit", "false") + + p2sh_gen = address_p2sh_generator() + p2pkh_gen = address_p2pkh_generator() + + wallet = MockWallet() + maker = OfflineMaker(wallet) + + cj_addr, cj_script = next(p2pkh_gen) + changeaddr, cj_change_script = next(p2pkh_gen) + + # test standard cj + tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, + [next(p2pkh_gen)[1] for s in xrange(4)], cj_script, cj_change_script, 'reloffer') + + assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "standard nonsw cj" + + # test cj with mixed outputs + tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, + list(chain((next(p2sh_gen)[1] for s in xrange(1)), + (next(p2pkh_gen)[1] for s in xrange(3)))), + cj_script, cj_change_script, 'reloffer') + + assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "nonsw cj with p2sh output" + + # test cj with only p2sh outputs + tx, offerlist = create_tx_and_offerlist(cj_addr, changeaddr, + [next(p2sh_gen)[1] for s in xrange(4)], cj_script, cj_change_script, 'reloffer') + + assert maker.verify_unsigned_tx(tx, offerlist) == (True, None), "nonsw cj with only p2sh outputs" + + +@pytest.fixture +def setup_env_nodeps(monkeypatch): + monkeypatch.setattr(jmclient.configure, 'get_blockchain_interface_instance', + lambda x: DummyBlockchainInterface()) + load_program_config()