Skip to content

Commit

Permalink
Adds full payjoin workflow test
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamISZ committed Apr 25, 2020
1 parent 381423b commit 9cc0333
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 5 deletions.
5 changes: 3 additions & 2 deletions jmbitcoin/jmbitcoin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
from jmbitcoin.amount import *
from bitcointx import select_chain_params
from bitcointx.core import (x, b2x, b2lx, lx, COutPoint, CTxOut, CTxIn,
CTxInWitness, CTxWitness, CMutableTransaction,
Hash160, coins_to_satoshi, satoshi_to_coins)
CTxInWitness, CTxWitness, CTransaction,
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)
Expand Down
2 changes: 1 addition & 1 deletion jmclient/jmclient/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@ def create_psbt_from_tx(self, tx, spent_outs=None):
if spent_outs[i] is None:
# as above, will not be signable in this case
continue
if isinstance(spent_outs[i], (btc.CMutableTransaction, btc.CMutableTxOut)):
if isinstance(spent_outs[i], (btc.CTransaction, btc.CTxOut)):
# note that we trust the caller to choose Tx vs TxOut as according
# to non-witness/witness:
txinput.utxo = spent_outs[i]
Expand Down
163 changes: 161 additions & 2 deletions jmclient/test/test_psbt_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,167 @@ def test_create_psbt_and_sign(setup_psbt_wallet, unowned_utxo, wallet_cls):
# transaction extraction must fail for not-fully-signed psbts:
with pytest.raises(ValueError) as e:
extracted_tx = signed_psbt.extract_transaction()



@pytest.mark.parametrize('payment_amt, wallet_cls_sender, wallet_cls_receiver', [
(0.05, SegwitLegacyWallet, SegwitLegacyWallet),
(0.95, SegwitLegacyWallet, SegwitWallet),
(0.05, SegwitWallet, SegwitLegacyWallet),
(0.95, SegwitWallet, SegwitWallet),
])
def test_payjoin_workflow(setup_psbt_wallet, payment_amt, wallet_cls_sender,
wallet_cls_receiver):
""" Workflow step 1:
Create a payment from a wallet, and create a finalized PSBT.
This step is fairly trivial as the functionality is built-in to
PSBTWalletMixin.
Note that only Segwit* wallets are supported for PayJoin.
Workflow step 2:
Receiver creates a new partially signed PSBT with the same amount
and at least one more utxo.
Workflow step 3:
Given a partially signed PSBT created by a receiver, here the sender
completes (co-signs) the PSBT they are given. Note this code is a PSBT
functionality check, and does NOT include the detailed checks that
the sender should perform before agreeing to sign (see:
https://github.com/btcpayserver/btcpayserver-doc/blob/eaac676866a4d871eda5fd7752b91b88fdf849ff/Payjoin-spec.md#receiver-side
).
"""

wallet_r = make_wallets(1, [[3,0,0,0,0]], 1,
wallet_cls=wallet_cls_receiver)[0]["wallet"]
wallet_s = make_wallets(1, [[3,0,0,0,0]], 1,
wallet_cls=wallet_cls_sender)[0]["wallet"]
for w in [wallet_r, wallet_s]:
w.sync_wallet(fast=True)

# destination address for payment:
destaddr = str(bitcoin.CCoinAddress.from_scriptPubKey(
bitcoin.pubkey_to_p2wpkh_script(bitcoin.privkey_to_pubkey(b"\x01"*33))))

payment_amt = bitcoin.coins_to_satoshi(payment_amt)

# *** STEP 1 ***
# **************

# create a normal tx from the sender wallet:
payment_psbt = direct_send(wallet_s, payment_amt, 0, destaddr,
accept_callback=dummy_accept_callback,
info_callback=dummy_info_callback,
with_final_psbt=True)

# ensure that the payemnt amount is what was intended:
out_amts = [x.nValue for x in payment_psbt.unsigned_tx.vout]
# NOTE this would have to change for more than 2 outputs:
assert any([out_amts[i] == payment_amt for i in [0, 1]])

