Browse Source

Merge #205: support for mixed addresses coinjoins

04aeb38 implement coinjoin with differing address types (undeath)
36491c4 add test case for mixed addresses coinjoin (undeath)
master
AdamISZ 7 years ago
parent
commit
5dbf05e74f
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 66
      jmclient/jmclient/cryptoengine.py
  2. 11
      jmclient/jmclient/maker.py
  3. 21
      jmclient/jmclient/taker.py
  4. 20
      jmclient/jmclient/wallet.py
  5. 11
      jmclient/test/commontest.py
  6. 48
      jmclient/test/test_coinjoin.py
  7. 11
      jmclient/test/test_taker.py

66
jmclient/jmclient/cryptoengine.py

@ -54,6 +54,21 @@ def pubkey_to_p2wpkh_script(pubkey):
return _pubkey_to_script(pubkey, P2WPKH_PRE)
def detect_script_type(script):
if script.startswith(P2PKH_PRE) and script.endswith(P2PKH_POST) and\
len(script) == 0x14 + len(P2PKH_PRE) + len(P2PKH_POST):
return TYPE_P2PKH
elif (script.startswith(P2SH_P2WPKH_PRE) and
script.endswith(P2SH_P2WPKH_POST) and
len(script) == 0x14 + len(P2SH_P2WPKH_PRE) + len(P2SH_P2WPKH_POST)):
return TYPE_P2SH_P2WPKH
elif script.startswith(P2WPKH_PRE) and\
len(script) == 0x14 + len(P2WPKH_PRE):
return TYPE_P2WPKH
raise EngineError("Unknown script type for script '{}'"
.format(hexlify(script)))
class classproperty(object):
"""
from https://stackoverflow.com/a/5192374
@ -192,6 +207,19 @@ class BTCEngine(object):
script = cls.pubkey_to_script(pubkey)
return btc.script_to_address(script, cls.VBYTE)
@classmethod
def pubkey_has_address(cls, pubkey, addr):
ascript = cls.address_to_script(addr)
return cls.pubkey_has_script(pubkey, ascript)
@classmethod
def pubkey_has_script(cls, pubkey, script):
stype = detect_script_type(script)
assert stype in ENGINES
engine = ENGINES[stype]
pscript = engine.pubkey_to_script(pubkey)
return script == pscript
@classmethod
def sign_transaction(cls, tx, index, privkey, amount):
raise NotImplementedError()
@ -272,3 +300,41 @@ class BTC_P2SH_P2WPKH(BTCEngine):
tx['ins'][index]['txinwitness'] = [sig, pubkey]
return tx
class BTC_P2WPKH(BTCEngine):
@classproperty
def VBYTE(cls):
return btc.BTC_P2SH_VBYTE[get_network()]
@classmethod
def pubkey_to_script(cls, pubkey):
return pubkey_to_p2wpkh_script(pubkey)
@classmethod
def sign_transaction(cls, tx, index, privkey, amount,
hashcode=btc.SIGHASH_ALL, **kwargs):
assert amount is not None
raise NotImplementedError("The following code is completely untested")
pubkey = cls.privkey_to_pubkey(privkey)
script = cls.pubkey_to_script(pubkey)
signing_tx = btc.segwit_signature_form(tx, index, script, amount,
hashcode=hashcode,
decoder_func=lambda x: x)
# FIXME: encoding mess
sig = unhexlify(btc.ecdsa_tx_sign(signing_tx, hexlify(privkey),
hashcode=hashcode, **kwargs))
tx['ins'][index]['script'] = script
tx['ins'][index]['txinwitness'] = [sig, pubkey]
return tx
ENGINES = {
TYPE_P2PKH: BTC_P2PKH,
TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH,
TYPE_P2WPKH: BTC_P2WPKH
}

11
jmclient/jmclient/maker.py

@ -16,6 +16,8 @@ from jmbase.support import get_log
from jmclient.support import (calc_cj_fee)
from jmclient.podle import verify_podle, PoDLE, PoDLEError
from twisted.internet import task
from .cryptoengine import EngineError
jlog = get_log()
class Maker(object):
@ -82,10 +84,11 @@ class Maker(object):
reason = "commitment utxo too small: " + str(res[0]['value'])
return reject(reason)
# FIXME: This only works if taker's commitment address is of same type
# as our wallet.
if res[0]['address'] != \
self.wallet.pubkey_to_addr(unhexlify(cr_dict['P'])):
try:
if not self.wallet.pubkey_has_script(
unhexlify(cr_dict['P']), unhexlify(res[0]['script'])):
raise EngineError()
except EngineError:
reason = "Invalid podle pubkey: " + str(cr_dict['P'])
return reject(reason)

21
jmclient/jmclient/taker.py

@ -17,6 +17,9 @@ from jmclient.support import (calc_cj_fee, weighted_order_choose, choose_orders,
from jmclient.wallet import estimate_tx_fee
from jmclient.podle import generate_podle, get_podle_commitments, PoDLE
from .output import generate_podle_error_string
from .cryptoengine import EngineError
jlog = get_log()
@ -377,14 +380,18 @@ class Taker(object):
#Extract the address fields from the utxos
#Construct the Bitcoin address for the auth_pub field
#Ensure that at least one address from utxos corresponds.
input_addresses = [d['address'] for d in utxo_data]
# FIXME: This only works if taker's commitment address is of same type
# as our wallet.
auth_address = self.wallet.pubkey_to_addr(unhexlify(auth_pub))
if not auth_address in input_addresses:
auth_pub_bin = unhexlify(auth_pub)
for inp in utxo_data:
try:
if self.wallet.pubkey_has_script(
auth_pub_bin, unhexlify(inp['script'])):
break
except EngineError:
pass
else:
jlog.warn("ERROR maker's (" + nick + ")"
" authorising pubkey is not included "
"in the transaction: " + str(auth_address))
" authorising pubkey is not included "
"in the transaction!")
#this will not be added to the transaction, so we will have
#to recheck if we have enough
continue

20
jmclient/jmclient/wallet.py

@ -20,8 +20,7 @@ from numbers import Integral
from .configure import jm_single
from .support import select_gradual, select_greedy, select_greediest, \
select
from .cryptoengine import BTC_P2PKH, BTC_P2SH_P2WPKH, TYPE_P2PKH, \
TYPE_P2SH_P2WPKH
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, ENGINES
from .support import get_random_bytes
from . import mn_encode, mn_decode, btc
@ -204,10 +203,7 @@ class BaseWallet(object):
'greediest': select_greediest
}
_ENGINES = {
TYPE_P2PKH: BTC_P2PKH,
TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH
}
_ENGINES = ENGINES
_ENGINE = None
@ -384,6 +380,14 @@ class BaseWallet(object):
engine = self._get_priv_from_path(path)[1]
return engine.script_to_address(script)
@classmethod
def pubkey_has_address(cls, pubkey, addr):
return cls._ENGINE.pubkey_has_address(pubkey, addr)
@classmethod
def pubkey_has_script(cls, pubkey, script):
return cls._ENGINE.pubkey_has_script(pubkey, script)
@deprecated
def get_key(self, mixdepth, internal, index):
raise NotImplementedError()
@ -1340,7 +1344,7 @@ class BIP32Wallet(BaseWallet):
class LegacyWallet(ImportWalletMixin, BIP32Wallet):
TYPE = TYPE_P2PKH
_ENGINE = BTC_P2PKH
_ENGINE = ENGINES[TYPE_P2PKH]
def _create_master_key(self):
return hexlify(self._entropy)
@ -1351,7 +1355,7 @@ class LegacyWallet(ImportWalletMixin, BIP32Wallet):
class BIP49Wallet(BIP32Wallet):
_BIP49_PURPOSE = 2**31 + 49
_ENGINE = BTC_P2SH_P2WPKH
_ENGINE = ENGINES[TYPE_P2SH_P2WPKH]
def _get_bip32_base_path(self):
return self._key_ident, self._BIP49_PURPOSE,\

11
jmclient/test/commontest.py

@ -91,13 +91,16 @@ class DummyBlockchainInterface(BlockchainInterface):
'confirms': wallet_outs[to][1]})
return results
if txouts[0] in known_outs:
addr = btc.pubkey_to_p2sh_p2wpkh_address(
known_outs[txouts[0]], get_p2sh_vbyte())
return [{'value': 200000000,
'address': btc.pubkey_to_p2sh_p2wpkh_address(
known_outs[txouts[0]], get_p2sh_vbyte()),
'confirms': 20}]
'address': addr,
'script': btc.address_to_script(addr),
'confirms': 20}]
for t in txouts:
result_dict = {'value': 10000000000,
'address': "mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ"}
'address': "mrcNu71ztWjAQA6ww9kHiW3zBWSQidHXTQ",
'script': '76a91479b000887626b294a914501a4cd226b58b23598388ac'}
if includeconf:
result_dict['confirms'] = 20
result.append(result_dict)

48
jmclient/test/test_coinjoin.py

@ -256,6 +256,54 @@ def test_coinjoin_mixdepth_wrap_maker(monkeypatch, tmpdir, setup_cj):
assert balances[4] == 4 * 10**8 - cj_amount + cj_fee
@pytest.mark.parametrize('wallet_cls,wallet_cls_sec', (
(SegwitLegacyWallet, LegacyWallet),
(LegacyWallet, SegwitLegacyWallet)
))
def test_coinjoin_mixed_maker_addresses(monkeypatch, tmpdir, setup_cj,
wallet_cls, wallet_cls_sec):
set_commitment_file(str(tmpdir.join('commitments.json')))
MAKER_NUM = 2
wallets = make_wallets_to_list(make_wallets(
MAKER_NUM + 1,
wallet_structures=[[1, 0, 0, 0, 0]] * MAKER_NUM + [[3, 0, 0, 0, 0]],
mean_amt=1, wallet_cls=wallet_cls))
wallets_sec = make_wallets_to_list(make_wallets(
MAKER_NUM,
wallet_structures=[[1, 0, 0, 0, 0]] * MAKER_NUM,
mean_amt=1, wallet_cls=wallet_cls_sec))
for i in range(MAKER_NUM):
wif = wallets_sec[i].get_wif(0, False, 0)
wallets[i].import_private_key(0, wif, key_type=wallets_sec[i].TYPE)
jm_single().bc_interface.tickchain()
jm_single().bc_interface.tickchain()
sync_wallets(wallets)
makers = [YieldGeneratorBasic(
wallets[i],
[0, 2000, 0, 'swabsoffer', 10**7]) for i in range(MAKER_NUM)]
orderbook = create_orderbook(makers)
cj_amount = int(1.1 * 10**8)
# mixdepth, amount, counterparties, dest_addr, waittime
schedule = [(0, cj_amount, MAKER_NUM, 'INTERNAL', 0)]
taker = create_taker(wallets[-1], schedule, monkeypatch)
active_orders, maker_data = init_coinjoin(taker, makers,
orderbook, cj_amount)
txdata = taker.receive_utxos(maker_data)
assert txdata[0], "taker.receive_utxos error"
taker_final_result = do_tx_signing(taker, makers, active_orders, txdata)
assert taker_final_result is not False
assert taker.on_finished_callback.status is not False
@pytest.fixture(scope='module')
def setup_cj():
load_program_config()

11
jmclient/test/test_taker.py

@ -197,9 +197,10 @@ def test_auth_pub_not_found(createcmtdata):
"498faa8b22534f3b443c6b0ce202f31e12f21668b4f0c7a005146808f250d4c3:0",
"3f3ea820d706e08ad8dc1d2c392c98facb1b067ae4c671043ae9461057bd2a3c:1"]
fake_query_results = [{'value': 200000000,
'address': "blah",
'utxo': utxos[i],
'confirms': 20} for i in range(3)]
'address': "mrKTGvFfYUEqk52qPKUroumZJcpjHLQ6pn",
'script': '76a914767c956efe6092a775fea39a06d1cac9aae956d788ac',
'utxo': utxos[i],
'confirms': 20} for i in range(3)]
jm_single().bc_interface.insert_fake_query_results(fake_query_results)
res = taker.receive_utxos(maker_response)
assert not res[0]
@ -418,9 +419,13 @@ def test_on_sig(createcmtdata, dummyaddr, schedule):
#my inputs are the first 2 utxos
taker.input_utxos = {utxos[0]:
{'address': bitcoin.privkey_to_address(privs[0], False, magicbyte=0x6f),
'script': bitcoin.mk_pubkey_script(
bitcoin.privkey_to_address(privs[0], False, magicbyte=0x6f)),
'value': 200000000},
utxos[1]:
{'address': bitcoin.privkey_to_address(privs[1], False, magicbyte=0x6f),
'script': bitcoin.mk_pubkey_script(
bitcoin.privkey_to_address(privs[1], False, magicbyte=0x6f)),
'value': 200000000}}
taker.utxos = {None: utxos[:2], "cp1": [utxos[2]], "cp2": [utxos[3]], "cp3":[utxos[4]]}
for i in range(2):

Loading…
Cancel
Save