3. Pre-signing review
3.1 Principle
A PSBT is chosen by whoever built it. BIP-174 makes display optional ("The
Signer can additionally compute the addresses and values being sent, and the
transaction fee, optionally showing this data to the user as a confirmation of
intent", bip-0174.mediawiki line 421). This profile makes it mandatory, and
it takes every fact on the review from the signer's own computation, never from
a field the PSBT could assert. The requirements are drawn from nullroute's
specs core.psbt.parse, core.psbt.review, core.psbt.sign, daemon.psbt
and ui.screens.lock in packages/ui/src/ui.spec.yaml.
3.2 What the review MUST show
SP-REV-1. Input that is not a well-formed PSBT MUST be refused before any review is shown, including empty input, a raw transaction, a descriptor and a truncated PSBT. Check: nullroute INV-PSBT-9.
SP-REV-2. Each input's amount MUST come from the UTXO record in the PSBT
(PSBT_IN_WITNESS_UTXO or PSBT_IN_NON_WITNESS_UTXO). A PSBT missing an
amount for any input MUST be refused, not reviewed with an assumed value.
Check: vector review-fee-and-amounts.json; nullroute INV-PSBT-5.
SP-REV-3. Unless every signature the signer will produce for the
transaction is a BIP-341 signature without SIGHASH_ANYONECANPAY, the signer
MUST, for every input, either confirm the amount from a
PSBT_IN_NON_WITNESS_UTXO whose TXID matches the input's prevout, or show the
fee as unverified and treat that as a blocking condition. Where every signature
is a BIP-341 signature without ANYONECANPAY, a witness UTXO is enough,
because each of those signatures commits to every input amount and a false
amount makes it invalid. Check: vector review-input-amounts.json (segwit v0
inputs with only a witness UTXO; a witness UTXO contradicting its non-witness
UTXO; a non-witness UTXO whose TXID does not match; an all-taproot case that
needs neither).
Basis, quoted from the BIPs:
- BIP-174, Signer (
bip-0174.mediawikiline 415): "The sighash algorithm for Segwit specified in BIP 143 is known to have an issue where an attacker could trick a user to sending Bitcoin to fees if they are able to convince the user to sign a malicious transaction multiple times. This is possible because the amounts inPSBT_IN_WITNESS_UTXOof other segwit inputs can be modified without effecting the signature for a particular input." - BIP-341 (
bip-0341.mediawikiline 133): "If theSIGHASH_ANYONECANPAYflag is not set, the message commits to the amounts of all transaction inputs", with the stated reason "This eliminates the possibility to lie to offline signing devices about the fee of a transaction."
SP-REV-4. The fee MUST be computed as the sum of input amounts minus the sum
of output amounts, in integer satoshis, and MUST NOT be read from any field. A
transaction whose outputs exceed its inputs MUST be refused. The fee MUST be
displayed in satoshis; other units MAY be added. Check: vector
review-fee-and-amounts.json; nullroute INV-PSBT-4, INV-PSBT-8, and
psbt-screen.test.tsx::shows-the-fee-in-several-units under INV-UI-12.
SP-REV-5. Every output MUST be displayed with its full address, or with its script where it has no address form, and its exact amount. An address MAY wrap across lines and MUST NOT be elided. Check: nullroute INV-PSBT-8, INV-PSBT-11; manual: review a transaction paying a 62-character address (SP-HW-2) and confirm every character is on screen.
SP-REV-6. An output MUST be labelled change only when the signer re-derives
its script from its own seed, or from a multisig descriptor registered on the
signer, at a path it then displays beside the output. No PSBT field, including
PSBT_OUT_BIP32_DERIVATION, PSBT_OUT_TAP_BIP32_DERIVATION, output position
or amount, MAY contribute to that label. An output beyond the signer's search
bound MUST be shown as a payment, and the bound MUST be documented. Check:
vector review-change.json (attacker's address in the change position with a
matching derivation hint; change past the search bound); nullroute INV-PSBT-2,
INV-PSBT-12, INV-PSBT-16, INV-UI-12.
SP-REV-7. An output the PSBT claims is the wallet's and which does not
re-derive MUST be presented as money leaving the wallet, and SHOULD say that the
transaction claimed it. Check: vector review-change.json; nullroute INV-UI-12
(says-when-the-transaction-claimed-an-output-was-ours).
SP-REV-8. The sighash type of the signatures to be produced MUST be displayed
in terms of what the signature does not commit to, not only as a number. Check:
vector review-sighash.json; nullroute INV-PSBT-6.
SP-REV-9. A non-zero nLockTime that consensus enforces MUST be displayed
as the block height or date before which the transaction cannot confirm.
Consensus enforces it only when at least one input's nSequence is not
0xffffffff (Bitcoin Core, IsFinalTx). When every input's is, the locktime is
ignored, and the signer MUST NOT display it as a time before which the
transaction cannot confirm. Whether the transaction signals replaceability
(BIP-125) MUST be displayed. Check: vector review-timelocks.json; nullroute
INV-PSBT-7, INV-PSBT-20.
SP-REV-10. When the wallet is not on mainnet, the network MUST be named on screen throughout. Check: nullroute INV-UI-3.
SP-REV-11. Key-value pairs the signer does not model MUST be preserved in
the signed PSBT (BIP-174, Signer: "The Signer must only add data to a PSBT",
line 417) and SHOULD be reported as present. Their presence MUST NOT by itself
block signing. Check: vector review-unknown-fields.json; nullroute
INV-PSBT-15.
SP-REV-12. Where the signer holds one key of a quorum, the review SHOULD state how many signatures the transaction needs, how many it has, and whether this signature completes it, and MUST treat a threshold it cannot read as unmet. Check: nullroute INV-QUORUM-4, INV-QUORUM-8, INV-UI-37, INV-UI-63.
SP-REV-13. A fee that is high against the amount leaving the wallet, or a
high fee rate, SHOULD produce a warning. This profile does not make a high fee
a blocking condition. Check: vector review-fee-and-amounts.json; nullroute
review.ts (high-fee, high-fee-rate, non-blocking, defaults 5 percent and
500 sat/vB).
SP-REV-14. A payment to one of the wallet's own receive addresses SHOULD be shown as a payment rather than as change. Check: nullroute INV-PSBT-13.
3.3 When signing MUST stay disabled
SP-REV-20. Reviewing MUST NOT sign. Signing MUST be a separate action that exists only after a review of that exact transaction has rendered. Check: nullroute INV-UI-11.
SP-REV-21. Signing MUST stay disabled until the user has brought the whole review into view, to its end. The surface the transaction was entered on MUST NOT count. Once reached, the end MAY latch so that scrolling back does not withdraw it. Check: nullroute INV-UI-103 (three bound tests); manual on hardware: load a review taller than the screen and confirm Sign stays disabled until the last element has been shown.
SP-REV-22. Any blocking condition MUST disable signing. It MAY be lifted
only by an explicit override that applies to one signature and is never
stored. The reason MUST be stated beside the disabled control. Check: vector
sign-override.json; nullroute INV-UI-13, INV-UI-62, INV-SIG-3.
SP-REV-23. Any sighash type other than SIGHASH_ALL (0x01) or
SIGHASH_DEFAULT (0x00, taproot only) MUST be a blocking condition. That
includes every ANYONECANPAY combination, SIGHASH_NONE, SIGHASH_SINGLE,
any value the signer does not recognise, and a transaction whose inputs do not
all request the same type. BIP-174 requires the refusal where the signer finds
a type unacceptable ("If unacceptable, they must fail", line 435); this profile
fixes which types are acceptable. Check: vector review-sighash.json (0x00,
0x01, 0x02, 0x03, 0x81, 0x82, 0x83, an undefined value such as 0x04, and a
mixed set); nullroute INV-PSBT-3.
SP-REV-24. A transaction with no input the signer owns MUST NOT be signable,
and the screen MUST say why. A signing attempt that produces no signature MUST
fail visibly rather than return an unsigned PSBT. Check: vector
review-ownership.json; nullroute INV-PSBT-14, INV-UI-14, INV-SIG-4.
SP-REV-25. Signing keys MUST be found by matching each input's own script
to the signer's derivations, not by trusting a derivation path the PSBT
supplies. Check: vector review-ownership.json (a PSBT whose derivation record
names the signer's fingerprint for a script it does not own); nullroute
INV-PSBT-14.
SP-REV-26. A failure to review or to sign MUST be displayed, and MUST NOT leave an earlier review or result on screen. Check: nullroute INV-UI-14.
3.4 Signing
SP-SIG-1. ECDSA signatures MUST use RFC 6979 nonces. Schnorr signatures MUST
use BIP-340 with aux_rand set to 32 zero bytes. Nothing else in the signing
path MAY consume randomness. BIP-340 permits this and names the cost:
"If randomness is not available at all at signing time, a simple counter ...
or even the constant array with 32 null bytes" may be used, and random
aux_rand "increases protection against [fault injection attacks]"
(bip-0340.mediawiki line 161). The profile accepts that cost so that anyone
holding the seed can recompute every signature byte for byte. Check: nullroute
INV-SIG-1, INV-SIG-2 (differential against bitcoinjs-lib, 120 cases); manual:
docs/VERIFICATION.md, section "4. Reproducing a signature".
3.5 nullroute conformance
| Id | Status | Evidence |
|---|---|---|
| SP-REV-1 | Met | INV-PSBT-9 |
| SP-REV-2 | Met | INV-PSBT-5 |
| SP-REV-3 | Met, more strictly than required | Review blocks, until the user overrides, whenever any input that is not taproot arrives without its previous transaction (INV-PSBT-17, packages/core/test/psbt.fee-attack.test.ts), including one this device does not sign, where the profile would permit a witness UTXO. With the previous transaction present, the signing library rejects one whose TXID or output contradicts the input while parsing (@scure/btc-signer 2.3.0, transaction.js lines 305-345, 536 and 613), and the test pins that. Bitcoin Core 31's walletcreatefundedpsbt includes the previous transaction for segwit v0 inputs, checked on regtest, so a Core-built PSBT passes without a warning |
| SP-REV-4 | Met | INV-PSBT-4, INV-PSBT-8, INV-UI-12 |
| SP-REV-5 | Met in the headless render; unverified on the panel | INV-PSBT-11; screens measured at 800x480 by tools/checks/check-screen-fit.mjs |
| SP-REV-6 | Met | INV-PSBT-2, INV-PSBT-12, INV-PSBT-16, INV-UI-12; search bound four script types, two branches, 100 addresses each (packages/daemon/src/psbt.spec.yaml lines 77-89) |
| SP-REV-7 | Met, including the SHOULD | INV-UI-12, INV-PSBT-2. A false claim is shown as a payment and reported as claimed, from PSBT_OUT_BIP32_DERIVATION and PSBT_OUT_TAP_BIP32_DERIVATION alike (review-change.json; psbt.review.test.ts::reports-a-false-taproot-claim-on-an-output) |
| SP-REV-8 | Met | INV-PSBT-6 |
| SP-REV-9 | Met | INV-PSBT-7. A transaction is replaceable when any input signals, as BIP-125 defines it, checked by review-timelocks.json and psbt.review.test.ts::counts-one-signalling-input-as-replaceable. INV-PSBT-20: a locktime every final input leaves unenforced is reported as ignored, not as a delay |
| SP-REV-10 | Met | INV-UI-3 |
| SP-REV-11 | Met | Unknown pairs never block and are reported (INV-PSBT-15), and survive parsing and signing in globals, inputs and outputs: parsePsbt keeps them for the signer, checked by review-unknown-fields.json and psbt.review.test.ts::keeps-fields-it-does-not-understand-through-signing |
| SP-REV-12 | Met | INV-QUORUM-4, INV-QUORUM-8, INV-UI-37, INV-UI-63 |
| SP-REV-13 | Met | review.ts raises non-blocking warnings above a fee rate and a fee percentage, which is what the requirement asks. docs/THREAT-MODEL.md now says so; it used to promise a second confirmation the code never had |
| SP-REV-14 | Met | INV-PSBT-13 |
| SP-REV-20 | Met | INV-UI-11 |
| SP-REV-21 | Met in tests; unverified on the panel | INV-UI-103 |
| SP-REV-22 | Met | INV-UI-13, INV-UI-62, INV-SIG-3; sign-override.json checks that the override applies to one signature and is not stored |
| SP-REV-23 | Met | INV-PSBT-3 |
| SP-REV-24 | Met | INV-PSBT-14, INV-UI-14, INV-SIG-4 |
| SP-REV-25 | Met | INV-PSBT-14 |
| SP-REV-26 | Met | INV-UI-14 |
| SP-SIG-1 | Met | INV-SIG-1, INV-SIG-2 |
Rendered from spec/signer-profile.md, the same bytes that ship with the code. SHA-256 4138440d8a2b925d07a9b6d1d96d4189a74c399b5fc717ae27a79d65c0065817