# ensure that we can actually broadcast the created tx:
# (note that 'extract_transaction' represents an implicit
# PSBT finality check).
extracted_tx = payment_psbt.extract_transaction().serialize()
# don't want to push the tx right now, because of test structure
# (in production code this isn't really needed, we will not
# produce invalid payment transactions).
res = jm_single().bc_interface.rpc('testmempoolaccept',
[[bintohex(extracted_tx)]])
assert res[0]["allowed"], "Payment transaction was rejected from mempool."

# *** STEP 2 ***
# **************

# This step will not be in Joinmarket code for the first cut,
# it will be done by the merchant, but included here for the data flow.
# receiver grabs a random utxo here (as per previous sentence, this is
# the merchant's responsibility, not ours, but see earlier code in
# jmclient.maker.P2EPMaker for possibe heuristics).
# for more generality we test with two receiver-utxos, not one.
all_receiver_utxos = wallet_r.get_all_utxos()
# TODO is there a less verbose way to get any 2 utxos from the dict?
receiver_utxos_keys = list(all_receiver_utxos.keys())[:2]
receiver_utxos = {k: v for k, v in all_receiver_utxos.items(
) if k in receiver_utxos_keys}

# receiver will do other checks as discussed above, including payment
# amount; as discussed above, this is out of the scope of this PSBT test.

# construct unsigned tx for payjoin-psbt:
payjoin_tx_inputs = [(x.prevout.hash[::-1],
x.prevout.n) for x in payment_psbt.unsigned_tx.vin]
payjoin_tx_inputs.extend(receiver_utxos.keys())
# find payment output and change output
pay_out = None
change_out = None
for o in payment_psbt.unsigned_tx.vout:
jm_out_fmt = {"value": o.nValue,
"address": str(bitcoin.CCoinAddress.from_scriptPubKey(
o.scriptPubKey))}
if o.nValue == payment_amt:
assert pay_out is None
pay_out = jm_out_fmt
else:
assert change_out is None
change_out = jm_out_fmt

# we now know there were two outputs and know which is payment.
# bump payment output with our input:
outs = [pay_out, change_out]
our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()])
pay_out["value"] += our_inputs_val
print("we bumped the payment output value by: ", our_inputs_val)
print("It is now: ", pay_out["value"])
unsigned_payjoin_tx = bitcoin.make_shuffled_tx(payjoin_tx_inputs, outs,
version=payment_psbt.unsigned_tx.nVersion,
locktime=payment_psbt.unsigned_tx.nLockTime)
print("we created this unsigned tx: ")
print(unsigned_payjoin_tx)
# to create the PSBT we need the spent_outs for each input,
# in the right order:
spent_outs = []
for i, inp in enumerate(unsigned_payjoin_tx.vin):
input_found = False
for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin):
if inp.prevout == inp2.prevout:
spent_outs.append(payment_psbt.inputs[j].utxo)
input_found = True
break
if input_found:
continue
# if we got here this input is ours, we must find
# it from our original utxo choice list:
for ru in receiver_utxos.keys():
if (inp.prevout.hash[::-1], inp.prevout.n) == ru:
spent_outs.append(
wallet_r.witness_utxos_to_psbt_utxos(
{ru: receiver_utxos[ru]})[0])
input_found = True
break
# there should be no other inputs:
assert input_found

r_payjoin_psbt = wallet_r.create_psbt_from_tx(unsigned_payjoin_tx,
spent_outs=spent_outs)
signresultandpsbt, err = wallet_r.sign_psbt(r_payjoin_psbt.serialize(),
with_sign_result=True)
assert not err, err
signresult, receiver_signed_psbt = signresultandpsbt
assert signresult.num_inputs_final == len(receiver_utxos)
assert not signresult.is_final

# *** STEP 3 ***
# **************

# take the half-signed PSBT, validate and co-sign:

signresultandpsbt, err = wallet_s.sign_psbt(
receiver_signed_psbt.serialize(), with_sign_result=True)
assert not err, err
signresult, sender_signed_psbt = signresultandpsbt
assert signresult.is_final
# broadcast the tx
extracted_tx = sender_signed_psbt.extract_transaction().serialize()
assert jm_single().bc_interface.pushtx(extracted_tx)

@pytest.fixture(scope="module")
def setup_psbt_wallet():
load_test_config()

0 comments on commit 9cc0333

Please sign in to comment.