Skip to content

Commit

Permalink
Adds psbt creation and signing support in JM wallet.
Browse files Browse the repository at this point in the history
This is a first draft and lacks detail. Basically:
Subclassed btcointx.CKeyBase to create a privkey type
that uses our signing code.
Sublcassed bitcointx.KeyStore to create a very simple
KeyStore consisting of lists of the above keys.
These are then used to allow signing of inputs in
transactions owned by our wallet.
Created a PSBTWalletMixin to perform the above function,
and added it to our three wallet types (although only
the default "SegwitLegacy" is tested for now).
Wrote a small proof of concept in the direct_send()
function, which creates a psbt for the transaction, then
validates and signs it. This POC will be removed though,
for more useful code.
  • Loading branch information
AdamISZ committed Apr 21, 2020
1 parent 2a10e86 commit 8bf0346
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 2 deletions.
2 changes: 2 additions & 0 deletions jmbitcoin/jmbitcoin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

47 changes: 47 additions & 0 deletions jmbitcoin/jmbitcoin/secp256k1_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()')
9 changes: 9 additions & 0 deletions jmclient/jmclient/taker_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
110 changes: 108 additions & 2 deletions jmclient/jmclient/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit 8bf0346

Please sign in to comment.