# 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.
"""The BIP375 roles: sending a silent payment through a psbt.
https://github.com/bitcoin/bips/blob/master/bip-0375.mediawiki
`btclib_wallet.psbt.psbt_in` and `btclib_wallet.psbt.psbt_out` carry BIP375's
six fields; this is what the two roles BIP375 adds *do* with them, and it is a
module rather than a function for the reason `btclib_wallet.psbt.musig2` is: a
role is a sequence of steps different parties take at different times.
**Why it matters more than a signature does.** A silent payment output
script is derived, not signed: get it wrong and the transaction is still
consensus-valid, so it confirms and the money is gone. The shares and the
BIP374 proofs are what make that derivation checkable by somebody holding
none of the keys -- and the Transaction Extractor is where the check has
to happen, being the last party before the bytes go on the wire.
The Signer's side, in the order BIP375 puts it:
- `set_input_share` writes an ECDH share and its proof for one input the
Signer holds the key of; `set_global_share` writes the one pair that
stands for every eligible input, which a Signer holding all the keys
may do instead.
- `assert_shares_as_valid` is what a Signer does with the shares it did
*not* write: every proof verified against the key of the input it
covers, or against the sum of them for a global one.
- `set_output_scripts` computes what the recipients are paid, once every
eligible input is covered, and clears the two modifiable flags -- the
scripts depend on the input set, so nothing may be added afterwards.
`assert_as_valid` is the Extractor's, and it is the four checks BIP375's
own validator publishes, in its order: the fields, then the share
coverage and the proofs, then which inputs are allowed to be there at
all, then the output scripts recomputed and compared.
`btclib_wallet.psbt.psbt.sign` and `btclib_wallet.psbt.psbt.extract_tx`
run the checks of their own role whenever an output carries
PSBT_OUT_SP_V0_INFO: `_assert_signable` and `_assert_extractable` are
what each asks. A psbt sent to a signer outside the process is asked
`_assert_sendable` first, which is `_assert_signable` for a psbt paying a
silent payment: `btclib_wallet.psbt_signer.request_signatures` asks it of
every signer, and `btclib_wallet.hwi.HwiSigner.sign_psbt` asks it of a
direct call.
**What a psbt input's public key is, and why it needs its own reader.**
`btclib_wallet.silent_payments.pub_key_from_input` reads it off a *signed*
input, from the witness or the scriptSig. An unsigned input has neither,
and BIP375 says where to look instead: the Updater "should add a
PSBT_IN_BIP32_DERIVATION for any p2wpkh, p2sh-p2wpkh, or p2pkh input so
the public key is available for creating the ecdh_shared_secret when the
private key is not known". `input_pub_key` is that reader, and it answers
None for an input BIP352 does not count -- a taproot NUMS internal key, a
p2sh wrapping anything but p2wpkh, a script type off the list.
**Which inputs count is decided by the script each one spends**, and not
by whether a key was found: the recipient sums the key of every input its
script type counts, so an input whose key the psbt does not carry is not
left out of the sum but refused. And nothing the psbt says about an input
is taken on its word -- a derivation key, a redeem script, a NUMS internal
key, a final scriptSig or witness each count only once checked against the
spent script, `_input_eligibility` saying how.
**The share is not the shared secret**, which is the thing in BIP375
easiest to get wrong: `a*B_scan` carries no input hash, and BIP352's
shared secret is `input_hash*a*B_scan`. So the Extractor multiplies the
share by the input hash, and the input hash needs the *sum of the public
keys* of the eligible inputs -- which is why reading those keys is the
first thing here and not an aside.
**The k of an output** is BIP375's own rule: it counts per scan key, over
the outputs sorted by spend key, then by output index.
`_ordered_sp_outputs` quotes the rule, and
`tests/psbt/silent_payments_test.py` says which of the vectors pins it.
**In one transaction, addresses that share a scan key must belong to one
recipient**, whether the other address is a fake or another owner's. Here k
follows the spend key, not the output's position: an address that reuses a
recipient's scan key, with a spend key that sorts below the recipient's,
takes a lower k than the recipient's wherever it is placed. The recipient's
scan, which stops at the first k it does not find, then never reaches the
recipient's output. The output stays spendable, but the recipient does not
find it. No order of the outputs prevents this. A recipient's own labelled
addresses are harmless where its scan holds those labels. Otherwise pay the
addresses in separate transactions.
"""
from __future__ import annotations
from collections.abc import Sequence
from btclib.alias import Integer, Octets, Point
from btclib.exceptions import BTClibValueError
from btclib.hashes import hash160
from btclib.script import serialize
from btclib.script.script_pub_key import (
is_p2pkh,
is_p2sh,
is_p2tr,
is_p2wpkh,
)
from btclib.script.sig_hash import ALL
from btclib.script.taproot import (
check_output_pubkey,
output_pubkey_from_merkle_root_var,
)
from ecc import dleq
from ecc.curves import (
bytes_from_point,
mult,
point_from_octets_var,
point_from_pub_key,
scalar_from_prv_key,
secp256k1,
)
from ecc.ssa import point_from_bip340pub_key_var
from btclib_wallet import silent_payments as sp
from btclib_wallet.psbt.psbt import (
INPUTS_MODIFIABLE,
OUTPUTS_MODIFIABLE,
Psbt,
_prev_out,
)
from btclib_wallet.psbt.psbt_in import PsbtIn
from btclib_wallet.psbt.psbt_out import PsbtOut
from btclib_wallet.psbt.psbt_utils import SP_SCAN_KEY_SIZE
__all__ = [
"assert_as_valid",
"assert_eligibility_as_valid",
"assert_output_scripts_as_valid",
"assert_shares_as_valid",
"eligible_pub_keys",
"input_pub_key",
"output_scripts",
"set_global_share",
"set_input_share",
"set_output_scripts",
"shared_secret_from_share",
]
# the highest witness version an input may spend while a silent payment
# output is present. Above it the spent script is one this protocol
# version has no rule for, and BIP352 skips such a transaction rather
# than guess -- so a sender must not build one
_MAX_WITNESS_VERSION = 1
# OP_1, which a version 1 witness program starts with: the versions run
# OP_1..OP_16 as 0x51..0x60, so a first byte above this is a version above 1
_OP_1 = 0x51
# the push opcodes of a scriptSig: OP_0 and the direct pushes run up to
# OP_PUSHDATA4, the PUSHDATA ones reading a length of 1, 2 or 4 bytes
_OP_PUSHDATA1 = 0x4C
_OP_PUSHDATA4 = 0x4E
# OP_1NEGATE, which with OP_1..OP_16 pushes a number and no data
_OP_1NEGATE = 0x4F
# BIP341's first byte of an annex, the witness element a spend may carry
# above its control block
_ANNEX_PREFIX = 0x50
# the two sizes of a SEC public key: BIP352 counts the compressed one and
# skips an input spent with the other
_COMPRESSED_SIZE = 33
_UNCOMPRESSED_SIZE = 65
# whether BIP352 counts an input, and the key it counts it with where the
# psbt carries one the spent script commits to
_Eligibility = tuple[bool, Point | None]
_INELIGIBLE: _Eligibility = (False, None)
def _witness_version(script: bytes) -> int | None:
"""Return the witness version of a program, or None if it is none.
The shape and not the length, which is deliberate: what this answers
is whether a version is above 1, and a v2-or-later program of a length
no BIP defines is still an input BIP352 has no rule for.
"""
if len(script) < 2 or script[1] != len(script) - 2:
return None
if script[0] == 0:
return 0
if _OP_1 <= script[0] <= _OP_1 + 15:
return script[0] - _OP_1 + 1
return None
def _script_pub_key(psbt_in: PsbtIn) -> bytes:
"""Return the script_pub_key of the output an input spends, or b""."""
prev_out = _prev_out(psbt_in)
return b"" if prev_out is None else prev_out.script_pub_key.script
def _witness_stack(psbt_in: PsbtIn) -> list[bytes]:
"""Return the final witness stack, BIP341's annex popped if present."""
stack = list(psbt_in.final_script_witness.stack)
if len(stack) > 1 and stack[-1][:1] == bytes([_ANNEX_PREFIX]):
stack.pop()
return stack
def _key_candidates(psbt_in: PsbtIn, *, script_sig_windows: bool) -> list[bytes]:
"""Return every string the psbt offers as the key of a single-key input.
The key data of PSBT_IN_BIP32_DERIVATION, and the last element of the
final witness. With `script_sig_windows`, every 33- and 65-byte window
of the final scriptSig too, BIP352's own reading of a p2pkh spend,
whose scriptSig a third party can pad. None of them is trusted:
`_committed_key` keeps the one the spent script commits to.
"""
candidates = list(psbt_in.hd_key_paths)
if stack := _witness_stack(psbt_in):
candidates.append(stack[-1])
if script_sig_windows:
script_sig = psbt_in.final_script_sig
for size in (_COMPRESSED_SIZE, _UNCOMPRESSED_SIZE):
candidates += [
script_sig[j : j + size] for j in range(len(script_sig) - size + 1)
]
return candidates
def _committed_key(key_hash: bytes, candidates: list[bytes]) -> _Eligibility:
"""Return what the key a script commits to by its hash says of the input.
The candidate whose hash160 is the one in the script, and no other: a
key the psbt names but the script does not commit to is a key nothing
binds to the input, and a share proved against it proves nothing about
what the recipient will sum. A compressed key is the input's key; an
uncompressed one is an input BIP352 skips; none found is an input
BIP352 counts whose key this psbt does not carry.
"""
committed = sorted(c for c in candidates if hash160(c) == key_hash)
for candidate in committed:
if len(candidate) == _COMPRESSED_SIZE:
try:
return True, point_from_octets_var(candidate, secp256k1)
except ValueError:
continue
if any(len(c) == _UNCOMPRESSED_SIZE for c in committed):
return _INELIGIBLE
return True, None
def _last_push(script: bytes) -> bytes | None:
"""Return the data of a push-only script's last push, or None.
BIP16's reading of a p2sh scriptSig: it must be push-only, and its last
push is the redeem script. A script with an opcode that is no push, or
one that ends inside a push, has no redeem script to read.
"""
pushed = None
i = 0
while i < len(script):
op = script[i]
i += 1
if op <= _OP_PUSHDATA4:
if op < _OP_PUSHDATA1:
size = op
else:
width = 1 << (op - _OP_PUSHDATA1)
size = int.from_bytes(script[i : i + width], "little")
i += width
if i + size > len(script):
return None
pushed = script[i : i + size]
i += size
elif op == _OP_1NEGATE:
pushed = b"\x81"
elif _OP_1 <= op <= _OP_1 + 15:
pushed = bytes([op - _OP_1 + 1])
else:
return None
return pushed
def _redeem_script(psbt_in: PsbtIn, script_hash: bytes) -> bytes | None:
"""Return the redeem script a p2sh input spends, if the psbt carries it.
PSBT_IN_REDEEM_SCRIPT, or the last push of the final scriptSig, which
is where BIP16 puts it whatever it wraps; either only where it hashes
to the script_pub_key.
"""
candidates = [psbt_in.redeem_script]
if psbt_in.final_script_sig:
last = _last_push(psbt_in.final_script_sig)
if last is not None:
candidates.append(last)
return next(
(
candidate
for candidate in candidates
if candidate and hash160(candidate) == script_hash
),
None,
)
def _is_proven_nums(psbt_in: PsbtIn, output_key: bytes) -> bool:
"""Answer whether the output's internal key is proven to be NUMS_H.
BIP352 skips a taproot input whose internal key is BIP341's NUMS
point, which has no key path. The psbt's word for it is believed only
with a proof against the output key: PSBT_IN_TAP_INTERNAL_KEY tweaked
by PSBT_IN_TAP_MERKLE_ROOT, or a control block -- of a
PSBT_IN_TAP_LEAF_SCRIPT, or of the final witness -- that proves its
leaf. A claim without one proves nothing: the input is then counted,
and its key is the output key, which is bound to it by construction.
"""
if psbt_in.taproot_internal_key == sp.NUMS_H:
tweaked, _ = output_pubkey_from_merkle_root_var(
sp.NUMS_H, psbt_in.taproot_merkle_root
)
if tweaked == output_key:
return True
leaves = [
(script, control)
for control, (script, _) in psbt_in.taproot_leaf_scripts.items()
]
stack = _witness_stack(psbt_in)
if len(stack) > 1:
leaves.append((stack[-2], stack[-1]))
for script, control in leaves:
if control[1:33] != sp.NUMS_H:
continue
try:
if check_output_pubkey(output_key, script, control):
return True
except ValueError:
continue
return False
def _input_eligibility(psbt_in: PsbtIn) -> _Eligibility:
"""Return whether BIP352 counts one psbt input, and its key if found.
Decided by the script the input spends, BIP352's four types, and by
the two exclusions that are not the type: a taproot output whose
internal key is NUMS_H, and a p2sh that wraps anything but p2wpkh.
Every claim the psbt makes towards the answer -- a derivation key, a
redeem script, an internal key, the final scriptSig and witness -- is
checked against that script before it counts, so that a claim cannot
take a counted input out of the sum. A final scriptSig or witness most
of all: consensus has not checked it yet, and whoever wrote it can
replace it after the other Signers signed.
A p2sh input whose redeem script the psbt does not carry is counted
and has no key: BIP352 may count it, and the answer is the psbt's to
give, not a default's.
"""
script = _script_pub_key(psbt_in)
if is_p2tr(script):
return _taproot_eligibility(psbt_in, script[2:])
if is_p2pkh(script):
candidates = _key_candidates(psbt_in, script_sig_windows=True)
return _committed_key(script[3:23], candidates)
program = script
if is_p2sh(script):
redeem_script = _redeem_script(psbt_in, script[2:22])
if redeem_script is None:
return True, None
# counted only where it wraps a p2wpkh, and read as that p2wpkh
program = redeem_script
if is_p2wpkh(program):
candidates = _key_candidates(psbt_in, script_sig_windows=False)
return _committed_key(program[2:], candidates)
return _INELIGIBLE
def _taproot_eligibility(psbt_in: PsbtIn, output_key: bytes) -> _Eligibility:
"""Return what BIP352 makes of a taproot input: its output key, or skip.
Skipped only where NUMS_H is proven to be the internal key; an output
key that is no x coordinate is counted and keyless.
"""
if _is_proven_nums(psbt_in, output_key):
return _INELIGIBLE
try:
return True, point_from_bip340pub_key_var(output_key, secp256k1)
except ValueError:
return True, None
def _no_key_error(i: int) -> BTClibValueError:
"""Return the refusal of a counted input whose key the psbt lacks."""
err_msg = f"input {i}: BIP352 counts it and the psbt carries no public key "
err_msg += "the script it spends commits to; BIP375 asks an Updater for "
err_msg += "PSBT_IN_BIP32_DERIVATION so that there is one"
return BTClibValueError(err_msg)
def _eligible_keys(psbt: Psbt) -> dict[int, Point | None]:
"""Return every input BIP352 counts, by index, with its key if found."""
eligible = {}
for i, psbt_in in enumerate(psbt.inputs):
counted, pub_key = _input_eligibility(psbt_in)
if counted:
eligible[i] = pub_key
return eligible
[docs]
def eligible_pub_keys(psbt: Psbt) -> dict[int, Point]:
"""Return the public key of every input BIP352 counts, by index.
The index is kept because the per-input shares are filed per input: a
coverage rule that answered "how many" rather than "which" could not
name the input whose share is missing.
A counted input whose key the psbt does not carry is refused rather
than left out: the recipient sums every counted input's key, so a sum
of the others derives a script nobody scans for.
"""
keys = {}
for i, pub_key in _eligible_keys(psbt).items():
if pub_key is None:
raise _no_key_error(i)
keys[i] = pub_key
return keys
def _scan_keys(psbt: Psbt) -> list[bytes]:
"""Return the scan key of every silent payment output, deduplicated."""
seen: dict[bytes, None] = {
psbt_out.sp_v0_info[:SP_SCAN_KEY_SIZE]: None
for psbt_out in psbt.outputs
if psbt_out.sp_v0_info
}
return list(seen)
def _share_and_sum(psbt: Psbt, scan_key: bytes) -> tuple[bytes, Point] | None:
"""Return the share standing for every eligible input, and their sum.
The global share where there is one, else the sum of the per-input
shares of the eligible inputs -- which is what makes the two
interchangeable downstream: `a_1*B + a_2*B` is `(a_1 + a_2)*B`, so a
transaction whose signers each contributed one share derives the same
outputs as one whose single signer contributed the lot.
None when there is nothing to derive from: no share at all, or no
eligible input to take a public key from. A counted input whose key
the psbt does not carry is refused, by `eligible_pub_keys`, once there
is a share to derive from.
"""
eligible = _eligible_keys(psbt)
share = psbt.sp_ecdh_shares.get(scan_key)
if share is None:
shares = [
psbt.inputs[i].sp_ecdh_shares[scan_key]
for i in eligible
if scan_key in psbt.inputs[i].sp_ecdh_shares
]
if not shares:
return None
share = bytes_from_point(sp.pub_key_sum(shares), secp256k1)
if not eligible:
return None
return share, sp.pub_key_sum(list(eligible_pub_keys(psbt).values()))
[docs]
def shared_secret_from_share(psbt: Psbt, share: Octets, A_sum: Point) -> Point:
"""Return BIP352's shared secret from a BIP375 share.
The step the two BIPs do not share a name for, and the one worth
spelling out: the psbt carries `a*B_scan`, with no input hash in it,
where BIP352's secret is `input_hash*a*B_scan`. So the share is
multiplied by the input hash here, and the input hash is what binds
the derivation to this transaction's smallest outpoint -- which is why
the psbt is an argument and the share alone would not do.
"""
outpoints = [psbt_in.prev_out for psbt_in in psbt.inputs]
return sp.shared_secret(sp.input_hash(outpoints, A_sum), share)
def _ordered_sp_outputs(psbt: Psbt) -> list[tuple[int, PsbtOut]]:
"""Return the silent payment outputs in the order their k follows.
Sorted by PSBT_OUT_SP_V0_INFO, then by output index. Within one scan
key that is the spend key's order, which is BIP375's rule: "If there
are multiple silent payment codes with the same scan key, sort the
codes lexicographically in ascending order to determine the ordering
of the k value. If there are multiple silent payment codes with both
the same scan and spend keys, sort the subgroup by output index in
ascending order." `bip-0375/validator/validate_psbt.py` sorts on the
same key, and `output_scripts` counts k per scan key, so how the
groups interleave does not matter.
"""
return sorted(
((i, o) for i, o in enumerate(psbt.outputs) if o.sp_v0_info),
key=lambda pair: (pair[1].sp_v0_info, pair[0]),
)
[docs]
def output_scripts(psbt: Psbt) -> dict[int, bytes]:
"""Return the script every silent payment output should pay, by index.
An output whose scan key has no share is absent from the answer rather
than raising: a psbt under construction is allowed to have one, which
is the "in progress" half of BIP375's own vectors, and what refuses
the ones that are not allowed is `assert_output_scripts_as_valid`. A
counted input whose key the psbt does not carry raises instead, the
sum every script depends on being unknown.
"""
scripts: dict[int, bytes] = {}
counters: dict[bytes, int] = {}
for i, psbt_out in _ordered_sp_outputs(psbt):
scan_key = psbt_out.sp_v0_info[:SP_SCAN_KEY_SIZE]
found = _share_and_sum(psbt, scan_key)
if found is None:
continue
share, A_sum = found
k = counters.get(scan_key, 0)
counters[scan_key] = k + 1
secret = shared_secret_from_share(psbt, share, A_sum)
B_m = psbt_out.sp_v0_info[SP_SCAN_KEY_SIZE:]
# `serialize(["OP_1", key])` and not `ScriptPubKey.p2tr(key)`: that
# classmethod takes an *internal* key and applies BIP341's tweak,
# where what BIP352 derives is already the output key. Tweaking it
# a second time is a script no recipient scans for
scripts[i] = serialize(["OP_1", sp.output_key(secret, B_m, k)])
return scripts
def _assert_pair(
shares: dict[bytes, bytes], proofs: dict[bytes, bytes], what: str
) -> None:
"""Raise unless the shares and the proofs name the same scan keys.
Each half is useless without the other: a share nobody can hold its
writer to is what carrying a proof exists to prevent, and a proof of a
share that is not there proves nothing at all.
"""
for scan_key in shares:
if scan_key not in proofs:
err_msg = f"{what} ECDH share with no proof beside it: scan key "
err_msg += scan_key.hex()
raise BTClibValueError(err_msg)
for scan_key in proofs:
if scan_key not in shares:
err_msg = f"{what} DLEQ proof with no share to prove: scan key "
err_msg += scan_key.hex()
raise BTClibValueError(err_msg)
def _assert_global_shares(psbt: Psbt) -> None:
"""Raise unless the global shares are proved against the input sum.
A global share stands for every eligible input at once, so what proves
it is the sum of their public keys -- and a psbt carrying one with no
eligible input to sum is a psbt whose share nothing can be checked
against, which is a different failure from a proof that does not
verify.
"""
_assert_pair(psbt.sp_ecdh_shares, psbt.sp_dleq_proofs, "global")
if not psbt.sp_ecdh_shares:
return
pub_keys = eligible_pub_keys(psbt)
if not pub_keys:
err_msg = "global ECDH share with no eligible input to prove it against"
raise BTClibValueError(err_msg)
A_sum = sp.pub_key_sum(list(pub_keys.values()))
for scan_key, share in psbt.sp_ecdh_shares.items():
if not dleq.verify_proof(A_sum, scan_key, share, psbt.sp_dleq_proofs[scan_key]):
raise BTClibValueError(f"invalid global DLEQ proof for {scan_key.hex()}")
def _assert_input_shares(psbt: Psbt, eligible: dict[int, Point | None]) -> None:
"""Raise unless every per-input share is proved against its own key.
A share on an input BIP352 does not count is passed over rather than
refused: `_share_and_sum` gives it no weight either, and one of
BIP375's valid vectors carries exactly that. An input that *is*
counted and carries no public key is the opposite case and is refused
-- BIP375 asks an Updater for PSBT_IN_BIP32_DERIVATION so that there
is one, and one of its invalid vectors is that field missing.
"""
for i, psbt_in in enumerate(psbt.inputs):
if psbt_in.sp_ecdh_shares and i not in eligible:
continue
_assert_pair(psbt_in.sp_ecdh_shares, psbt_in.sp_dleq_proofs, f"input {i}")
for scan_key, share in psbt_in.sp_ecdh_shares.items():
A = eligible.get(i)
if A is None:
err_msg = f"input {i}: ECDH share on an input with no public key to "
err_msg += "prove it against; BIP375 asks an Updater for "
err_msg += "PSBT_IN_BIP32_DERIVATION so that there is one"
raise BTClibValueError(err_msg)
proof = psbt_in.sp_dleq_proofs[scan_key]
if not dleq.verify_proof(A, scan_key, share, proof):
err_msg = f"input {i}: invalid DLEQ proof for {scan_key.hex()}"
raise BTClibValueError(err_msg)
[docs]
def assert_shares_as_valid(psbt: Psbt) -> None:
"""Raise unless every ECDH share the psbt carries is proved.
BIP375's second check, and the one that makes a share worth reading: a
proof is verified against the public key of what it covers -- the sum
of the eligible inputs' keys for a global share, that one input's key
for a per-input one -- so a share can be trusted by a party holding
none of the private keys.
"""
_assert_global_shares(psbt)
_assert_input_shares(psbt, _eligible_keys(psbt))
def _assert_covered(psbt: Psbt, scan_key: bytes) -> None:
"""Raise unless every eligible input contributes to this scan key.
Asked of a scan key whose output script is set, or about to be:
before that the psbt is under construction, and a share that has not
arrived is a signer that has not signed. A script is a claim about
every eligible input, so a missing share means the script was derived
from fewer keys than the recipient will sum -- and the recipient would
find nothing.
The walk is over the inputs BIP352 counts, whether or not their key
was found: an input whose key is missing is still summed by the
recipient, so it is refused for the key, which is what it lacks first.
"""
if scan_key in psbt.sp_ecdh_shares:
return
for i, pub_key in _eligible_keys(psbt).items():
if pub_key is None:
raise _no_key_error(i)
if scan_key not in psbt.inputs[i].sp_ecdh_shares:
err_msg = f"input {i}: no ECDH share for scan key {scan_key.hex()}, "
err_msg += "which every counted input owes a script derived for it"
raise BTClibValueError(err_msg)
[docs]
def assert_eligibility_as_valid(psbt: Psbt) -> None:
"""Raise unless every input may be there at all, silent payments present.
BIP375's third check, and the two rules are BIP352's reasons in a
psbt's terms. An input spending a witness program above version 1 is
one this protocol version has no derivation rule for, so BIP352 skips
the whole transaction -- which makes building one a way to pay an
address nobody will scan. And a sighash type other than SIGHASH_ALL
lets the inputs or the outputs change after the scripts were derived
from them: BIP352 permits NONE and SINGLE, BIP375 does not, because
here the scripts are computed from the number and the position of the
codes.
"""
if not _scan_keys(psbt):
return
for i, psbt_in in enumerate(psbt.inputs):
version = _witness_version(_script_pub_key(psbt_in))
if version is not None and version > _MAX_WITNESS_VERSION:
err_msg = f"input {i}: spends witness version {version}, which a psbt "
err_msg += "with a silent payment output must not"
raise BTClibValueError(err_msg)
if psbt_in.sig_hash_type is not None and psbt_in.sig_hash_type != ALL:
err_msg = f"input {i}: sig hash type {psbt_in.sig_hash_type}, where a "
err_msg += "psbt with a silent payment output requires SIGHASH_ALL"
raise BTClibValueError(err_msg)
[docs]
def assert_output_scripts_as_valid(psbt: Psbt) -> None:
"""Raise unless every silent payment script is the one derived.
BIP375's fourth check and the Extractor's reason to exist: this is the
error a signature cannot catch, a wrong output script being
consensus-valid. An output that carries no script yet is passed over
-- that is a psbt still being built -- and one that carries a script
without the shares to derive it is not.
"""
scripted = [
(i, psbt_out.sp_v0_info, script)
for i, psbt_out in enumerate(psbt.outputs)
if psbt_out.sp_v0_info and (script := psbt_out.script_pub_key) is not None
]
# coverage before derivation: a script derived from fewer inputs than
# the recipient sums is the error to name, rather than whichever
# refusal deriving it would meet first
for _, info, _ in scripted:
_assert_covered(psbt, info[:SP_SCAN_KEY_SIZE])
derived = output_scripts(psbt) if scripted else {}
for i, info, script in scripted:
scan_key = info[:SP_SCAN_KEY_SIZE]
if i not in derived:
err_msg = f"output {i}: PSBT_OUT_SCRIPT with no ECDH share to derive it "
err_msg += f"from, for scan key {scan_key.hex()}"
raise BTClibValueError(err_msg)
if script != derived[i]:
err_msg = f"output {i}: PSBT_OUT_SCRIPT is not the silent payment script "
err_msg += f"its address derives: {script.hex()} instead of "
err_msg += derived[i].hex()
raise BTClibValueError(err_msg)
def _assert_modifiable_cleared(psbt: Psbt) -> None:
"""Raise if a derived output script may still have its inputs changed.
BIP375: a Signer that sets a missing PSBT_OUT_SCRIPT "must set the
Inputs Modifiable and Outputs Modifiable flags to False". The script
is a function of the input set and of the position of the codes, so a
psbt that publishes one and still invites changes publishes a script
that the next Constructor invalidates.
"""
if psbt.tx_modifiable is None:
return
if not any(o.sp_v0_info and o.script_pub_key is not None for o in psbt.outputs):
return
if psbt.tx_modifiable & (INPUTS_MODIFIABLE | OUTPUTS_MODIFIABLE):
err_msg = (
"PSBT_GLOBAL_TX_MODIFIABLE still invites changes, with a silent "
"payment output script already derived from what it would change"
)
raise BTClibValueError(err_msg)
[docs]
def assert_as_valid(psbt: Psbt) -> None:
"""Raise unless the psbt satisfies BIP375, the roles included.
`Psbt.assert_valid` is the format; this is the protocol on top of it,
and it is what a Transaction Extractor owes a silent payment before it
hands the bytes over. The four checks in BIP375's own order, each
naming what failed: the fields, the shares and their proofs, which
inputs may be present, and the output scripts.
A psbt with no silent payment output passes everything here, there
being nothing to derive.
"""
psbt.assert_valid()
_assert_modifiable_cleared(psbt)
assert_shares_as_valid(psbt)
assert_eligibility_as_valid(psbt)
assert_output_scripts_as_valid(psbt)
def _assert_output_scripts_set(psbt: Psbt, what: str) -> None:
"""Raise if a silent payment output has no PSBT_OUT_SCRIPT yet.
A refusal the checks above do not make, since a psbt under
construction may lack a script: signed or extracted, such an output
pays the empty script, which anybody can spend.
"""
for i, psbt_out in enumerate(psbt.outputs):
if psbt_out.sp_v0_info and psbt_out.script_pub_key is None:
err_msg = f"output {i}: silent payment output with no PSBT_OUT_SCRIPT, "
raise BTClibValueError(err_msg + what)
def _assert_signable(psbt: Psbt) -> None:
"""Raise unless BIP375 lets a Signer sign this psbt.
`btclib_wallet.psbt.psbt.sign` asks it whenever an output carries
PSBT_OUT_SP_V0_INFO. First the modifiable flags, whose violation
makes the psbt one of BIP375's invalid ones whatever the role; then
the two rules "the Signer must fail" on, an input spending a witness
version above 1 and a sighash type other than SIGHASH_ALL; then the
shares other Signers wrote, which the Signer "should verify". Each of
those says the psbt is wrong, where the missing script, next, says
only that it is early: "the Signer must not yet add a signature"
names a psbt waiting for `set_output_scripts`. Last, every
script present is the one the shares derive, a signature being what
commits the funds to it.
"""
_assert_modifiable_cleared(psbt)
assert_eligibility_as_valid(psbt)
assert_shares_as_valid(psbt)
what = "and BIP375's Signer must not yet sign: set_output_scripts derives it"
_assert_output_scripts_set(psbt, what)
assert_output_scripts_as_valid(psbt)
def _assert_extractable(psbt: Psbt) -> None:
"""Raise unless BIP375 lets a Transaction Extractor extract this psbt.
`btclib_wallet.psbt.psbt.extract_tx` asks it whenever an output
carries PSBT_OUT_SP_V0_INFO: `assert_as_valid`, and a script on every
silent payment output, the Extractor being where a psbt stops being
under construction.
"""
assert_as_valid(psbt)
what = "so the extracted transaction would pay the empty script"
_assert_output_scripts_set(psbt, what)
def _assert_sendable(psbt: Psbt) -> None:
"""Raise unless a psbt may go to a signer outside the process.
The Signer's checks, `_assert_signable`, asked before the psbt leaves
rather than left to whatever answers: a signer that does not know
BIP375 signs a silent payment output whatever script it carries, and
its signature commits the funds to that script. A psbt paying no
silent payment has nothing here to ask.
"""
if not any(psbt_out.sp_v0_info for psbt_out in psbt.outputs):
return
psbt.assert_valid()
_assert_signable(psbt)
def _share_for(a: int, scan_key: bytes, aux: Octets | None) -> tuple[bytes, bytes]:
"""Return the ECDH share for one scalar and scan key, and its proof."""
B_scan = point_from_pub_key(scan_key)
share = bytes_from_point(mult(a, B_scan), secp256k1)
return share, dleq.generate_proof(a, B_scan, aux)
[docs]
def set_global_share(
psbt: Psbt, prv_keys: Sequence[Integer], aux: Octets | None = None
) -> None:
"""Write the one ECDH share standing for every eligible input.
What a Signer holding *every* eligible input's key may do instead of
one share each: the sum of those keys, once, with one proof against
the sum of their public keys. Fewer bytes in the psbt and one
verification for every reader of it.
The keys are given in the order of the eligible inputs, and the sum is
checked against the sum of their public keys before anything is
written: a global share proved against the wrong sum is a share that
fails for every recipient at once.
"""
pub_keys = eligible_pub_keys(psbt)
if len(prv_keys) != len(pub_keys):
err_msg = f"{len(prv_keys)} private keys for {len(pub_keys)} eligible inputs"
raise BTClibValueError(err_msg)
a = 0
for prv_key in prv_keys:
a = (a + scalar_from_prv_key(prv_key)) % secp256k1.n
if a == 0:
raise BTClibValueError("input private keys sum to zero")
if mult(a) != sp.pub_key_sum(list(pub_keys.values())):
err_msg = "the private keys do not sum to the eligible inputs' public keys"
raise BTClibValueError(err_msg)
for scan_key in _scan_keys(psbt):
share, proof = _share_for(a, scan_key, aux)
psbt.sp_ecdh_shares[scan_key] = share
psbt.sp_dleq_proofs[scan_key] = proof
[docs]
def set_output_scripts(psbt: Psbt) -> None:
"""Derive every silent payment output script, and freeze the psbt.
The Signer's last step before it signs: BIP375 forbids a signature
while an output has no script, and requires the two modifiable flags
cleared once one is written -- the script is a function of the input
set, so a psbt that still invites inputs invites its own scripts to
become wrong.
Every silent payment output must be derivable, or nothing is written:
a psbt half-derived is one whose recipients each need the other's
signer to have finished. And derivable means BIP375's "if all
eligible inputs have an ECDH share or the global ECDH share is set":
a script summed from the shares that have arrived is one
`assert_output_scripts_as_valid` refuses.
The module docstring's "In one transaction, addresses that share a
scan key must belong to one recipient" says which addresses it may
pay together.
"""
for scan_key in _scan_keys(psbt):
_assert_covered(psbt, scan_key)
scripts = output_scripts(psbt)
if missing := [
i for i, o in enumerate(psbt.outputs) if o.sp_v0_info and i not in scripts
]:
err_msg = f"no ECDH share to derive the script of output(s) {missing}"
raise BTClibValueError(err_msg)
if not scripts:
return
for i, script in scripts.items():
psbt.outputs[i].script_pub_key = script
psbt.tx_modifiable = (psbt.tx_modifiable or 0) & ~(
INPUTS_MODIFIABLE | OUTPUTS_MODIFIABLE
)