Source code for btclib_wallet.tx_builder

# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.

"""Compose the psbt spending a set of outputs, at a fee rate, with change.

Every part of this is elsewhere and nothing composes them, so every
caller composes them again: `Psbt` holds what is being built,
`psbt.prevouts` says what its inputs are worth, `Psbt.vsize_estimate`
says how large the signed transaction will be, `fee.fee_from_vsize`
prices that size and `fee.dust_threshold` says whether the change is
worth creating. `build_psbt` is that composition, and the three
decisions it makes are the ones a hand-written builder gets wrong.

**The fee comes from the rate and the size, and the size comes from the
psbt.** A transaction that is not signed has no size to be priced by --
`Tx.vsize` is read off a serialization, and the signatures are not
written yet -- so the object built here is a psbt: `Psbt.vsize_estimate`
sizes the missing signatures from each input's utxo and scripts, which
is the one thing in the tree that answers before there is anything to
sign. The unsigned transaction is `built.psbt.tx`, a property away, so a
second entry point answering with a `Tx` would be a second spelling of
one answer rather than a second answer.

**Change is an output or it is fee.** Below `dust_threshold` for its own
script an output cannot be relayed, so it is not created and its value
is left to the fee; and the transaction is then smaller than the one
that was priced, so the fee it owes is computed again rather than
reused. `change_index` is which output it is, or None for the branch
that dropped it.

**An input is a `PsbtIn`**: the outpoint it spends, the output that
outpoint names -- `witness_utxo` or `non_witness_utxo`, whichever its
kind of input takes -- and whatever else says how it will be unlocked, a
redeem script or a witness script included. Two things follow from
taking the psbt's own map rather than a pair of an outpoint and a
`TxOut`. Nothing here fetches anything, an outpoint alone saying neither
what it is worth nor what it spends, and a builder that fetches is a
builder with a node in it; and an input whose script is wrapped or
multisig is estimated exactly, its redeem or witness script being a
field of the map that arrives rather than an argument this function
would have to grow. The outputs are `TxOut` and not `PsbtOut` because
the asymmetry is real: the input map is *read*, every byte a signature
will take being computed from it, where nothing computed here reads an
output map -- what a wallet writes into one, `descriptors`'
`update_psbt_output` on the change output at `change_index` included, is
written after this returns and changes no size.

Explicitly not here, both of them boundaries this library draws
elsewhere too:

- **coin selection**. Which utxos to spend is policy with a literature
  behind it, and keeping it out is what lets a caller bring its own;
  this spends the ones it is given, all of them. `tx.input_weight` is
  the number that caller prices a candidate with -- one input, before
  there is a psbt to put it in -- where what a fee is bought by here is
  the whole transaction, which `Psbt.vsize_estimate` is the one arithmetic
  for.
- **a node, an rpc, or wallet state**. The same arguments give the same
  answer forever, which is `fee`'s own boundary: what is downstream of a
  network -- a fee estimate for a confirmation target, which utxos are
  confirmed, the block height that would make an anti-fee-sniping lock
  time -- is fed in rather than fetched.
"""

from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass
from math import ceil

from btclib import var_int
from btclib.alias import Octets
from btclib.consensus import WITNESS_SCALE_FACTOR
from btclib.exceptions import BTClibValueError
from btclib.fee import DUST_RELAY_FEE_RATE, FeeRate, dust_threshold, fee_from_vsize
from btclib.tx import TxOut
from btclib.tx.tx import SEGWIT_MARKER
from btclib.utils import assert_type, bytes_from_octets

from btclib_wallet.psbt.psbt import Psbt, prevouts
from btclib_wallet.psbt.psbt_in import PsbtIn
from btclib_wallet.psbt.psbt_out import PsbtOut
from btclib_wallet.psbt.psbt_size import SolutionSizer
from btclib_wallet.psbt.psbt_utils import PSBT_V0

__all__ = [
    "FundedPsbt",
    "build_psbt",
]


