From a0a0d28f4e4f69602656f2515907a34e72b1c30c Mon Sep 17 00:00:00 2001 From: chris-belcher Date: Mon, 27 Apr 2020 15:37:46 +0100 Subject: [PATCH] Add support for spending timelocked UTXOs The cryptoengine class BTC_Timelocked_P2WSH now implements sign_transaction() which can be used to spend timelocked UTXOs. FidelityBondMixin.is_timelocked_path() is now used outside the class so its leading underscore has been removed. --- jmclient/jmclient/cryptoengine.py | 24 +++++++++++++++++------- jmclient/jmclient/taker_utils.py | 18 +++++++++++++++++- jmclient/jmclient/wallet.py | 8 ++++---- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/jmclient/jmclient/cryptoengine.py b/jmclient/jmclient/cryptoengine.py index 800c301..1e1ff22 100644 --- a/jmclient/jmclient/cryptoengine.py +++ b/jmclient/jmclient/cryptoengine.py @@ -332,14 +332,19 @@ class BTC_Timelocked_P2WSH(BTCEngine): return btc.bin_to_b58check(priv, cls.WIF_PREFIX) @classmethod - def sign_transaction(cls, tx, index, privkey, amount, + def sign_transaction(cls, tx, index, privkey_locktime, amount, hashcode=btc.SIGHASH_ALL, **kwargs): - raise Exception("not implemented yet") + assert amount is not None - @classmethod - def sign_transaction(cls, tx, index, privkey, amount, - hashcode=btc.SIGHASH_ALL, **kwargs): - raise RuntimeError("Cannot spend from watch-only wallets") + privkey, locktime = privkey_locktime + privkey = hexlify(privkey).decode() + pubkey = btc.privkey_to_pubkey(privkey) + pubkey = unhexlify(pubkey) + redeem_script = cls.pubkey_to_script_code((pubkey, locktime)) + tx = btc.serialize(tx) + sig = btc.get_p2sh_signature(tx, index, redeem_script, privkey, + amount) + return btc.apply_freeze_signature(tx, index, redeem_script, sig) class BTC_Watchonly_Timelocked_P2WSH(BTC_Timelocked_P2WSH): @@ -368,7 +373,7 @@ class BTC_Watchonly_Timelocked_P2WSH(BTC_Timelocked_P2WSH): @classmethod def sign_transaction(cls, tx, index, privkey, amount, hashcode=btc.SIGHASH_ALL, **kwargs): - raise Exception("not implemented yet") + raise RuntimeError("Cannot spend from watch-only wallets") class BTC_Watchonly_P2SH_P2WPKH(BTC_P2SH_P2WPKH): @@ -392,6 +397,11 @@ class BTC_Watchonly_P2SH_P2WPKH(BTC_P2SH_P2WPKH): return super(BTC_Watchonly_P2SH_P2WPKH, cls).derive_bip32_pub_export( master_key, BTC_Watchonly_Timelocked_P2WSH.get_watchonly_path(path)) + @classmethod + def sign_transaction(cls, tx, index, privkey, amount, + hashcode=btc.SIGHASH_ALL, **kwargs): + raise RuntimeError("Cannot spend from watch-only wallets") + ENGINES = { TYPE_P2PKH: BTC_P2PKH, TYPE_P2SH_P2WPKH: BTC_P2SH_P2WPKH, diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index c5f958a..8160caa 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -117,12 +117,28 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, change_addr = wallet_service.get_internal_addr(mixdepth) outs.append({"value": changeval, "address": change_addr}) + #compute transaction locktime, has special case for spending timelocked coins + tx_locktime = compute_tx_locktime() + if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \ + isinstance(wallet_service.wallet, FidelityBondMixin): + for outpoint, utxo in utxos.items(): + path = wallet_service.script_to_path( + wallet_service.addr_to_script(utxo["address"])) + if not FidelityBondMixin.is_timelocked_path(path): + continue + path_locktime = path[-1] + tx_locktime = max(tx_locktime, path_locktime+1) + #compute_tx_locktime() gives a locktime in terms of block height + #timelocked addresses use unix time instead + #OP_CHECKLOCKTIMEVERIFY can only compare like with like, so we + #must use unix time as the transaction locktime + #Now ready to construct transaction log.info("Using a fee of : " + amount_to_str(fee_est) + ".") if amount != 0: log.info("Using a change value of: " + amount_to_str(changeval) + ".") txsigned = sign_tx(wallet_service, make_shuffled_tx( - list(utxos.keys()), outs, False, 2, compute_tx_locktime()), utxos) + list(utxos.keys()), outs, False, 2, tx_locktime), utxos) log.info("Got signed transaction:\n") log.info(pformat(txsigned)) tx = serialize(txsigned) diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 0136484..37a62fc 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -1694,7 +1694,7 @@ class FidelityBondMixin(object): return timenumber @classmethod - def _is_timelocked_path(cls, path): + def is_timelocked_path(cls, path): return len(path) > 4 and path[4] == cls.BIP32_TIMELOCK_ID def get_xpub_from_fidelity_bond_master_pub_key(cls, mpk): @@ -1737,7 +1737,7 @@ class FidelityBondMixin(object): return (cls.BIP32_EXT_ID, cls.BIP32_INT_ID, cls.BIP32_TIMELOCK_ID, cls.BIP32_BURN_ID) def _get_priv_from_path(self, path): - if self._is_timelocked_path(path): + if self.is_timelocked_path(path): key_path = path[:-1] locktime = path[-1] engine = self._TIMELOCK_ENGINE @@ -1766,7 +1766,7 @@ class FidelityBondMixin(object): refering to the pubkey plus the timelock value which together are needed to create the address """ def get_path_repr(self, path): - if self._is_timelocked_path(path) and len(path) == 7: + if self.is_timelocked_path(path) and len(path) == 7: return super(FidelityBondMixin, self).get_path_repr(path[:-1]) +\ ":" + str(path[-1]) else: @@ -1785,7 +1785,7 @@ class FidelityBondMixin(object): )) def get_details(self, path): - if self._is_timelocked_path(path): + if self.is_timelocked_path(path): return self._get_mixdepth_from_path(path), path[-3], path[-2] else: return super(FidelityBondMixin, self).get_details(path)