Skip to the content
nullroute
Browse the docs

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.mediawiki line 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 in PSBT_IN_WITNESS_UTXO of other segwit inputs can be modified without effecting the signature for a particular input."
  • BIP-341 (bip-0341.mediawiki line 133): "If the SIGHASH_ANYONECANPAY flag 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