# 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.
"""BIP174's Updater for the script a descriptor describes.
`update_psbt_input` and `update_psbt_output` write what
`Descriptor.expansion` returns into one input or one output of a psbt, for
signers to fill in at their own pace and `finalize` to assemble. The
output half is also what tells change from a payment, and it makes that
claim only where the descriptor derives the very script being paid:
`Descriptor.index_of` is the same question asked the other way round, and
neither answers from a key origin whose fingerprint happens to match.
"""
from __future__ import annotations
from copy import deepcopy
from btclib.exceptions import BTClibTypeError, BTClibValueError
from btclib.utils import assert_type, is_integer
from btclib_wallet.descriptors.descriptors import Descriptor, Expansion
from btclib_wallet.descriptors.key_expression import PrvKeys
from btclib_wallet.psbt.psbt import Psbt
from btclib_wallet.psbt.psbt_in import PsbtIn
from btclib_wallet.psbt.psbt_out import PsbtOut
__all__ = ["update_psbt_input", "update_psbt_output"]
def _assert_arguments(
descriptor: Descriptor, psbt: Psbt, map_i: int, side: str
) -> None:
"""Refuse a descriptor, a psbt or a map index of another type."""
assert_type(descriptor, Descriptor, "descriptor")
assert_type(psbt, Psbt, "psbt")
if not is_integer(map_i):
err_msg = f"invalid {side} index type: {type(map_i).__name__}"
raise BTClibTypeError(err_msg)
def _write(psbt_map: PsbtIn | PsbtOut, expansion: Expansion) -> None:
"""Write the fields an input and an output share.
One function for both maps, because BIP174 gives the two the same
fields wherever they mean the same thing: a redeem script, a witness
script and a key origin say what they say whether the psbt is spending
the script or paying to it. Only a ``tr()`` differs, an input carrying
the leaf it spends and an output the whole tree, and that is where the
two functions below part company.
A mapping is added to and not replaced: BIP174 keys it by public key,
so a psbt already carrying another signer's key keeps it, and this
descriptor's entry wins for a key held by both. A script and an
internal key are written where the descriptor has one, and left alone
where it has none.
"""
psbt_map.hd_key_paths = {**psbt_map.hd_key_paths, **expansion.hd_key_paths}
if expansion.redeem_script:
psbt_map.redeem_script = expansion.redeem_script
if expansion.witness_script:
psbt_map.witness_script = expansion.witness_script
if expansion.taproot_internal_key:
psbt_map.taproot_internal_key = expansion.taproot_internal_key
psbt_map.taproot_hd_key_paths = {
**psbt_map.taproot_hd_key_paths,
**expansion.taproot_hd_key_paths,
}
psbt_map.musig2_participant_pub_keys = {
**psbt_map.musig2_participant_pub_keys,
**expansion.musig2_participant_pub_keys,
}
[docs]
def update_psbt_output(
descriptor: Descriptor,
psbt: Psbt,
vout_i: int,
index: int = 0,
prv_keys: PrvKeys | None = None,
) -> Psbt:
"""Return the psbt with output `vout_i` told what the descriptor knows.
The Updater's other half, and what makes an output recognizable as the
wallet's own: the redeem script of a ``sh()``, the witness script of a
``wsh()``, the internal key and the whole script tree of a ``tr()``,
and the origin of every key. A signing device reads them to tell
change from a payment -- it can derive the script itself and see that
the money comes back -- and a wallet reading a psbt somebody else
built reads them for the same reason.
Unlike the input half, the script *is* checked: the output being paid
is in the psbt already, so this refuses unless the descriptor derives
exactly that script at `index`. Marking an output as one's own is a
claim about where money goes, and the only evidence for it is the
whole script -- never a key origin whose four-byte fingerprint
matches, which is what `Descriptor.index_of` is for and what it says.
The output tree is BIP371's PSBT_OUT_TAP_TREE and not the leaf script
an input carries: an output has no leaf being spent, so what it
publishes is every leaf, each with its depth, which is what lets a
reader rebuild the tree and check the output key for itself. No merkle
root goes with it, the root being the fold of the tree.
"""
_assert_arguments(descriptor, psbt, vout_i, "output")
expansion = descriptor.expansion(index, prv_keys)
# an IndexError out of a public function is not an answer, and a
# negative index would quietly update the output at the other end
if not 0 <= vout_i < len(psbt.outputs):
raise BTClibValueError(f"invalid output index: {vout_i}")
paid = psbt.tx.vout[vout_i].script_pub_key.script
if all(
script.script != paid for script in descriptor.script_pub_keys(index, prv_keys)
):
err_msg = f"output {vout_i} pays to {paid.hex()}, which is not the"
err_msg += f" script this descriptor describes at index {index}"
raise BTClibValueError(err_msg)
psbt = deepcopy(psbt)
psbt_out = psbt.outputs[vout_i]
_write(psbt_out, expansion)
if expansion.taproot_tree is not None:
psbt_out.taproot_tree = list(expansion.taproot_tree)
psbt.assert_valid()
return psbt