Browse Source

Adds test case for fee bumping a tx using PSBT

Additionally, modify docstring of estimate_tx_size
master
Adam Gibson 4 years ago
parent
commit
635f3f10bc
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 10
      jmbitcoin/jmbitcoin/secp256k1_transaction.py
  2. 94
      jmclient/test/test_tx_creation.py

10
jmbitcoin/jmbitcoin/secp256k1_transaction.py

@ -110,10 +110,12 @@ def estimate_tx_size(ins, outs, txtype='p2pkh', outtype=None):
'''Estimate transaction size. '''Estimate transaction size.
The txtype field as detailed below is used to distinguish The txtype field as detailed below is used to distinguish
the type, but there is at least one source of meaningful roughness: the type, but there is at least one source of meaningful roughness:
we assume the output types are the same as the input (to be fair, we assume that the scriptPubKey type of all the outputs are the same as
outputs only contribute a little to the overall total). This combined the input, unless `outtype` is specified, in which case *one* of
with a few bytes variation in signature sizes means we will expect, the outputs is assumed to be that other type, with all of the other
say, 10% inaccuracy here. outputs being of the same type as before.
This, combined with a few bytes variation in signature sizes means
we will sometimes see small inaccuracies in this estimate.
Assuming p2pkh: Assuming p2pkh:
out: 8+1+3+20+2=34, in: 32+4+1+1+~72+1+33+4=148, out: 8+1+3+20+2=34, in: 32+4+1+1+~72+1+33+4=148,

94
jmclient/test/test_tx_creation.py

@ -11,7 +11,7 @@ from commontest import make_wallets, make_sign_and_push, ensure_bip65_activated
import jmbitcoin as bitcoin import jmbitcoin as bitcoin
import pytest import pytest
from jmbase import get_log from jmbase import get_log
from jmclient import load_test_config, jm_single from jmclient import load_test_config, jm_single, direct_send, estimate_tx_fee, compute_tx_locktime
log = get_log() log = get_log()
#just a random selection of pubkeys for receiving multisigs; #just a random selection of pubkeys for receiving multisigs;
@ -86,6 +86,7 @@ def test_absurd_fees(setup_tx_creation):
ins_full = wallet_service.select_utxos(0, amount) ins_full = wallet_service.select_utxos(0, amount)
with pytest.raises(ValueError) as e_info: with pytest.raises(ValueError) as e_info:
txid = make_sign_and_push(ins_full, wallet_service, amount, estimate_fee=True) txid = make_sign_and_push(ins_full, wallet_service, amount, estimate_fee=True)
jm_single().bc_interface.absurd_fees = False
def test_create_sighash_txs(setup_tx_creation): def test_create_sighash_txs(setup_tx_creation):
#non-standard hash codes: #non-standard hash codes:
@ -137,6 +138,97 @@ def test_spend_p2wpkh(setup_tx_creation):
txid = jm_single().bc_interface.pushtx(tx.serialize()) txid = jm_single().bc_interface.pushtx(tx.serialize())
assert txid assert txid
def test_spend_then_rbf(setup_tx_creation):
""" Test plan: first, create a normal spend with
rbf enabled in direct_send, then broadcast but
do not mine a block. Then create a re-spend of
the same utxos with a higher fee and check
that broadcast succeeds.
"""
# First phase: broadcast with RBF enabled.
#
# set a baseline feerate:
old_feerate = jm_single().config.get("POLICY", "tx_fees")
jm_single().config.set("POLICY", "tx_fees", "20000")
# set up a single wallet with some coins:
wallet_service = make_wallets(1, [[2, 0, 0, 0, 1]], 3)[0]['wallet']
wallet_service.sync_wallet(fast=True)
# ensure selection of two utxos, doesn't really matter
# but a more general case than only one:
amount = 350000000
# destination doesn't matter; this is easiest:
destn = wallet_service.get_internal_addr(1)
# While `direct_send` usually encapsulates utxo selection
# for user, here we need to know what was chosen, hence
# we return the transaction object, not directly broadcast.
tx1 = direct_send(wallet_service, amount, 0,
destn, answeryes=True,
return_transaction=True,
optin_rbf=True)
assert tx1
# record the utxos for reuse:
assert isinstance(tx1, bitcoin.CTransaction)
utxos_objs = (x.prevout for x in tx1.vin)
utxos = [(x.hash[::-1], x.n) for x in utxos_objs]
# in order to sign on those utxos, we need their script and value.
scrs = {}
vals = {}
for u, details in wallet_service.get_utxos_by_mixdepth()[0].items():
if u in utxos:
scrs[u] = details["script"]
vals[u] = details["value"]
assert len(scrs.keys()) == 2
assert len(vals.keys()) == 2
# This will go to mempool but not get mined because
# we don't call `tick_forward_chain`.
push_succeed = jm_single().bc_interface.pushtx(tx1.serialize())
if push_succeed:
# mimics real operations with transaction monitor:
wallet_service.process_new_tx(tx1)
else:
assert False
# Second phase: bump fee.
#
# we set a larger fee rate.
jm_single().config.set("POLICY", "tx_fees", "30000")
# just a different destination to avoid confusion:
destn2 = wallet_service.get_internal_addr(2)
# We reuse *both* utxos so total fees are comparable
# (modulo tiny 1 byte differences in signatures).
# Ordinary wallet operations would remove the first-spent utxos,
# so for now we build a PSBT using the code from #921 to select
# the same utxos (it could be done other ways).
# Then we broadcast the PSBT and check it is allowed
# before constructing the outputs, we need a good fee estimate,
# using the bumped feerate:
fee = estimate_tx_fee(2, 2, wallet_service.get_txtype())
# reset the feerate:
total_input_val = sum(vals.values())
jm_single().config.set("POLICY", "tx_fees", old_feerate)
outs = [{"address": destn2, "value": 1000000},
{"address": wallet_service.get_internal_addr(0),
"value": total_input_val - 1000000 - fee}]
tx2 = bitcoin.mktx(utxos, outs, version=2,
locktime=compute_tx_locktime())
spent_outs = []
for u in utxos:
spent_outs.append(bitcoin.CTxOut(nValue=vals[u],
scriptPubKey=scrs[u]))
psbt_unsigned = wallet_service.create_psbt_from_tx(tx2,
spent_outs=spent_outs)
signresultandpsbt, err = wallet_service.sign_psbt(
psbt_unsigned.serialize(), with_sign_result=True)
assert not err
signresult, psbt_signed = signresultandpsbt
tx2_signed = psbt_signed.extract_transaction()
# the following assertion is sufficient, because
# tx broadcast would fail if the replacement were
# not allowed by Core:
assert jm_single().bc_interface.pushtx(tx2_signed.serialize())
def test_spend_freeze_script(setup_tx_creation): def test_spend_freeze_script(setup_tx_creation):
ensure_bip65_activated() ensure_bip65_activated()

Loading…
Cancel
Save