diff --git a/jmbitcoin/jmbitcoin/__init__.py b/jmbitcoin/jmbitcoin/__init__.py index e22a3f7d9..c8d8be415 100644 --- a/jmbitcoin/jmbitcoin/__init__.py +++ b/jmbitcoin/jmbitcoin/__init__.py @@ -7,8 +7,10 @@ from bitcointx.core import (x, b2x, b2lx, lx, COutPoint, CTxOut, CTxIn, CTxInWitness, CTxWitness, CMutableTransaction, Hash160, coins_to_satoshi, satoshi_to_coins) +from bitcointx.core.key import KeyStore from bitcointx.core.script import (CScript, OP_0, SignatureHash, SIGHASH_ALL, SIGVERSION_WITNESS_V0, CScriptWitness) from bitcointx.wallet import (CBitcoinSecret, P2WPKHBitcoinAddress, CCoinAddress, P2SHCoinAddress) +from bitcointx.core.psbt import PartiallySignedTransaction diff --git a/jmbitcoin/jmbitcoin/secp256k1_main.py b/jmbitcoin/jmbitcoin/secp256k1_main.py index db9eaaf25..45b3db422 100644 --- a/jmbitcoin/jmbitcoin/secp256k1_main.py +++ b/jmbitcoin/jmbitcoin/secp256k1_main.py @@ -9,6 +9,7 @@ from bitcointx import base58 from bitcointx.core import Hash +from bitcointx.core.key import CKeyBase, CPubKey from bitcointx.signmessage import BitcoinMessage #Required only for PoDLE calculation: @@ -215,3 +216,49 @@ def ecdsa_raw_verify(msg, pub, sig, rawmsg=False): except Exception as e: return False return retval + +class JMCKey(bytes, CKeyBase): + """An encapsulated private key. + This subclasses specifically for JM's own signing code. + + Attributes: + + pub - The corresponding CPubKey for this private key + secret_bytes - Secret data, 32 bytes (needed because subclasses may have trailing data) + + is_compressed() - True if compressed + + """ + + def __init__(self, b): + CKeyBase.__init__(self, b, compressed=True) + + def is_compressed(self): + return True + + @property + def secret_bytes(self): + assert isinstance(self, bytes) + return self[:32] + + def sign(self, hash): + assert isinstance(hash, (bytes, bytearray)) + if len(hash) != 32: + raise ValueError('Hash must be exactly 32 bytes long') + # TODO: non default sighash flag. + return ecdsa_raw_sign(hash, self.secret_bytes + b"\x01", rawmsg=True) + + + def verify(self, hash, sig): + return self.pub.verify(hash, sig) + + def verify_nonstrict(self, hash, sig): + return self.pub.verify_nonstrict(hash, sig) + + @classmethod + def from_secret_bytes(cls, secret, compressed=True): + return cls(secret, compressed=compressed) + + @classmethod + def from_bytes(cls, data): + raise NotImplementedError('subclasses must override from_bytes()') diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index 95f33df20..eabda048a 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -77,6 +77,15 @@ def direct_send(wallet_service, amount, mixdepth, destaddr, answeryes=False, if amount != 0: log.info("Using a change value of: " + amount_to_str(changeval) + ".") tx = make_shuffled_tx(list(utxos.keys()), outs, 2, compute_tx_locktime()) + + newpsbt = wallet_service.create_psbt_from_tx(tx, wallet_service.utxos_to_txouts(utxos)) + print(newpsbt) + serialized_signed_psbt, err = wallet_service.sign_psbt(newpsbt.serialize()) + if err != None: + print("Could not sign psbt, error: ", err) + else: + print(bintohex(serialized_signed_psbt)) + inscripts = {} for i, txinp in enumerate(tx.vin): u = (txinp.prevout.hash[::-1], txinp.prevout.n) diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 1b6058c9e..4e6e49d28 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -960,12 +960,118 @@ def check_gap_indices(self, used_indices): return False return True + @staticmethod + def utxos_to_txouts(utxos): + """ converts utxos as returned by select_utxos to + CTxOut objects as used by bitcointx. + """ + return [btc.CMutableTxOut(v["value"], + v["script"]) for _, v in utxos.items()] + def close(self): self._storage.close() def __del__(self): self.close() +class DummyKeyStore(btc.KeyStore): + @classmethod + def from_iterable(cls, iterable, **kwargs): + kstore = cls(**kwargs) + for k in iterable: + kstore.add_key(k) + return kstore + + def add_key(self, k): + print("working with key: ", k) + print("It is: ", type(k)) + print("Is it a CKeyBase?: ", isinstance(k, btc.CKeyBase)) + if isinstance(k, btc.CKeyBase): + if k.pub.key_id in self._privkeys: + assert self._privkeys[k.pub.key_id] == k + else: + self._privkeys[k.pub.key_id] = k + else: + raise ValueError('object supplied to add_key is of unrecognized type') + +class PSBTWalletMixin(object): + """ + Mixin for BaseWallet to provide BIP174 + functions. + """ + def __init__(self, storage, **kwargs): + super(PSBTWalletMixin, self).__init__(storage, **kwargs) + + @staticmethod + def witness_utxos_to_psbt_utxos(utxos): + """ Given a dict of utxos as returned from select_utxos, + convert them to the format required to populate PSBT inputs, + names CTxOut. Note that the non-segwit case is different, there + you should provide an entire CMutableTransaction object instead. + """ + res = [] + for k, v in utxos: + res.append(btc.CMutableTxOut(v["value"], v["script"])) + return res + + def create_psbt_from_tx(self, tx, spent_outs=None): + """ Given a CMutableTransaction object, which should not currently + contain signatures, we create and return a new PSBT object of type + btc.PartiallySignedTransaction. + Optionally the information about the spent outputs that is stored + in PSBT_IN_NONWITNESS_UTXO, PSBT_IN_WITNESS_UTXO and PSBT_IN_REDEEM_SCRIPT + can also be provided, one item per input, in the tuple (spent_outs). + These objects should be either CMutableTransaction, CTxOut or None, + Note that redeem script information cannot be provided for inputs which + we don't own. + """ + new_psbt = btc.PartiallySignedTransaction(unsigned_tx=tx) + if spent_outs is None: + # user has not provided input script information; psbt + # will not yet be usable for signing. + return new_psbt + for i, txinput in enumerate(new_psbt.inputs): + if spent_outs[i] is None: + continue + if isinstance(spent_outs[i], (btc.CMutableTransaction, btc.CMutableTxOut)): + # note that we trust the caller to choose Tx vs TxOut as according + # to non-witness/witness: + txinput.utxo = spent_outs[i] + else: + assert False, "invalid spent output type passed into PSBT creator" + # we now insert redeemscripts where that is possible and necessary: + for i, txinput in enumerate(new_psbt.inputs): + if isinstance(txinput.utxo, btc.CMutableTxOut): + # witness; TODO: native case, possibly p2sh legacy case + path = self.script_to_path(txinput.utxo.scriptPubKey) + privkey, _ = self._get_priv_from_path(path) + txinput.redeem_script = btc.pubkey_to_p2wpkh_script(btc.privkey_to_pubkey(privkey)) + return new_psbt + + def sign_psbt(self, in_psbt): + """ Given a serialized PSBT in raw binary format, + iterate over the inputs and sign all that we can sign with this wallet. + Return: (psbt, msg) + msg: error message or None + psbt: signed psbt in binary serialzation, or None if error. + """ + try: + new_psbt = btc.PartiallySignedTransaction.from_binary(in_psbt) + except: + return None, "Unable to deserialize the PSBT object, invalid format." + privkeys = [] + for k, v in self._utxos._utxo.items(): + for k2, v2 in v.items(): + privkeys.append(self._get_priv_from_path(v2[0])) + jmckeys = list(btc.JMCKey(x[0][:-1]) for x in privkeys) + new_keystore = DummyKeyStore.from_iterable(jmckeys) + try: + signresult = new_psbt.sign(new_keystore) + except Exception as e: + return None, repr(e) + # TODO: use the information in the SignResult object + # to compare with expected (finalized or not, how many sigs added). + return new_psbt.serialize(), None class ImportWalletMixin(object): """ @@ -1553,10 +1659,10 @@ class BIP84Wallet(BIP32PurposedWallet): _PURPOSE = 2**31 + 84 _ENGINE = ENGINES[TYPE_P2WPKH] -class SegwitLegacyWallet(ImportWalletMixin, BIP39WalletMixin, BIP49Wallet): +class SegwitLegacyWallet(ImportWalletMixin, BIP39WalletMixin, PSBTWalletMixin, BIP49Wallet): TYPE = TYPE_P2SH_P2WPKH -class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, BIP84Wallet): +class SegwitWallet(ImportWalletMixin, BIP39WalletMixin, PSBTWalletMixin, BIP84Wallet): TYPE = TYPE_P2WPKH WALLET_IMPLEMENTATIONS = {