# 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.
"""How large the inputs of a Psbt will be once they are signed.
`Tx.size`, `Tx.vsize` and `Tx.weight` are read off a serialization, so
all three need the signatures a `Psbt` does not have yet. What a psbt
does have is, per input, the output being spent and the scripts that
unlock it, and those say how many bytes the missing signatures will
take -- which is what a fee has to be computed from, before there is
anything to sign.
Two rules are what make the answer honest rather than merely available.
**A signature is 72 bytes**, the sig_hash byte that follows it in the
script included. A DER signature is `30 <len> 02 <r len> r 02 <s len> s`,
so 71 bytes when the 32-byte `r` needs the leading zero its high bit
calls for and 70 when it does not: with the sig_hash byte, 72 or 71. Low
s is exactly the rule that keeps `s` from ever needing that zero, which
is why 72 is the largest and not merely the likeliest. Assuming the
shorter form would underpay the intended fee rate one transaction in
two, so the worst case is what is assumed here and every estimate is an
upper bound.
**An input whose type cannot be read raises.** The utxo, the redeem
script, the witness script and the derivation data are what the type is
read from, and an input that carries too little of them has no estimate:
guessing costs bytes in one direction only, and a fee computed from a
guess is a transaction that does not relay.
A caller who does not have to guess says so, with a `SolutionSizer`.
`miniscript_sizer` is one for every input whose witness script is a
BIP379 miniscript, and `satisfaction_sizer` one for an input whose
signers are known. Two inputs are refused above not for want of data but
for want of knowledge nobody but the caller has -- a script of no
standard type, and a taproot script path, where which leaf will be spent
is not in the psbt -- and a sizer is asked exactly there, never in place
of an answer this file can work out. What it returns is the whole of
what the input will push, the witness script of a p2wsh and the control
block of a taproot leaf included: a caller who knows the solution holds
those too, and one rule with no exceptions is what keeps the two sides
from each appending the other's element.
"""
from __future__ import annotations
from collections.abc import Callable, Iterable, Mapping
from typing import cast
from btclib.alias import Command, Octets
from btclib.exceptions import BTClibTypeError, BTClibValueError
from btclib.hashes import hash160
from btclib.script import serialize, type_and_payload
from btclib.tx import TxIn
from btclib.utils import assert_type, bytes_from_octets, is_octets
from btclib_wallet.descriptors import miniscript
from btclib_wallet.psbt.psbt_in import PsbtIn
__all__ = [
"COMPRESSED_PUB_KEY_SIZE",
"SCHNORR_SIG_SIZE",
"SIG_SIZE",
"SolutionSizer",
"estimated_input_sizes",
"miniscript_sizer",
"satisfaction_sizer",
]
# what an input will push, by the caller who knows: the size of every
# element of the witness stack, or of every push of a legacy script_sig,
# and None for an input this caller cannot answer for either
SolutionSizer = Callable[[PsbtIn, TxIn], "list[int] | None"]
# a DER signature and its sig_hash byte, at the largest a low-s signature
# can be; see the module docstring, which is where the assumption is
# stated because it is the one number a caller has to know about
SIG_SIZE = 72
# a BIP340 signature, to which a sig_hash byte is appended only when the
# sig_hash type is not the default one
SCHNORR_SIG_SIZE = 64
# a SEC compressed point
COMPRESSED_PUB_KEY_SIZE = 33
# OP_1 is 0x51 and OP_16 is 0x60, so the m and the n of an m-of-n
# multisig script are each written as its value plus this
_OP_INT_OFFSET = 0x50
def _script_pub_key(psbt_in: PsbtIn, tx_in: TxIn) -> bytes:
"""Return the script_pub_key of the output the input spends.
Not `psbt._signable_payload`, which answers a neighbouring question
and a different one: it returns the payload a Signer has to check a
script against, and refuses a witness utxo that is not a segwit
output. Here every type is an answer, and the whole script is what
the type is read from.
Where the input carries both utxo fields the non-witness one is read,
as `psbt._prev_out` reads it, the outpoint vouching for it and for
nothing else. It is checked against that outpoint here and the
witness utxo against it, as `Psbt.assert_valid` checks them: the pair
reaches this public function from the caller, and nothing need have
asked `assert_valid` about it first.
"""
non_witness_utxo = psbt_in.non_witness_utxo
witness_utxo = psbt_in.witness_utxo
if non_witness_utxo is None:
if witness_utxo is None:
raise BTClibValueError("no utxo")
return witness_utxo.script_pub_key.script
prev_out = tx_in.prev_out
if non_witness_utxo.id != prev_out.tx_id:
raise BTClibValueError("mismatched non-witness utxo / outpoint tx_id")
if prev_out.vout >= len(non_witness_utxo.vout):
raise BTClibValueError("outpoint vout out of range for the non-witness utxo")
spent = non_witness_utxo.vout[prev_out.vout]
if witness_utxo is not None and (
witness_utxo.value != spent.value
or witness_utxo.script_pub_key.script != spent.script_pub_key.script
):
raise BTClibValueError("mismatched witness utxo / non-witness utxo output")
return spent.script_pub_key.script
def _pub_key_size(psbt_in: PsbtIn, payload: bytes) -> int:
"""Return the size of the pub key a p2pkh input will push.
Compressed unless the derivation data says otherwise. A p2pkh script
commits to the hash160 of a key and not to its length, so the psbt is
the only thing that can tell the two apart, and an uncompressed key
is 32 bytes more: assuming compressed for an input that carries the
key would be assuming low, which is what this file is against.
"""
for pub_key in psbt_in.hd_key_paths:
if hash160(pub_key) == payload:
return len(pub_key)
return COMPRESSED_PUB_KEY_SIZE
def _solution_sizes(
script_type: str, payload: bytes, psbt_in: PsbtIn
) -> list[int] | None:
"""Return the size of each element that will satisfy the script.
In the order the elements go on the stack, so this is the witness
stack of a segwit input and the pushes of a legacy script_sig, one
list because it is one list of signatures either way.
None for a type this file has no answer for, which is the word a
`SolutionSizer` uses for the same thing: the caller of both tells
"no answer" from an answer by looking at what came back, and a
second spelling of it -- a raise, a set of the types answered for,
a caller catching what it does not want -- would be one more thing
to keep in agreement with the branches below.
"""
if script_type == "p2pkh":
return [SIG_SIZE, _pub_key_size(psbt_in, payload)]
if script_type == "p2pk":
# the pub key is in the script being spent, so only a signature
# is pushed
return [SIG_SIZE]
if script_type == "p2wpkh":
# no _pub_key_size: BIP143 refuses an uncompressed key in a
# witness v0 program, so there is nothing to look up
return [SIG_SIZE, COMPRESSED_PUB_KEY_SIZE]
if script_type == "p2ms":
# type_and_payload has vetted the shape -- 0 < m <= n < 17 -- so
# the first byte is OP_m and the arithmetic below cannot be wrong.
# The leading zero-size element is the one CHECKMULTISIG pops and
# does not use (BIP147)
m = payload[0] - _OP_INT_OFFSET
return [0, *[SIG_SIZE] * m]
return None
def _asked(
sizer: SolutionSizer | None,
psbt_in: PsbtIn,
tx_in: TxIn,
err_msg: str,
) -> list[int]:
"""Return what the sizer says the input pushes, or raise what it cannot.
The one place a sizer is consulted, so the rule that it answers only
where this file cannot is this function existing rather than a
condition repeated at each site. A sizer answering None is a caller
saying "not this input either", which is the refusal that was there
before it was asked.
"""
sizes = sizer(psbt_in, tx_in) if sizer else None
if sizes is None:
raise BTClibValueError(err_msg)
return sizes
def _taproot_sig_size(psbt_in: PsbtIn) -> int:
"""Return the size of the BIP341 key path signature.
64 bytes, or 65 when the input asks for a sig_hash type other than
the default one: that type is the byte appended to the signature, and
SIGHASH_DEFAULT is the absence of it.
"""
return SCHNORR_SIG_SIZE + (1 if psbt_in.sig_hash_type else 0)
def _p2wsh_witness_sizes(
psbt_in: PsbtIn,
tx_in: TxIn,
sizer: SolutionSizer | None,
) -> list[int]:
"""Return the witness stack of a p2wsh input, asking where it cannot."""
witness_script = psbt_in.witness_script
if not witness_script:
raise BTClibValueError("no witness script")
inner_type, inner_payload = type_and_payload(witness_script)
sizes = _solution_sizes(inner_type, inner_payload, psbt_in)
if sizes is None:
err_msg = f"no estimate for a script of type '{inner_type}'"
return _asked(sizer, psbt_in, tx_in, err_msg)
return [*sizes, len(witness_script)]
def _taproot_witness_sizes(
psbt_in: PsbtIn,
tx_in: TxIn,
sizer: SolutionSizer | None,
) -> list[int]:
"""Return the witness stack of a taproot input, asking for a script path.
A script path witness is the script's own inputs, the leaf script and
the control block, and which leaf will be spent is not something the
psbt says: an input carrying three of them has three different
answers and no way to choose. A caller that has chosen answers all
three.
"""
if psbt_in.taproot_leaf_scripts:
err_msg = "no estimate for a taproot script path"
return _asked(sizer, psbt_in, tx_in, err_msg)
return [_taproot_sig_size(psbt_in)]
def _assert_input_types(psbt_in: PsbtIn, tx_in: TxIn) -> None:
"""Refuse either object before a field is read off it.
Both are read for their fields, so a value of another type is an
AttributeError about a field name rather than a refusal. Every
`SolutionSizer` takes the same pair and owes its callers the same
check, so the sizers below ask this one.
"""
assert_type(psbt_in, PsbtIn, "psbt_in")
assert_type(tx_in, TxIn, "tx_in")
def _assert_arguments(
psbt_in: PsbtIn, tx_in: TxIn, sizer: SolutionSizer | None
) -> None:
"""Refuse an argument of the wrong type before any field is read.
The sizer is asked for here and not where it is consulted, which is a
single branch: one of no callable type went unnoticed for every input
`estimated_input_sizes` answers for on its own.
"""
_assert_input_types(psbt_in, tx_in)
if sizer is not None and not callable(sizer):
err_msg = f"invalid sizer type: {type(sizer).__name__}" # type: ignore[unreachable]
raise BTClibTypeError(err_msg)
[docs]
def miniscript_sizer(psbt_in: PsbtIn, tx_in: TxIn) -> list[int] | None:
"""Size the spend of a psbt input whose witness script is a miniscript.
A `SolutionSizer`, which is what `estimated_input_sizes` takes for the
inputs whose spend it cannot read. A caller passes it::
script_sig, witness = estimated_input_sizes(
psbt_in, tx_in, sizer=miniscript_sizer
)
The witness of a p2wsh spend is the satisfaction and then the witness
script, so that is the list: `Miniscript.max_witness_stack` and the
script's own length. It needs no signature and no preimage, an estimate
being made before either exists -- the size of a signature is the
context's, and BIP379 fixes a preimage at 32 bytes.
None where the input is not this sizer's business: no witness script,
one that is no miniscript, or one no witness can satisfy at all. The
caller then answers as it did before, which for the last of those is a
refusal: a script nobody can spend has no spend to estimate.
"""
# `tx_in` is unread here and checked anyway: the pair is what a
# `SolutionSizer` declares, so the guarantee is the signature's rather
# than this body's, and the sizer beside this one does read it
_assert_input_types(psbt_in, tx_in)
if not psbt_in.witness_script:
return None
known = (*psbt_in.partial_sigs, *psbt_in.hd_key_paths)
try:
node = miniscript.from_script(
psbt_in.witness_script,
miniscript.P2WSH,
{hash160(key): key for key in known},
)
# not a miniscript, which is the answer "not mine" and not an error
except BTClibValueError:
return None
stack = node.max_witness_stack
if stack is None:
return None
return [*stack, len(psbt_in.witness_script)]
[docs]
def satisfaction_sizer(keys: Iterable[Octets]) -> SolutionSizer:
"""Return a `SolutionSizer` for the satisfaction these keys will build.
`miniscript_sizer` answers "how large could this get", every branch
open and every signature assumed -- right where a caller does not yet
know which branch a spend will take, and an overpay where it does. A
quorum with a timelocked recovery quorum behind the same address is
exactly that second caller: which of the two a transaction takes is
decided before anything is estimated, the recovery spend being a
separate operation with its own inputs and its own signatures.
`Miniscript.satisfy` already answers the narrower question -- given
which keys will sign, which branch and how large -- so this is that
answer as a `SolutionSizer`, beside `miniscript_sizer` and not a mode
of it::
witness = estimated_input_sizes(
psbt_in, tx_in, sizer=satisfaction_sizer(recovery_keys)
)
`satisfy` never checks a signature against anything, so a filler one of
`SIG_SIZE` bytes for each key does exactly what a real one would for
sizing, and none of them has to exist yet, any more than the ones
`miniscript_sizer` assumes do. The preimages are the psbt input's own,
and the sequence is the input being sized, so an ``older()`` branch is
measured against what this spend actually carries. An ``after()`` branch
is not, `locktime` being the transaction's and not this call's to read
-- it is answered as unmet, and a script whose only open branch needs
one refuses rather than guesses.
None for that refusal and the two `miniscript_sizer` already answers
with it: no witness script, or one that is no miniscript. A caller
then falls back exactly as it would for any other sizer answering
"not mine".
"""
# an Octets is itself iterable, so `Iterable[Octets]` accepts one as
# far as the annotation goes: one key handed where the list was meant
# is as many keys as it has octets, and each of them invalid
if is_octets(keys):
raise BTClibTypeError(f"invalid keys type: {type(keys).__name__}")
if not isinstance(keys, Iterable):
raise BTClibTypeError(f"invalid keys type: {type(keys).__name__}")
signer_keys = tuple(bytes_from_octets(key) for key in keys)
def sizer(psbt_in: PsbtIn, tx_in: TxIn) -> list[int] | None:
_assert_input_types(psbt_in, tx_in)
if not psbt_in.witness_script:
return None
known = (*psbt_in.partial_sigs, *psbt_in.hd_key_paths, *signer_keys)
try:
node = miniscript.from_script(
psbt_in.witness_script,
miniscript.P2WSH,
{hash160(key): key for key in known},
)
# not a miniscript, which is the answer "not mine" and not an error
except BTClibValueError:
return None
signatures = dict.fromkeys(signer_keys, bytes(SIG_SIZE))
spend = miniscript.SpendContext(
sha256_preimages=psbt_in.sha256_preimages,
hash256_preimages=psbt_in.hash256_preimages,
ripemd160_preimages=psbt_in.ripemd160_preimages,
hash160_preimages=psbt_in.hash160_preimages,
sequence=tx_in.sequence,
)
try:
stack = node.satisfy(cast("Mapping[Octets, Octets]", signatures), spend)
# these keys build no satisfaction, or only a malleable one
except BTClibValueError:
return None
return [*(len(element) for element in stack), len(psbt_in.witness_script)]
return sizer