[docs] @dataclass(frozen=True) class FundedPsbt: """A psbt whose fee is paid, and what paying it decided. The three answers Bitcoin Core's `fundrawtransaction` gives, which is the same triple under its own names: the transaction, `fee`, and `changepos` -- spelled here as an index into the psbt's outputs, and None where Core writes -1 for the transaction that has no change output. `fee` is what the inputs are worth less what the outputs hold. It is at least what `fee_rate` asked of the estimated size and can exceed it, by exactly the change that was too small to create. """ psbt: Psbt fee: int change_index: int | None @property def change(self) -> int: """Return the satoshi the change output holds, 0 where there is none.""" if self.change_index is None: return 0 return self.psbt.outputs[self.change_index].amount or 0
def _target_overhead_vsize(outputs: Sequence[TxOut], candidate_count: int) -> int: """Return what a transaction's own bytes cost, no input counted. Version, lock time, the output count and the outputs themselves, read off `Psbt.vsize_estimate` over a psbt of these outputs and no inputs at all -- this function is that estimate, not a second copy of its arithmetic, so a change to one reaches the other. Version and lock time cost four bytes each whatever value they hold, so the placeholder's own 2 and 0 answer for a caller who will pass `build_psbt` something else too. Padded by one virtual byte for the segwit marker `build_psbt` writes once any selected input carries a witness, which this function -- given only the outputs, before a single input is chosen -- cannot know either way. The pad rounds up rather than down: left off, a selection built entirely of witness inputs would ask `build_psbt` for a few satoshi it does not have; left on, a selection of non-witness inputs alone asks for one virtual byte more than `build_psbt` will actually charge, an overshoot rather than a shortfall. `candidate_count` bounds the other unknown, the input count's own var_int: the placeholder above prices it at zero inputs, one byte, but the true count is whatever the selection this overhead feeds ends up choosing, which is not yet decided when this is asked and can be as large as the whole pool it is choosing from. Read from `candidate_count` -- the pool's own size, always at least the selected count -- rather than the placeholder's zero, so the estimate never charges for fewer input-count bytes than the real selection can turn out to need; below 253 candidates the two var_ints are one byte either way and this adds nothing. `coin_selection` is the caller this exists for: what one input costs is `tx.input_weight`'s answer, supplied per candidate; what the rest of the transaction costs is this one, and a selection's own target is the two added together, so that a match to it is a match `build_psbt` can fund. """ psbt_outputs = [ PsbtOut(amount=tx_out.value, script_pub_key=tx_out.script_pub_key.script) for tx_out in outputs ] placeholder = Psbt( 2, [], psbt_outputs, PSBT_V0, {}, fallback_lock_time=0, check_validity=False ) marker_pad = ceil(len(SEGWIT_MARKER) / WITNESS_SCALE_FACTOR) # the placeholder above already prices a zero-input var_int, one byte; # this is only the extra width a pool of 253 or more candidates can add input_count_pad = len(var_int.serialize(candidate_count)) - len( var_int.serialize(0) ) return placeholder.vsize_estimate() + marker_pad + input_count_pad def _assert_arguments( inputs: Sequence[PsbtIn], outputs: Sequence[TxOut], fee_rate: FeeRate, dust_fee_rate: FeeRate, ) -> None: """Refuse an argument of the wrong type before a field is read off it. The two sequences are checked as sequences and then element by element, which is the shape `tests/built_object_contract_test.py` calls a sequence walked before it is checked: `None` is not iterable from underneath this library, and a `str` is a sequence of one-character strings that would each be read for fields it has not got. """ assert_type(inputs, Sequence, "inputs") for psbt_in in inputs: assert_type(psbt_in, PsbtIn, "psbt input") assert_type(outputs, Sequence, "outputs") for tx_out in outputs: assert_type(tx_out, TxOut, "output") assert_type(fee_rate, FeeRate, "fee rate") assert_type(dust_fee_rate, FeeRate, "dust fee rate")
[docs] def build_psbt( inputs: Sequence[PsbtIn], outputs: Sequence[TxOut], fee_rate: FeeRate, change_script_pub_key: Octets | None = None, *, tx_version: int = 2, lock_time: int = 0, dust_fee_rate: FeeRate = DUST_RELAY_FEE_RATE, sizer: SolutionSizer | None = None, ) -> FundedPsbt: """Return the psbt spending these inputs at this rate, and its change. `inputs` are the psbt's own input maps, each carrying the outpoint it spends and the output that outpoint names; `outputs` are what is being paid. What is left over pays the fee, and `change_script_pub_key` is where the rest of it goes -- to an output of that script when it would be worth more than `dust_threshold` asks, and to the fee when it would not. No change script at all is every leftover satoshi to the fee, which is what a caller sweeping an address means and what a caller who forgot the argument gets, so it is spelled rather than defaulted. Raised, all as `BTClibValueError`: no inputs, no outputs left to pay, an outpoint spent twice, an input carrying no utxo, an input whose type the psbt does not determine -- `psbt_size`'s rule, and `sizer` is where a caller answers for one -- and inputs that do not cover the outputs and the fee. `tx_version` and `lock_time` are the transaction's, defaulting to Core's own 2 and to no lock time: the block height that would make a lock time worth setting is a node's answer, and this function has no node. Each input's sequence is its own `PsbtIn.sequence`, and an input naming none spends with the final sequence -- no lock time and no BIP125 replacement, which a caller wanting either sets on the input rather than having overwritten here. `dust_fee_rate` is the rate the dust threshold is computed at, Core's `-dustrelayfee` default; it is not `fee_rate`, an output being dust by what the network will relay rather than by what this transaction chose to pay. The psbt is version 0, which every Signer reads; `Psbt.to_v2` is the other one. """ _assert_arguments(inputs, outputs, fee_rate, dust_fee_rate) if not inputs: raise BTClibValueError("no inputs") change_script = ( None if change_script_pub_key is None else bytes_from_octets(change_script_pub_key) ) psbt_outputs = [ PsbtOut(amount=tx_out.value, script_pub_key=tx_out.script_pub_key.script) for tx_out in outputs ] change_index: int | None = None if change_script is not None: change_index = len(psbt_outputs) # the amount is what the fee leaves and the fee is what this # psbt's size costs, so the output has to be in it before there # is an amount to put in the output. A value is eight bytes # whatever it holds, so the estimate does not move when the # placeholder below is replaced by the answer computed from it psbt_outputs.append(PsbtOut(amount=0, script_pub_key=change_script)) # not validated here: `prevouts` below validates, and it is the first # thing that reads the psbt psbt = Psbt( tx_version, inputs, psbt_outputs, PSBT_V0, {}, fallback_lock_time=lock_time, check_validity=False, ) # `prevouts` validates, and the psbt's transaction with it, so the # outpoint spent twice this sum would double count is refused before # the sum is made -- `Tx.assert_valid`'s rule, Core's # `bad-txns-inputs-duplicate`, rather than one this function repeats total_in = sum(prev_out.value for prev_out in prevouts(psbt)) total_out = sum(tx_out.value for tx_out in outputs) remainder = total_in - total_out if change_script is not None: fee = fee_from_vsize(psbt.vsize_estimate(sizer), fee_rate) change = remainder - fee if change >= dust_threshold(change_script, dust_fee_rate): # the last output, this having appended it psbt.outputs[-1].amount = change # the one state nothing else has judged: every other exit # returns what `vsize_estimate` last validated psbt.assert_valid() return FundedPsbt(psbt, fee, change_index) # dust cannot be created, so what would have been change is fee psbt.outputs.pop() change_index = None if not psbt.outputs: err_msg = "no outputs: nothing is paid, and there is no change to create" raise BTClibValueError(err_msg) # the whole leftover, which is at least what the rate asks: the # transaction is smaller than the one priced above, so what it owes # is computed again rather than the larger figure reused owed = fee_from_vsize(psbt.vsize_estimate(sizer), fee_rate) if remainder < owed: err_msg = f"the inputs are worth {total_in} satoshi, " err_msg += f"where the outputs and the fee need {total_out + owed}" raise BTClibValueError(err_msg) return FundedPsbt(psbt, remainder, change_index)