diff --git a/jmbitcoin/jmbitcoin/secp256k1_transaction.py b/jmbitcoin/jmbitcoin/secp256k1_transaction.py index 09f17d5..2cf212b 100644 --- a/jmbitcoin/jmbitcoin/secp256k1_transaction.py +++ b/jmbitcoin/jmbitcoin/secp256k1_transaction.py @@ -110,10 +110,12 @@ def estimate_tx_size(ins, outs, txtype='p2pkh', outtype=None): '''Estimate transaction size. The txtype field as detailed below is used to distinguish 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, - outputs only contribute a little to the overall total). This combined - with a few bytes variation in signature sizes means we will expect, - say, 10% inaccuracy here. + we assume that the scriptPubKey type of all the outputs are the same as + the input, unless `outtype` is specified, in which case *one* of + the outputs is assumed to be that other type, with all of the other + 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: out: 8+1+3+20+2=34, in: 32+4+1+1+~72+1+33+4=148, diff --git a/jmclient/test/test_tx_creation.py b/jmclient/test/test_tx_creation.py index 8075656..4dec62f 100644 --- a/jmclient/test/test_tx_creation.py +++ b/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 pytest 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() #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) with pytest.raises(ValueError) as e_info: 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): #non-standard hash codes: @@ -137,6 +138,97 @@ def test_spend_p2wpkh(setup_tx_creation): txid = jm_single().bc_interface.pushtx(tx.serialize()) 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): ensure_bip65_activated()