diff --git a/CHANGELOG.md b/CHANGELOG.md index b8d7b87..f759185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.3.0 +- Add Bitcoin APIs, sandbox actions, and simulator transaction-vector coverage + ## 0.2.0 - Add Cardano support diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b762af..eb9cae5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,7 +53,7 @@ npm run test:sim On Linux x64, the harness downloads and caches the simulator binaries listed in `test/simulators.json` under `test/simulators/`, verifies their sha256 hashes, -and starts the newest listed simulator by default. +and runs the test matrix against every listed firmware version. Useful environment variables: @@ -92,8 +92,9 @@ The package is intended as a drop-in TypeScript replacement for the current - Public errors must keep the `{ code, message, err? }` shape. New public error behavior should go through `src/internal/errors.ts` and match the compatibility taxonomy unless there is an explicit reason to extend it. -- BTC and BIP85 methods remain compatibility stubs in this iteration. Do not - document or test them as implemented until their protocol support lands. +- Bitcoin xpub, address, script config, PSBT, and message-signing support is + implemented; keep it aligned with the Rust/WASM reference and simulator + vectors. BIP85 methods remain compatibility stubs. - Cardano xpub, address, and transaction-signing support is implemented; keep it aligned with the Rust/WASM reference behavior and simulator vectors. diff --git a/README.md b/README.md index 12bd9dd..0e7afc2 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,14 @@ Pure TypeScript library for integrating BitBox02 hardware wallets in browser applications. -`@bitboxswiss/bitbox-api` is source-compatible with the current `bitbox-api` -Rust/WASM package for the implemented surface, but it does not ship WASM and -does not require a WASM init step. - ## Status - **Implemented:** WebHID and BitBoxBridge transports, Noise XX pairing, - device metadata helpers, Ethereum xpub/address/signing methods, antiklepto, - transaction data streaming, EIP-712 typed messages, `ethIdentifyCase()`, - and Cardano xpub/address/signing methods. -- **Stubbed with `code: 'unsupported'`:** BTC and BIP85 methods. + device metadata helpers, Bitcoin xpub/address/PSBT/message signing methods, + script config registration, Ethereum xpub/address/signing methods, + antiklepto, transaction data streaming, EIP-712 typed messages, + `ethIdentifyCase()`, and Cardano xpub/address/signing methods. +- **Stubbed with `code: 'unsupported'`:** BIP85 methods. - **Stubbed with `code: 'not-implemented'`:** `showMnemonic()` and `changePassword()`. @@ -25,6 +22,8 @@ npm install @bitboxswiss/bitbox-api `@noble/ciphers`, `@noble/curves`, and `@noble/hashes` are peer dependencies so the library does not perturb the crypto dependency graph of wallet/Web3 apps. +`bitcoinjs-lib`, used to parse and update PSBTs, is installed as a regular +dependency. - **npm 7+** installs peer dependencies automatically. - **pnpm/yarn** peer handling depends on your package-manager version and @@ -37,23 +36,6 @@ the library does not perturb the crypto dependency graph of wallet/Web3 apps. The package is ESM-only. -## Migrating from `bitbox-api` - -For currently implemented methods, the intended migration is just the import -name: - -```ts -import * as bitbox from '@bitboxswiss/bitbox-api'; -``` - -There is no `init()` call and no WASM loader. Existing Webpack/Vite WASM plugin -configuration from `bitbox-api` is not needed for this package. - -BTC and BIP85 methods are still present in the public type surface for -compatibility, but currently reject with typed errors as listed in -[Status](#status). Ethereum, Cardano, and the general device helpers listed -above are wired through the TypeScript transport. - ## Browser Requirements - WebHID works in Chromium-based browsers in a secure context, such as HTTPS or @@ -111,6 +93,48 @@ closed. Reconnect before retrying. Call `bb02.close()` when your app is done with the device. `close()` is idempotent and invokes the `onClose` callback supplied to the connect function. +## Bitcoin Usage + +Bitcoin-family methods accept coin, keypath, script config, and xpub type +strings. Keypaths can be strings or number arrays. + +```ts +const account = "m/84'/0'/0'"; +const keypath = `${account}/0/0`; + +const xpub = await bb02.btcXpub('btc', account, 'xpub', false); +const address = await bb02.btcAddress( + 'btc', + keypath, + { simpleType: 'p2wpkh' }, + true, +); +``` + +`btcSignPSBT()` accepts and returns a base64-encoded PSBT. Single-signature +P2WPKH, wrapped P2WPKH, and P2TR script configs are inferred from PSBT key +origins. Pass `force_script_config` for multisig and policy wallets, which must +first be registered on the device. + +```ts +const signedPsbt = await bb02.btcSignPSBT( + 'btc', + psbtBase64, + undefined, + 'default', +); + +const messageSignature = await bb02.btcSignMessage( + 'btc', + { scriptConfig: { simpleType: 'p2wpkh' }, keypath }, + new TextEncoder().encode('hello bitbox'), +); +``` + +The returned PSBT contains the device signatures but is not finalized. Use a +Bitcoin library to validate, combine, or finalize it as appropriate for the +wallet. + ## Ethereum Usage Keypaths can be strings such as `m/44'/60'/0'/0/0` or number arrays. Chain IDs @@ -289,11 +313,12 @@ Common client-facing codes include: - `user-abort` / `bitbox-user-abort`: the user cancelled in the browser or on the device. - `invalid-type`, `keypath-parse`, `chain-id-too-large`: invalid host inputs. +- `psbt-parse` and `psbt-*`: malformed PSBTs or unsupported PSBT contents. +- `btc-sign`: an invalid or incomplete Bitcoin signing exchange. - `communication`, `noise`, `noise-config`, `pairing-rejected`: transport, pairing, or encrypted-channel failures. - `version`: the connected firmware is too old for the requested method. -- `unsupported` / `not-implemented`: public compatibility methods that are not - wired in this TypeScript iteration. +- `unsupported` / `not-implemented`: methods that are not currently available. ## Sandbox and Development @@ -307,13 +332,14 @@ make sandbox-dev Open the printed Vite URL, usually `http://localhost:5173`. -For build, test, simulator, protobuf, and contribution workflow details, see -[CONTRIBUTING.md](CONTRIBUTING.md). +The Bitcoin accordion covers xpubs, addresses, script config registration, +PSBT signing, and message signing. Bitcoin unit tests are part of +`npm run build && npm test`; the firmware transaction-vector suite runs against +the simulator matrix with: -## API Compatibility +```bash +npm run test:sim -- test/simulator-btc.test.ts +``` -`test/api-snapshot.test.ts` compares the built TypeScript declarations against -`../bitbox-api-rs/pkg/bitbox_api.d.ts` when the reference checkout is present. -The guard keeps the public surface drop-in compatible while still allowing -deliberate source-compatible TypeScript widenings, such as optional close -callbacks and safer `bigint` chain ID paths. +For build, test, simulator, protobuf, and contribution workflow details, see +[CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/package-lock.json b/package-lock.json index f16de72..7f5b5fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "@bitboxswiss/bitbox-api", - "version": "0.1.1", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitboxswiss/bitbox-api", - "version": "0.1.1", + "version": "0.3.0", "license": "Apache-2.0", "dependencies": { - "@bufbuild/protobuf": "2.11.0" + "@bufbuild/protobuf": "2.11.0", + "bitcoinjs-lib": "7.0.1", + "bs58check": "4.0.0" }, "devDependencies": { "@bufbuild/buf": "1.68.3", @@ -27,6 +29,7 @@ "eslint": "10.2.1", "react": "18.3.1", "react-dom": "18.3.1", + "tiny-secp256k1": "2.2.4", "typescript": "6.0.3", "typescript-eslint": "8.59.0", "vite": "6.4.2", @@ -931,7 +934,6 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "dev": true, "license": "MIT", "engines": { "node": "^14.21.3 || >=16" @@ -2110,6 +2112,49 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/bip174": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-3.0.0.tgz", + "integrity": "sha512-N3vz3rqikLEu0d6yQL8GTrSkpYb35NQKWMR7Hlza0lOj6ZOlvQ3Xr7N9Y+JPebaCVoEUHdBeBSuLxcHr71r+Lw==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.9", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bitcoinjs-lib": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-7.0.1.tgz", + "integrity": "sha512-vwEmpL5Tpj0I0RBdNkcDMXePoaYSTeKY6mL6/l5esbnTs+jGdPDuLp4NY1hSh6Zk5wSgePygZ4Wx5JJao30Pww==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^3.0.0", + "bs58check": "^4.0.0", + "uint8array-tools": "^0.0.9", + "valibot": "^1.2.0", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -2123,6 +2168,25 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3066,6 +3130,29 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-secp256k1": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", + "integrity": "sha512-FoDTcToPqZE454Q04hH9o2EhxWsm7pOSpicyHkgTwKhdKWdsTUuqfP5MLq3g+VjAtl2vSx6JpXGdwA2qpYkI0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tiny-secp256k1/node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3140,7 +3227,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3174,6 +3261,15 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uint8array-tools": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.9.tgz", + "integrity": "sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -3191,6 +3287,38 @@ "punycode": "^2.1.0" } }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/varuint-bitcoin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", + "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.8" + } + }, + "node_modules/varuint-bitcoin/node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", diff --git a/package.json b/package.json index 326d60c..4a710fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitboxswiss/bitbox-api", - "version": "0.2.0", + "version": "0.3.0", "description": "A pure TypeScript library to interact with BitBox hardware wallets.", "license": "Apache-2.0", "type": "module", @@ -52,6 +52,7 @@ "eslint": "10.2.1", "react": "18.3.1", "react-dom": "18.3.1", + "tiny-secp256k1": "2.2.4", "typescript": "6.0.3", "typescript-eslint": "8.59.0", "vite": "6.4.2", @@ -61,6 +62,7 @@ "bitbox", "bitbox02", "hardware-wallet", + "bitcoin", "ethereum", "typescript" ], @@ -70,7 +72,9 @@ }, "homepage": "https://bitbox.swiss/", "dependencies": { - "@bufbuild/protobuf": "2.11.0" + "@bufbuild/protobuf": "2.11.0", + "bitcoinjs-lib": "7.0.1", + "bs58check": "4.0.0" }, "peerDependencies": { "@noble/ciphers": "^1.3.0", diff --git a/sandbox/src/App.tsx b/sandbox/src/App.tsx index 5548e06..1cd050f 100644 --- a/sandbox/src/App.tsx +++ b/sandbox/src/App.tsx @@ -5,6 +5,7 @@ import * as bitbox from '@bitboxswiss/bitbox-api'; import './App.css'; import { Cardano } from './Cardano'; +import { Bitcoin } from './Bitcoin'; import { Ethereum } from './Ethereum'; import { General } from './General'; import { ErrorNotification } from './ErrorNotification'; @@ -89,6 +90,9 @@ function App() { + + + {bb02.ethSupported() && ( diff --git a/sandbox/src/Bitcoin.tsx b/sandbox/src/Bitcoin.tsx new file mode 100644 index 0000000..f36d19d --- /dev/null +++ b/sandbox/src/Bitcoin.tsx @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { type FormEvent, useState } from 'react'; +import * as bitbox from '@bitboxswiss/bitbox-api'; + +import { ErrorNotification } from './ErrorNotification'; +import { ResultBlock, formatResult } from './form-utils'; + +type Props = { bb02: bitbox.PairedBitBox }; + +const BTC_COINS: bitbox.BtcCoin[] = ['btc', 'tbtc', 'ltc', 'tltc', 'rbtc']; +const XPUB_TYPES: bitbox.XPubType[] = [ + 'tpub', 'xpub', 'ypub', 'zpub', 'vpub', 'upub', 'Vpub', 'Zpub', 'Upub', 'Ypub', +]; +const SIMPLE_TYPES: bitbox.BtcSimpleType[] = ['p2wpkhP2sh', 'p2wpkh', 'p2tr']; + +function ErrorView({ + error, + clear, +}: { + error: bitbox.Error | undefined; + clear: () => void; +}) { + return error === undefined + ? null + : ; +} + +function CoinSelect({ + coin, + setCoin, +}: { + coin: bitbox.BtcCoin; + setCoin: (coin: bitbox.BtcCoin) => void; +}) { + return ( + + Coin + setCoin(event.target.value as bitbox.BtcCoin)}> + {BTC_COINS.map(option => ( + {option} + ))} + + + ); +} + +function BtcXPub({ bb02 }: Props) { + const [coin, setCoin] = useState('btc'); + const [keypath, setKeypath] = useState("m/84'/0'/0'"); + const [xpubType, setXpubType] = useState('xpub'); + const [display, setDisplay] = useState(true); + const [result, setResult] = useState(''); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(''); + setError(undefined); + try { + setResult(await bb02.btcXpub(coin, keypath, xpubType, display)); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( + + XPub + + + + Keypath + setKeypath(event.target.value)} /> + + + XPub type + setXpubType(event.target.value as bitbox.XPubType)} + > + {XPUB_TYPES.map(option => ( + {option} + ))} + + + + Display on device + setDisplay(event.target.checked)} + /> + + Get XPub + + setError(undefined)} /> + + + ); +} + +function BtcXPubs({ bb02 }: Props) { + const [coin, setCoin] = useState('btc'); + const [keypaths, setKeypaths] = useState( + `["m/49'/0'/0'", "m/84'/0'/0'", "m/86'/0'/0'"]`, + ); + const [xpubType, setXpubType] = useState('xpub'); + const [result, setResult] = useState(); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(undefined); + setError(undefined); + try { + setResult(await bb02.btcXpubs( + coin, + JSON.parse(keypaths) as bitbox.Keypath[], + xpubType, + )); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( + + Multiple XPubs + + + Keypaths + setKeypaths(event.target.value)} rows={5} /> + + XPub type + setXpubType(event.target.value as bitbox.BtcXPubsType)} + > + tpub + xpub + + + Get XPubs + + setError(undefined)} /> + + + ); +} + +function BtcAddressSimple({ bb02 }: Props) { + const [coin, setCoin] = useState('btc'); + const [simpleType, setSimpleType] = useState('p2wpkhP2sh'); + const [display, setDisplay] = useState(true); + const [isChange, setIsChange] = useState(false); + const [addressIndex, setAddressIndex] = useState(0); + const [account, setAccount] = useState(0); + const [result, setResult] = useState(''); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + + const coinType = coin === 'btc' ? 0 : coin === 'ltc' ? 2 : 1; + const purpose = simpleType === 'p2wpkhP2sh' ? 49 : simpleType === 'p2wpkh' ? 84 : 86; + const keypath = `m/${purpose}'/${coinType}'/${account}'/${isChange ? 1 : 0}/${addressIndex}`; + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(''); + setError(undefined); + try { + setResult(await bb02.btcAddress(coin, keypath, { simpleType }, display)); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( + + Address + + + + Simple type + setSimpleType(event.target.value as bitbox.BtcSimpleType)} + > + {SIMPLE_TYPES.map(option => ( + + {option} + + ))} + + + + Account + setAccount(Number(event.target.value))} + /> + + + Change + setIsChange(event.target.checked)} + /> + + + Address index + setAddressIndex(Number(event.target.value))} + /> + + + Display on device + setDisplay(event.target.checked)} + /> + + Keypath: {keypath} + Get address + + setError(undefined)} /> + + + ); +} + +function BtcSignPSBT({ bb02 }: Props) { + const [coin, setCoin] = useState('btc'); + const [psbt, setPSBT] = useState(''); + const [formatUnit, setFormatUnit] = useState('default'); + const [result, setResult] = useState(''); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(''); + setError(undefined); + try { + setResult(await bb02.btcSignPSBT(coin, psbt, undefined, formatUnit)); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( + + Sign PSBT + + + + Format unit + setFormatUnit(event.target.value as bitbox.BtcFormatUnit)} + > + default + sat + + + PSBT + setPSBT(event.target.value)} + placeholder="base64 PSBT" + rows={8} + /> + Sign PSBT + + setError(undefined)} /> + + + ); +} + +function BtcSignMessage({ bb02 }: Props) { + const [simpleType, setSimpleType] = useState('p2wpkhP2sh'); + const [keypath, setKeypath] = useState("m/49'/0'/0'/0/0"); + const [message, setMessage] = useState('message'); + const [result, setResult] = useState(); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(undefined); + setError(undefined); + try { + setResult(await bb02.btcSignMessage( + 'btc', + { scriptConfig: { simpleType }, keypath }, + new TextEncoder().encode(message), + )); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( + + Sign message + + + Simple type + setSimpleType(event.target.value as bitbox.BtcSimpleType)} + > + p2wpkhP2sh + p2wpkh + + + + Keypath + setKeypath(event.target.value)} /> + + Message + setMessage(event.target.value)} rows={4} /> + Sign message + + setError(undefined)} /> + + + ); +} + +function BtcMultisigAddress({ bb02 }: Props) { + const [scriptType, setScriptType] = useState('p2wsh'); + const [result, setResult] = useState(''); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + const coin: bitbox.BtcCoin = 'tbtc'; + const keypath = "m/48'/1'/0'/2'"; + const otherXpub = 'tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk'; + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(''); + setError(undefined); + try { + const ourXpub = await bb02.btcXpub(coin, keypath, 'tpub', false); + const config: bitbox.BtcScriptConfig = { + multisig: { + threshold: 1, + xpubs: [ourXpub, otherXpub], + ourXpubIndex: 0, + scriptType, + }, + }; + if (!await bb02.btcIsScriptConfigRegistered(coin, config, keypath)) { + await bb02.btcRegisterScriptConfig( + coin, + config, + keypath, + 'autoXpubTpub', + undefined, + ); + } + setResult(await bb02.btcAddress(coin, `${keypath}/0/10`, config, true)); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( + + Multisig address + + + Script type + setScriptType( + event.target.value as bitbox.BtcMultisigScriptType, + )} + > + p2wsh + p2wshP2sh + + + Account keypath: {keypath} + Get multisig address + + setError(undefined)} /> + + + ); +} + +function BtcPolicyAddress({ bb02 }: Props) { + const [result, setResult] = useState(''); + const [running, setRunning] = useState(false); + const [error, setError] = useState(); + const coin: bitbox.BtcCoin = 'tbtc'; + const policy = 'wsh(andor(pk(@0/**),older(12960),pk(@1/**)))'; + const keypath = "m/48'/1'/0'/3'"; + const otherXpub = 'tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk'; + + const submit = async (event: FormEvent) => { + event.preventDefault(); + setRunning(true); + setResult(''); + setError(undefined); + try { + const rootFingerprint = await bb02.rootFingerprint(); + const ourXpub = await bb02.btcXpub(coin, keypath, 'xpub', false); + const config: bitbox.BtcScriptConfig = { + policy: { + policy, + keys: [ + { rootFingerprint, keypath, xpub: ourXpub }, + { xpub: otherXpub }, + ], + }, + }; + if (!await bb02.btcIsScriptConfigRegistered(coin, config, undefined)) { + await bb02.btcRegisterScriptConfig( + coin, + config, + undefined, + 'autoXpubTpub', + undefined, + ); + } + setResult(await bb02.btcAddress(coin, `${keypath}/0/10`, config, true)); + } catch (err) { + setError(bitbox.ensureError(err)); + } finally { + setRunning(false); + } + }; + + return ( + + Policy address + + Policy: {policy} + Account keypath: {keypath} + Get policy address + + setError(undefined)} /> + + + ); +} + +export function Bitcoin({ bb02 }: Props) { + return ( + <> + + + + + + + + > + ); +} diff --git a/src/index.ts b/src/index.ts index 9e17d71..c8d5cd1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,6 +32,15 @@ import { ethSignTypedMessage as ethSignTypedMessageImpl, ethXpub as ethXpubImpl, } from './internal/eth/methods.js'; +import { + btcAddress as btcAddressImpl, + btcIsScriptConfigRegistered as btcIsScriptConfigRegisteredImpl, + btcRegisterScriptConfig as btcRegisterScriptConfigImpl, + btcSignMessage as btcSignMessageImpl, + btcSignPSBT as btcSignPSBTImpl, + btcXpub as btcXpubImpl, + btcXpubs as btcXpubsImpl, +} from './internal/btc/methods.js'; import { isMultiEdition, type HwwCommunication, type Info } from './internal/hww.js'; import type { NoiseConfig } from './internal/noise-config.js'; import { @@ -597,79 +606,105 @@ export class PairedBitBox { throw notImplementedError('changePassword'); } - /** Compatibility stub: Bitcoin support is not implemented in this TypeScript iteration. */ + /** Retrieves a Bitcoin-family account xpub. */ async btcXpub( - _coin: BtcCoin, - _keypath: Keypath, - _xpub_type: XPubType, - _display: boolean, + coin: BtcCoin, + keypath: Keypath, + xpub_type: XPubType, + display: boolean, ): Promise { - this.#requireOpen('btcXpub'); - throw unsupportedError('btcXpub'); + return this.#runExclusive('btcXpub', open => + btcXpubImpl(open.channel, coin, keypath, xpub_type, display), + ); } - /** Compatibility stub: Bitcoin support is not implemented in this TypeScript iteration. */ + /** Retrieves multiple Bitcoin-family xpubs at once. */ async btcXpubs( - _coin: BtcCoin, - _keypaths: Keypath[], - _xpub_type: BtcXPubsType, + coin: BtcCoin, + keypaths: Keypath[], + xpub_type: BtcXPubsType, ): Promise { - this.#requireOpen('btcXpubs'); - throw unsupportedError('btcXpubs'); + return this.#runExclusive('btcXpubs', open => + btcXpubsImpl(open.channel, open.info, coin, keypaths, xpub_type), + ); } - /** Compatibility stub: Bitcoin support is not implemented in this TypeScript iteration. */ + /** Checks whether a multisig or policy script config is registered. */ async btcIsScriptConfigRegistered( - _coin: BtcCoin, - _script_config: BtcScriptConfig, - _keypath_account?: Keypath, + coin: BtcCoin, + script_config: BtcScriptConfig, + keypath_account?: Keypath, ): Promise { - this.#requireOpen('btcIsScriptConfigRegistered'); - throw unsupportedError('btcIsScriptConfigRegistered'); + return this.#runExclusive('btcIsScriptConfigRegistered', open => + btcIsScriptConfigRegisteredImpl( + open.channel, + coin, + script_config, + keypath_account, + ), + ); } - /** Compatibility stub: Bitcoin support is not implemented in this TypeScript iteration. */ + /** Registers a multisig or policy script config on the device. */ async btcRegisterScriptConfig( - _coin: BtcCoin, - _script_config: BtcScriptConfig, - _keypath_account: Keypath | undefined, - _xpub_type: BtcRegisterXPubType, - _name?: string, + coin: BtcCoin, + script_config: BtcScriptConfig, + keypath_account: Keypath | undefined, + xpub_type: BtcRegisterXPubType, + name?: string, ): Promise { - this.#requireOpen('btcRegisterScriptConfig'); - throw unsupportedError('btcRegisterScriptConfig'); + return this.#runExclusive('btcRegisterScriptConfig', open => + btcRegisterScriptConfigImpl( + open.channel, + coin, + script_config, + keypath_account, + xpub_type, + name, + ), + ); } - /** Compatibility stub: Bitcoin support is not implemented in this TypeScript iteration. */ + /** Retrieves a Bitcoin-family address at the provided keypath. */ async btcAddress( - _coin: BtcCoin, - _keypath: Keypath, - _script_config: BtcScriptConfig, - _display: boolean, + coin: BtcCoin, + keypath: Keypath, + script_config: BtcScriptConfig, + display: boolean, ): Promise { - this.#requireOpen('btcAddress'); - throw unsupportedError('btcAddress'); + return this.#runExclusive('btcAddress', open => + btcAddressImpl(open.channel, coin, keypath, script_config, display), + ); } - /** Compatibility stub: Bitcoin support is not implemented in this TypeScript iteration. */ + /** Signs a base64-encoded PSBT and returns the updated PSBT. */ async btcSignPSBT( - _coin: BtcCoin, - _psbt: string, - _force_script_config: BtcScriptConfigWithKeypath | undefined, - _format_unit: BtcFormatUnit, + coin: BtcCoin, + psbt: string, + force_script_config: BtcScriptConfigWithKeypath | undefined, + format_unit: BtcFormatUnit, ): Promise { - this.#requireOpen('btcSignPSBT'); - throw unsupportedError('btcSignPSBT'); + return this.#runExclusive('btcSignPSBT', open => + btcSignPSBTImpl( + open.channel, + open.info, + coin, + psbt, + force_script_config, + format_unit, + ), + ); } - /** Compatibility stub: Bitcoin support is not implemented in this TypeScript iteration. */ + /** Signs a message using the provided Bitcoin script config. */ async btcSignMessage( - _coin: BtcCoin, - _script_config: BtcScriptConfigWithKeypath, - _msg: Uint8Array, + coin: BtcCoin, + script_config: BtcScriptConfigWithKeypath, + msg: Uint8Array, ): Promise { - this.#requireOpen('btcSignMessage'); - throw unsupportedError('btcSignMessage'); + return this.#runExclusive('btcSignMessage', open => + btcSignMessageImpl(open.channel, open.info, coin, script_config, msg), + ); } /** Does this device support ETH functionality? Currently this means BitBox02 Multi or Nova Multi. */ diff --git a/src/internal/btc/config.ts b/src/internal/btc/config.ts new file mode 100644 index 0000000..3d94228 --- /dev/null +++ b/src/internal/btc/config.ts @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { create } from '@bufbuild/protobuf'; +import { secp256k1 } from '@noble/curves/secp256k1'; +import bs58check from 'bs58check'; +import type { + BtcCoin, + BtcFormatUnit, + BtcRegisterXPubType, + BtcScriptConfig, + BtcScriptConfigWithKeypath, + BtcXPubsType, + Keypath, + XPubType, +} from '../../index.js'; +import { + BTCCoin, + BTCPubRequest_XPubType, + BTCRegisterScriptConfigRequest_XPubType, + BTCScriptConfig_MultisigSchema, + BTCScriptConfig_Multisig_ScriptType, + BTCScriptConfig_PolicySchema, + BTCScriptConfig_SimpleType, + BTCScriptConfigSchema, + BTCScriptConfigWithKeypathSchema, + BTCSignInitRequest_FormatUnit, + BTCXpubsRequest_XPubType, + type BTCScriptConfig, + type BTCScriptConfigWithKeypath, +} from '../../proto/gen/btc_pb.js'; +import { + KeyOriginInfoSchema, + XPubSchema, +} from '../../proto/gen/common_pb.js'; +import { invalidTypeError } from '../errors.js'; +import { parseKeypath } from '../keypath.js'; +import { hexToBytes, validateUint32 } from '../utils.js'; + +const XPUB_MAINNET_VERSION = 0x0488b21e; +const XPUB_TESTNET_VERSION = 0x043587cf; + +function invalid(detail: string): never { + throw invalidTypeError(detail); +} + +export function mapCoin(coin: BtcCoin): BTCCoin { + switch (coin) { + case 'btc': return BTCCoin.BTC; + case 'tbtc': return BTCCoin.TBTC; + case 'ltc': return BTCCoin.LTC; + case 'tltc': return BTCCoin.TLTC; + case 'rbtc': return BTCCoin.RBTC; + default: return invalid('wrong type for BtcCoin'); + } +} + +export function mapXpubType(xpubType: XPubType): BTCPubRequest_XPubType { + switch (xpubType) { + case 'tpub': return BTCPubRequest_XPubType.TPUB; + case 'xpub': return BTCPubRequest_XPubType.XPUB; + case 'ypub': return BTCPubRequest_XPubType.YPUB; + case 'zpub': return BTCPubRequest_XPubType.ZPUB; + case 'vpub': return BTCPubRequest_XPubType.VPUB; + case 'upub': return BTCPubRequest_XPubType.UPUB; + case 'Vpub': return BTCPubRequest_XPubType.CAPITAL_VPUB; + case 'Zpub': return BTCPubRequest_XPubType.CAPITAL_ZPUB; + case 'Upub': return BTCPubRequest_XPubType.CAPITAL_UPUB; + case 'Ypub': return BTCPubRequest_XPubType.CAPITAL_YPUB; + default: return invalid('wrong type for XPubType'); + } +} + +export function mapXpubsType(xpubType: BtcXPubsType): BTCXpubsRequest_XPubType { + switch (xpubType) { + case 'xpub': return BTCXpubsRequest_XPubType.XPUB; + case 'tpub': return BTCXpubsRequest_XPubType.TPUB; + default: return invalid('wrong type for BTCXPubsType'); + } +} + +export function mapFormatUnit(formatUnit: BtcFormatUnit): BTCSignInitRequest_FormatUnit { + switch (formatUnit) { + case 'default': return BTCSignInitRequest_FormatUnit.DEFAULT; + case 'sat': return BTCSignInitRequest_FormatUnit.SAT; + default: return invalid('wrong type for BtcFormatUnit'); + } +} + +export function mapRegisterXpubType( + xpubType: BtcRegisterXPubType, +): BTCRegisterScriptConfigRequest_XPubType { + switch (xpubType) { + case 'autoElectrum': return BTCRegisterScriptConfigRequest_XPubType.AUTO_ELECTRUM; + case 'autoXpubTpub': return BTCRegisterScriptConfigRequest_XPubType.AUTO_XPUB_TPUB; + default: return invalid('wrong type for BtcRegisterXPubType'); + } +} + +// bitcoinjs-lib does not expose BIP32 parsing, while the firmware protobuf needs +// the individual fields rather than the Base58Check-encoded xpub. +function parseXpub(value: unknown) { + if (typeof value !== 'string') { + return invalid('wrong type for BtcScriptConfig'); + } + let decoded: Uint8Array; + try { + decoded = bs58check.decode(value); + } catch { + return invalid('wrong type for BtcScriptConfig'); + } + if (decoded.length !== 78) { + return invalid('wrong type for BtcScriptConfig'); + } + const view = new DataView(decoded.buffer, decoded.byteOffset, decoded.byteLength); + const version = view.getUint32(0, false); + const publicKey = decoded.slice(45, 78); + if (version !== XPUB_MAINNET_VERSION && version !== XPUB_TESTNET_VERSION) { + return invalid('wrong type for BtcScriptConfig'); + } + try { + secp256k1.ProjectivePoint.fromHex(publicKey); + } catch { + return invalid('wrong type for BtcScriptConfig'); + } + return create(XPubSchema, { + depth: decoded.slice(4, 5), + parentFingerprint: decoded.slice(5, 9), + childNum: view.getUint32(9, false), + chainCode: decoded.slice(13, 45), + publicKey, + }); +} + +function parseRootFingerprint(value: unknown): Uint8Array { + if (value === undefined) { + return new Uint8Array(); + } + if (typeof value !== 'string' || !/^[0-9a-fA-F]{8}$/.test(value)) { + return invalid('wrong type for BtcScriptConfig'); + } + return hexToBytes(value); +} + +function parseOptionalKeypath(value: unknown): number[] { + if (value === undefined) { + return []; + } + if (typeof value !== 'string' && !Array.isArray(value)) { + return invalid('wrong type for BtcScriptConfig'); + } + return parseKeypath(value as Keypath); +} + +function mapSimpleType(value: unknown): BTCScriptConfig_SimpleType { + switch (value) { + case 'p2wpkhP2sh': return BTCScriptConfig_SimpleType.P2WPKH_P2SH; + case 'p2wpkh': return BTCScriptConfig_SimpleType.P2WPKH; + case 'p2tr': return BTCScriptConfig_SimpleType.P2TR; + default: return invalid('wrong type for BtcScriptConfig'); + } +} + +export function mapScriptConfig(config: BtcScriptConfig): BTCScriptConfig { + if (config === null || typeof config !== 'object') { + return invalid('wrong type for BtcScriptConfig'); + } + const value = config as unknown as Record; + if ('simpleType' in value) { + return create(BTCScriptConfigSchema, { + config: { case: 'simpleType', value: mapSimpleType(value.simpleType) }, + }); + } + if ('multisig' in value) { + const multisig = value.multisig; + if (multisig === null || typeof multisig !== 'object') { + return invalid('wrong type for BtcScriptConfig'); + } + const ms = multisig as Record; + if (!Array.isArray(ms.xpubs)) { + return invalid('wrong type for BtcScriptConfig'); + } + let scriptType: BTCScriptConfig_Multisig_ScriptType; + switch (ms.scriptType) { + case 'p2wsh': scriptType = BTCScriptConfig_Multisig_ScriptType.P2WSH; break; + case 'p2wshP2sh': scriptType = BTCScriptConfig_Multisig_ScriptType.P2WSH_P2SH; break; + default: return invalid('wrong type for BtcScriptConfig'); + } + return create(BTCScriptConfigSchema, { + config: { + case: 'multisig', + value: create(BTCScriptConfig_MultisigSchema, { + threshold: validateUint32( + ms.threshold as number, + 'wrong type for BtcScriptConfig', + ), + xpubs: ms.xpubs.map(parseXpub), + ourXpubIndex: validateUint32( + ms.ourXpubIndex as number, + 'wrong type for BtcScriptConfig', + ), + scriptType, + }), + }, + }); + } + if ('policy' in value) { + const policy = value.policy; + if (policy === null || typeof policy !== 'object') { + return invalid('wrong type for BtcScriptConfig'); + } + const p = policy as Record; + if (typeof p.policy !== 'string' || !Array.isArray(p.keys)) { + return invalid('wrong type for BtcScriptConfig'); + } + return create(BTCScriptConfigSchema, { + config: { + case: 'policy', + value: create(BTCScriptConfig_PolicySchema, { + policy: p.policy, + keys: p.keys.map((key) => { + if (key === null || typeof key !== 'object') { + return invalid('wrong type for BtcScriptConfig'); + } + const k = key as Record; + return create(KeyOriginInfoSchema, { + rootFingerprint: parseRootFingerprint(k.rootFingerprint), + keypath: parseOptionalKeypath(k.keypath), + xpub: parseXpub(k.xpub), + }); + }), + }), + }, + }); + } + return invalid('wrong type for BtcScriptConfig'); +} + +export function mapScriptConfigWithKeypath( + value: BtcScriptConfigWithKeypath, +): BTCScriptConfigWithKeypath { + if (value === null || typeof value !== 'object') { + return invalid('wrong type for BtcScriptConfigWithKeypath'); + } + const input = value as unknown as Record; + if (typeof input.keypath !== 'string' && !Array.isArray(input.keypath)) { + return invalid('wrong type for BtcScriptConfigWithKeypath'); + } + return create(BTCScriptConfigWithKeypathSchema, { + scriptConfig: mapScriptConfig(input.scriptConfig as BtcScriptConfig), + keypath: parseKeypath(input.keypath as Keypath), + }); +} + +export function makeSimpleScriptConfigWithKeypath( + simpleType: BTCScriptConfig_SimpleType, + keypath: number[], +): BTCScriptConfigWithKeypath { + return create(BTCScriptConfigWithKeypathSchema, { + scriptConfig: create(BTCScriptConfigSchema, { + config: { case: 'simpleType', value: simpleType }, + }), + keypath, + }); +} diff --git a/src/internal/btc/methods.ts b/src/internal/btc/methods.ts new file mode 100644 index 0000000..4e2defe --- /dev/null +++ b/src/internal/btc/methods.ts @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { create } from '@bufbuild/protobuf'; +import type { + BtcCoin, + BtcFormatUnit, + BtcRegisterXPubType, + BtcScriptConfig, + BtcScriptConfigWithKeypath, + BtcSignMessageSignature, + BtcXPubsType, + Keypath, + XPubType, +} from '../../index.js'; +import { + AntiKleptoHostNonceCommitmentSchema, + AntiKleptoSignatureRequestSchema, +} from '../../proto/gen/antiklepto_pb.js'; +import { + BTCIsScriptConfigRegisteredRequestSchema, + type BTCCoin, + BTCOutputType, + BTCPrevTxInitRequestSchema, + BTCPrevTxInputRequestSchema, + BTCPrevTxOutputRequestSchema, + BTCPubRequestSchema, + BTCRegisterScriptConfigRequestSchema, + BTCScriptConfigRegistrationSchema, + BTCScriptConfig_SimpleType, + BTCSignInitRequestSchema, + BTCSignInputRequestSchema, + BTCSignMessageRequestSchema, + BTCSignNextResponse_Type, + BTCSignOutputRequestSchema, + type BTCSignInitRequest_FormatUnit, + BTCXpubsRequestSchema, + type BTCScriptConfigWithKeypath, +} from '../../proto/gen/btc_pb.js'; +import { KeypathSchema } from '../../proto/gen/common_pb.js'; +import { RequestSchema } from '../../proto/gen/hww_pb.js'; +import { rootFingerprint } from '../device.js'; +import { btcSignError, invalidTypeError } from '../errors.js'; +import { atLeast, parseSemver, type Info } from '../hww.js'; +import { parseKeypath } from '../keypath.js'; +import type { EncryptedChannel } from '../pairing.js'; +import { query, unexpectedResponse } from '../proto-query.js'; +import { requireVersion } from '../version.js'; +import { genHostNonce, hostCommit, verifyEcdsa, verifyEcdsaCompact } from '../eth/antiklepto.js'; +import { + mapCoin, + mapFormatUnit, + mapRegisterXpubType, + mapScriptConfig, + mapScriptConfigWithKeypath, + mapXpubsType, + mapXpubType, +} from './config.js'; +import { addSignatures, parsePsbt, preparePsbt, type Transaction } from './psbt.js'; +import { + queryBtc, + queryBtcSignNext, + queryNestedBtcSignNext, +} from './query.js'; + +async function queryXpub( + channel: EncryptedChannel, + coin: BTCCoin, + keypath: number[], + xpubType: XPubType, + display: boolean, +): Promise { + const response = await query(channel, create(RequestSchema, { + request: { + case: 'btcPub', + value: create(BTCPubRequestSchema, { + coin, + keypath, + output: { case: 'xpubType', value: mapXpubType(xpubType) }, + display, + }), + }, + })); + if (response.response.case !== 'pub') { + throw unexpectedResponse(); + } + return response.response.value.pub; +} + +export async function btcXpub( + channel: EncryptedChannel, + coin: BtcCoin, + keypath: Keypath, + xpubType: XPubType, + display: boolean, +): Promise { + return queryXpub(channel, mapCoin(coin), parseKeypath(keypath), xpubType, display); +} + +export async function btcXpubs( + channel: EncryptedChannel, + info: Info, + coin: BtcCoin, + keypaths: Keypath[], + xpubType: BtcXPubsType, +): Promise { + const parsedKeypaths = keypaths.map(parseKeypath); + const mappedCoin = mapCoin(coin); + const mappedType = mapXpubsType(xpubType); + if (!atLeast(parseSemver(info.version), { major: 9, minor: 24, patch: 0 })) { + const xpubs: string[] = []; + for (const keypath of parsedKeypaths) { + xpubs.push(await queryXpub(channel, mappedCoin, keypath, xpubType, false)); + } + return xpubs; + } + + const response = await queryBtc(channel, { + case: 'xpubs', + value: create(BTCXpubsRequestSchema, { + coin: mappedCoin, + xpubType: mappedType, + keypaths: parsedKeypaths.map(keypath => create(KeypathSchema, { keypath })), + }), + }); + if (response.case !== 'pubs') { + throw unexpectedResponse(); + } + return response.value.pubs; +} + +function registration( + coin: BtcCoin, + scriptConfig: BtcScriptConfig, + keypathAccount: Keypath | undefined, +) { + return create(BTCScriptConfigRegistrationSchema, { + coin: mapCoin(coin), + scriptConfig: mapScriptConfig(scriptConfig), + keypath: keypathAccount === undefined ? [] : parseKeypath(keypathAccount), + }); +} + +export async function btcIsScriptConfigRegistered( + channel: EncryptedChannel, + coin: BtcCoin, + scriptConfig: BtcScriptConfig, + keypathAccount: Keypath | undefined, +): Promise { + const response = await queryBtc(channel, { + case: 'isScriptConfigRegistered', + value: create(BTCIsScriptConfigRegisteredRequestSchema, { + registration: registration(coin, scriptConfig, keypathAccount), + }), + }); + if (response.case !== 'isScriptConfigRegistered') { + throw unexpectedResponse(); + } + return response.value.isRegistered; +} + +export async function btcRegisterScriptConfig( + channel: EncryptedChannel, + coin: BtcCoin, + scriptConfig: BtcScriptConfig, + keypathAccount: Keypath | undefined, + xpubType: BtcRegisterXPubType, + name: string | undefined, +): Promise { + const response = await queryBtc(channel, { + case: 'registerScriptConfig', + value: create(BTCRegisterScriptConfigRequestSchema, { + registration: registration(coin, scriptConfig, keypathAccount), + name: name ?? '', + xpubType: mapRegisterXpubType(xpubType), + }), + }); + if (response.case !== 'success') { + throw unexpectedResponse(); + } +} + +export async function btcAddress( + channel: EncryptedChannel, + coin: BtcCoin, + keypath: Keypath, + scriptConfig: BtcScriptConfig, + display: boolean, +): Promise { + const response = await query(channel, create(RequestSchema, { + request: { + case: 'btcPub', + value: create(BTCPubRequestSchema, { + coin: mapCoin(coin), + keypath: parseKeypath(keypath), + output: { + case: 'scriptConfig', + value: mapScriptConfig(scriptConfig), + }, + display, + }), + }, + })); + if (response.response.case !== 'pub') { + throw unexpectedResponse(); + } + return response.response.value.pub; +} + +function isTaprootSimple(scriptConfig: BTCScriptConfigWithKeypath): boolean { + return ( + scriptConfig.scriptConfig?.config.case === 'simpleType' && + scriptConfig.scriptConfig.config.value === BTCScriptConfig_SimpleType.P2TR + ); +} + +function isTaprootPolicy(scriptConfig: BTCScriptConfigWithKeypath): boolean { + return ( + scriptConfig.scriptConfig?.config.case === 'policy' && + scriptConfig.scriptConfig.config.value.policy.startsWith('tr(') + ); +} + +function isSchnorr(scriptConfig: BTCScriptConfigWithKeypath): boolean { + return isTaprootSimple(scriptConfig) || isTaprootPolicy(scriptConfig); +} + +function makeHostNonceCommitment(hostNonce: Uint8Array) { + return create(AntiKleptoHostNonceCommitmentSchema, { + commitment: hostCommit(hostNonce), + }); +} + +async function btcSign( + channel: EncryptedChannel, + info: Info, + coin: BTCCoin, + transaction: Transaction, + outputScriptConfigs: BTCScriptConfigWithKeypath[], + outputScriptConfigIndices: (number | undefined)[], + formatUnit: BTCSignInitRequest_FormatUnit, +): Promise { + if ( + outputScriptConfigIndices.length !== 0 && + outputScriptConfigIndices.length !== transaction.outputs.length + ) { + throw btcSignError('output script config indices must match transaction outputs'); + } + requireVersion(info, { major: 9, minor: 4, patch: 0 }); + if ( + transaction.scriptConfigs + .concat(outputScriptConfigs) + .some(isTaprootSimple) + ) { + requireVersion(info, { major: 9, minor: 10, patch: 0 }); + } + if ( + transaction.outputs.some(output => + output.kind === 'external' && output.outputType === BTCOutputType.OP_RETURN) + ) { + requireVersion(info, { major: 9, minor: 24, patch: 0 }); + } + + const signatures: Uint8Array[] = []; + let nextResponse = await queryBtcSignNext(channel, { + case: 'btcSignInit', + value: create(BTCSignInitRequestSchema, { + coin, + scriptConfigs: transaction.scriptConfigs, + outputScriptConfigs, + version: transaction.version, + numInputs: transaction.inputs.length, + numOutputs: transaction.outputs.length, + locktime: transaction.locktime, + formatUnit, + containsSilentPaymentOutputs: false, + }), + }); + + let isInputsPass2 = false; + for (;;) { + switch (nextResponse.type) { + case BTCSignNextResponse_Type.INPUT: { + const inputIndex = nextResponse.index; + const input = transaction.inputs[inputIndex]; + if (input === undefined) { + throw unexpectedResponse(); + } + const scriptConfig = transaction.scriptConfigs[input.scriptConfigIndex]; + if (scriptConfig === undefined) { + throw unexpectedResponse(); + } + const performAntiklepto = isInputsPass2 && !isSchnorr(scriptConfig); + const hostNonce = performAntiklepto ? genHostNonce() : undefined; + nextResponse = await queryBtcSignNext(channel, { + case: 'btcSignInput', + value: create(BTCSignInputRequestSchema, { + prevOutHash: input.prevOutHash, + prevOutIndex: input.prevOutIndex, + prevOutValue: input.prevOutValue, + sequence: input.sequence, + keypath: input.keypath, + scriptConfigIndex: input.scriptConfigIndex, + ...(hostNonce === undefined + ? {} + : { hostNonceCommitment: makeHostNonceCommitment(hostNonce) }), + }), + }); + + if (hostNonce !== undefined) { + if ( + nextResponse.type !== BTCSignNextResponse_Type.HOST_NONCE || + nextResponse.antiKleptoSignerCommitment === undefined + ) { + throw unexpectedResponse(); + } + const signerCommitment = nextResponse.antiKleptoSignerCommitment.commitment; + nextResponse = await queryNestedBtcSignNext(channel, { + case: 'antikleptoSignature', + value: create(AntiKleptoSignatureRequestSchema, { hostNonce }), + }); + if (!nextResponse.hasSignature) { + throw unexpectedResponse(); + } + verifyEcdsaCompact(hostNonce, signerCommitment, nextResponse.signature); + } + + if (isInputsPass2) { + if (!nextResponse.hasSignature) { + throw unexpectedResponse(); + } + signatures.push(nextResponse.signature); + } + if (inputIndex === transaction.inputs.length - 1) { + isInputsPass2 = true; + } + break; + } + case BTCSignNextResponse_Type.PREVTX_INIT: { + const previous = transaction.inputs[nextResponse.index]?.prevTx; + if (previous === undefined) { + throw btcSignError("input's previous transaction required but missing"); + } + nextResponse = await queryNestedBtcSignNext(channel, { + case: 'prevtxInit', + value: create(BTCPrevTxInitRequestSchema, { + version: previous.version, + numInputs: previous.inputs.length, + numOutputs: previous.outputs.length, + locktime: previous.locktime, + }), + }); + break; + } + case BTCSignNextResponse_Type.PREVTX_INPUT: { + const previous = transaction.inputs[nextResponse.index]?.prevTx; + const input = previous?.inputs[nextResponse.prevIndex]; + if (input === undefined) { + throw unexpectedResponse(); + } + nextResponse = await queryNestedBtcSignNext(channel, { + case: 'prevtxInput', + value: create(BTCPrevTxInputRequestSchema, input), + }); + break; + } + case BTCSignNextResponse_Type.PREVTX_OUTPUT: { + const previous = transaction.inputs[nextResponse.index]?.prevTx; + const output = previous?.outputs[nextResponse.prevIndex]; + if (output === undefined) { + throw unexpectedResponse(); + } + nextResponse = await queryNestedBtcSignNext(channel, { + case: 'prevtxOutput', + value: create(BTCPrevTxOutputRequestSchema, output), + }); + break; + } + case BTCSignNextResponse_Type.OUTPUT: { + const outputIndex = nextResponse.index; + const output = transaction.outputs[outputIndex]; + if (output === undefined) { + throw unexpectedResponse(); + } + nextResponse = await queryBtcSignNext(channel, { + case: 'btcSignOutput', + value: output.kind === 'internal' + ? create(BTCSignOutputRequestSchema, { + ours: true, + value: output.value, + keypath: output.keypath, + scriptConfigIndex: output.scriptConfigIndex, + ...(outputScriptConfigIndices[outputIndex] === undefined + ? {} + : { + outputScriptConfigIndex: + outputScriptConfigIndices[outputIndex], + }), + }) + : create(BTCSignOutputRequestSchema, { + ours: false, + type: output.outputType, + value: output.value, + payload: output.payload, + }), + }); + break; + } + case BTCSignNextResponse_Type.DONE: + return signatures; + case BTCSignNextResponse_Type.HOST_NONCE: + case BTCSignNextResponse_Type.PAYMENT_REQUEST: + default: + throw unexpectedResponse(); + } + } +} + +export async function btcSignPSBT( + channel: EncryptedChannel, + info: Info, + coin: BtcCoin, + psbtBase64: string, + forcedScriptConfig: BtcScriptConfigWithKeypath | undefined, + formatUnit: BtcFormatUnit, +): Promise { + const psbt = parsePsbt(psbtBase64); + const mappedCoin = mapCoin(coin); + const mappedScriptConfig = forcedScriptConfig === undefined + ? undefined + : mapScriptConfigWithKeypath(forcedScriptConfig); + const mappedFormatUnit = mapFormatUnit(formatUnit); + // Since v9.15.0, the device accepts any device-owned output as internal, not only change. + requireVersion(info, { major: 9, minor: 15, patch: 0 }); + const fingerprint = await rootFingerprint(channel); + const prepared = preparePsbt(info, fingerprint, psbt, mappedScriptConfig); + const signatures = await btcSign( + channel, + info, + mappedCoin, + prepared.transaction, + prepared.outputScriptConfigs, + prepared.outputScriptConfigIndices, + mappedFormatUnit, + ); + return addSignatures(prepared, signatures); +} + +export async function btcSignMessage( + channel: EncryptedChannel, + info: Info, + coin: BtcCoin, + scriptConfig: BtcScriptConfigWithKeypath, + msg: Uint8Array, +): Promise { + requireVersion(info, { major: 9, minor: 5, patch: 0 }); + if (!(msg instanceof Uint8Array)) { + throw invalidTypeError('wrong type for Uint8Array'); + } + const hostNonce = genHostNonce(); + const response = await queryBtc(channel, { + case: 'signMessage', + value: create(BTCSignMessageRequestSchema, { + coin: mapCoin(coin), + scriptConfig: mapScriptConfigWithKeypath(scriptConfig), + msg, + hostNonceCommitment: makeHostNonceCommitment(hostNonce), + }), + }); + if (response.case !== 'antikleptoSignerCommitment') { + throw unexpectedResponse(); + } + + const signatureResponse = await queryBtc(channel, { + case: 'antikleptoSignature', + value: create(AntiKleptoSignatureRequestSchema, { hostNonce }), + }); + if (signatureResponse.case !== 'signMessage') { + throw unexpectedResponse(); + } + const signature = signatureResponse.value.signature; + if (signature.length !== 65) { + throw unexpectedResponse('signature must be 65 bytes'); + } + verifyEcdsa(hostNonce, response.value.commitment, signature); + + const sig = Array.from(signature.slice(0, 64)); + const recid = signature[64]!; + return { + sig, + recid, + // BitBox02 uses only compressed pubkeys. Electrum encodes that as +4. + electrumSig65: [27 + 4 + recid, ...sig], + }; +} diff --git a/src/internal/btc/psbt.ts b/src/internal/btc/psbt.ts new file mode 100644 index 0000000..50134aa --- /dev/null +++ b/src/internal/btc/psbt.ts @@ -0,0 +1,559 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { equals } from '@bufbuild/protobuf'; +import { + opcodes, + payments, + type PaymentCreator, + Psbt, + script as bitcoinScript, + Transaction as BitcoinTransaction, +} from 'bitcoinjs-lib'; +import { + BTCOutputType, + BTCScriptConfig_SimpleType, + BTCScriptConfigWithKeypathSchema, + type BTCScriptConfigWithKeypath, +} from '../../proto/gen/btc_pb.js'; +import { + CODE_PSBT_INVALID_ACCOUNT_KEYPATH, + CODE_PSBT_INVALID_OP_RETURN, + CODE_PSBT_KEY_NOT_FOUND, + CODE_PSBT_KEY_NOT_UNIQUE, + CODE_PSBT_SIGN_ERROR, + CODE_PSBT_UNKNOWN_OUTPUT_TYPE, + btcSignError, + psbtError, + psbtParseError, +} from '../errors.js'; +import type { Info } from '../hww.js'; +import { atLeast, parseSemver } from '../hww.js'; +import { HARDENED, parseKeypath } from '../keypath.js'; +import { hexToBytes } from '../utils.js'; +import { makeSimpleScriptConfigWithKeypath } from './config.js'; + +type PsbtInput = Psbt['data']['inputs'][number]; +type PsbtOutput = Psbt['data']['outputs'][number]; + +const HASH_PAYMENTS: [PaymentCreator, BTCOutputType][] = [ + [payments.p2pkh, BTCOutputType.P2PKH], + [payments.p2sh, BTCOutputType.P2SH], + [payments.p2wpkh, BTCOutputType.P2WPKH], + [payments.p2wsh, BTCOutputType.P2WSH], +]; + +export type OurKey = + | { kind: 'segwit'; pubkey: Uint8Array; keypath: number[] } + | { kind: 'taprootInternal'; keypath: number[] } + | { + kind: 'taprootScript'; + pubkey: Uint8Array; + leafHash: Uint8Array; + keypath: number[]; + }; + +export type PrevTx = { + version: number; + inputs: { + prevOutHash: Uint8Array; + prevOutIndex: number; + signatureScript: Uint8Array; + sequence: number; + }[]; + outputs: { value: bigint; pubkeyScript: Uint8Array }[]; + locktime: number; +}; + +export type TxInput = { + prevOutHash: Uint8Array; + prevOutIndex: number; + prevOutValue: bigint; + sequence: number; + keypath: number[]; + scriptConfigIndex: number; + prevTx?: PrevTx; +}; + +export type TxOutput = + | { + kind: 'internal'; + keypath: number[]; + value: bigint; + scriptConfigIndex: number; + } + | { + kind: 'external'; + payload: Uint8Array; + outputType: BTCOutputType; + value: bigint; + }; + +export type Transaction = { + scriptConfigs: BTCScriptConfigWithKeypath[]; + version: number; + inputs: TxInput[]; + outputs: TxOutput[]; + locktime: number; +}; + +export type PreparedPsbt = { + psbt: Psbt; + transaction: Transaction; + ourKeys: OurKey[]; + outputScriptConfigs: BTCScriptConfigWithKeypath[]; + outputScriptConfigIndices: (number | undefined)[]; +}; + +function bytesEqual(left: Uint8Array, right: Uint8Array): boolean { + if (left.length !== right.length) { + return false; + } + return left.every((byte, index) => byte === right[index]); +} + +function derivationPath(path: string): number[] { + return path === 'm' ? [] : parseKeypath(path); +} + +function findOurKey( + ourRootFingerprint: Uint8Array, + outputInfo: PsbtInput | PsbtOutput, +): OurKey { + for (const derivation of outputInfo.tapBip32Derivation ?? []) { + if (!bytesEqual(derivation.masterFingerprint, ourRootFingerprint)) { + continue; + } + // TODO: check for fingerprint collision. + const keypath = derivationPath(derivation.path); + if ( + outputInfo.tapInternalKey !== undefined && + bytesEqual(outputInfo.tapInternalKey, derivation.pubkey) + ) { + if (derivation.leafHashes.length !== 0) { + // We do not support the same key as both the internal key and a leaf key. + throw psbtError( + CODE_PSBT_KEY_NOT_UNIQUE, + 'Taproot pubkeys must be unique across the internal key and all leaf scripts.', + ); + } + return { kind: 'taprootInternal', keypath }; + } + // BIP-388 makes pubkeys unique, so our key cannot occur in multiple leaves. + if (derivation.leafHashes.length !== 1) { + throw psbtError( + CODE_PSBT_KEY_NOT_UNIQUE, + 'Taproot pubkeys must be unique across the internal key and all leaf scripts.', + ); + } + return { + kind: 'taprootScript', + pubkey: derivation.pubkey, + leafHash: derivation.leafHashes[0]!, + keypath, + }; + } + + for (const derivation of outputInfo.bip32Derivation ?? []) { + if (bytesEqual(derivation.masterFingerprint, ourRootFingerprint)) { + // TODO: check for fingerprint collision. + return { + kind: 'segwit', + pubkey: derivation.pubkey, + keypath: derivationPath(derivation.path), + }; + } + } + throw psbtError(CODE_PSBT_KEY_NOT_FOUND, 'Could not find our key in an input.'); +} + +function paymentHash( + payment: PaymentCreator, + script: Uint8Array, +): Uint8Array | undefined { + try { + return payment({ output: script }).hash; + } catch { + return undefined; + } +} + +function p2trPayload(script: Uint8Array): Uint8Array | undefined { + // Parsing a p2tr payment otherwise requires bitcoinjs-lib's optional ECC backend. + const chunks = bitcoinScript.decompile(script); + if ( + chunks?.length === 2 && + chunks[0] === opcodes.OP_1 && + chunks[1] instanceof Uint8Array && + chunks[1].length === 32 && + bytesEqual(bitcoinScript.compile(chunks), script) + ) { + return chunks[1]; + } + return undefined; +} + +function invalidOpReturn(detail: string): never { + throw psbtError(CODE_PSBT_INVALID_OP_RETURN, `Invalid OP_RETURN script: ${detail}`); +} + +function readOpReturnPush(script: Uint8Array): Uint8Array { + const chunks = bitcoinScript.decompile(script); + if (chunks === null) { + return invalidOpReturn('failed to parse OP_RETURN payload'); + } + if (chunks.length === 1) { + return invalidOpReturn('naked OP_RETURN is not supported'); + } + if (chunks.length !== 2) { + return invalidOpReturn('only one data push supported after OP_RETURN'); + } + let canonical: Uint8Array; + try { + canonical = bitcoinScript.compile(chunks); + } catch { + return invalidOpReturn('failed to parse OP_RETURN payload'); + } + if (!bytesEqual(canonical, script)) { + return invalidOpReturn('failed to parse OP_RETURN payload'); + } + const payload = chunks[1]; + if (payload === opcodes.OP_0) { + return new Uint8Array(); + } + if (!(payload instanceof Uint8Array)) { + return invalidOpReturn('no data push found after OP_RETURN'); + } + return payload; +} + +export function payloadFromPkscript( + script: Uint8Array, +): { data: Uint8Array; outputType: BTCOutputType } { + for (const [payment, outputType] of HASH_PAYMENTS) { + const data = paymentHash(payment, script); + if (data !== undefined) { + return { data, outputType }; + } + } + const p2tr = p2trPayload(script); + if (p2tr !== undefined) { + return { data: p2tr, outputType: BTCOutputType.P2TR }; + } + const chunks = bitcoinScript.decompile(script); + if (chunks?.[0] === opcodes.OP_RETURN) { + const payload = readOpReturnPush(script); + return { data: payload, outputType: BTCOutputType.OP_RETURN }; + } + throw psbtError( + CODE_PSBT_UNKNOWN_OUTPUT_TYPE, + 'Unrecognized/unsupported output type.', + ); +} + +function hardenedPrefix(keypath: number[]): number[] { + const firstUnhardened = keypath.findIndex(element => element < HARDENED); + return keypath.slice(0, firstUnhardened === -1 ? undefined : firstUnhardened); +} + +function scriptConfigFromUtxo( + script: Uint8Array, + keypath: number[], + redeemScript: Uint8Array | undefined, +): BTCScriptConfigWithKeypath { + const accountKeypath = hardenedPrefix(keypath); + if (paymentHash(payments.p2wpkh, script) !== undefined) { + return makeSimpleScriptConfigWithKeypath( + BTCScriptConfig_SimpleType.P2WPKH, + accountKeypath, + ); + } + if ( + paymentHash(payments.p2sh, script) !== undefined && + redeemScript !== undefined && + paymentHash(payments.p2wpkh, redeemScript) !== undefined + ) { + return makeSimpleScriptConfigWithKeypath( + BTCScriptConfig_SimpleType.P2WPKH_P2SH, + accountKeypath, + ); + } + if (p2trPayload(script) !== undefined) { + return makeSimpleScriptConfigWithKeypath( + BTCScriptConfig_SimpleType.P2TR, + accountKeypath, + ); + } + // Multisig and policy configs cannot currently be inferred and must be forced. + throw psbtError( + CODE_PSBT_UNKNOWN_OUTPUT_TYPE, + 'Unrecognized/unsupported output type.', + ); +} + +function parseTransaction(raw: Uint8Array): BitcoinTransaction { + try { + return BitcoinTransaction.fromBuffer(raw); + } catch (error) { + throw psbtError( + CODE_PSBT_SIGN_ERROR, + error instanceof Error ? error.message : String(error), + ); + } +} + +function previousTransaction(transaction: BitcoinTransaction): PrevTx { + return { + version: transaction.version >>> 0, + inputs: transaction.ins.map(input => ({ + prevOutHash: input.hash, + prevOutIndex: input.index, + signatureScript: input.script, + sequence: input.sequence, + })), + outputs: transaction.outs.map(output => ({ + value: output.value, + pubkeyScript: output.script, + })), + locktime: transaction.locktime, + }; +} + +function spendUtxo( + inputIndex: number, + txInput: Psbt['txInputs'][number], + psbtInput: PsbtInput, + previous: BitcoinTransaction | undefined, +): { value: bigint; script: Uint8Array } { + if (psbtInput.witnessUtxo !== undefined) { + return psbtInput.witnessUtxo; + } + if (previous === undefined) { + throw psbtError(CODE_PSBT_SIGN_ERROR, 'missing spend utxo in PSBT'); + } + if (!bytesEqual(previous.getHash(), txInput.hash)) { + throw psbtError( + CODE_PSBT_SIGN_ERROR, + `non-witness UTXO hash for input ${inputIndex} does not match the prevout`, + ); + } + const output = previous.outs[txInput.index]; + if (output === undefined) { + throw psbtError(CODE_PSBT_SIGN_ERROR, 'previous output index is out of bounds'); + } + return output; +} + +function isSimpleScriptConfig(config: BTCScriptConfigWithKeypath): boolean { + return config.scriptConfig?.config.case === 'simpleType'; +} + +function isSameAccount( + inputScriptConfigs: BTCScriptConfigWithKeypath[], + outputScriptConfig: BTCScriptConfigWithKeypath, +): boolean { + for (const inputScriptConfig of inputScriptConfigs) { + if (isSimpleScriptConfig(inputScriptConfig)) { + const inputAccount = inputScriptConfig.keypath[2]; + const outputAccount = outputScriptConfig.keypath[2]; + if (inputAccount === undefined || outputAccount === undefined) { + throw psbtError( + CODE_PSBT_INVALID_ACCOUNT_KEYPATH, + 'Account script configs must contain a BIP44 account keypath.', + ); + } + if (inputAccount !== outputAccount) { + return false; + } + } else if ( + !equals( + BTCScriptConfigWithKeypathSchema, + inputScriptConfig, + outputScriptConfig, + ) + ) { + return false; + } + } + return true; +} + +function findOrAddScriptConfig( + configs: BTCScriptConfigWithKeypath[], + config: BTCScriptConfigWithKeypath, +): number { + const index = configs.findIndex(existing => + equals(BTCScriptConfigWithKeypathSchema, existing, config)); + if (index !== -1) { + return index; + } + configs.push(config); + return configs.length - 1; +} + +export function parsePsbt(value: string): Psbt { + try { + return Psbt.fromBase64(value); + } catch (error) { + throw psbtParseError(error instanceof Error ? error.message : String(error)); + } +} + +export function preparePsbt( + info: Info, + rootFingerprint: string, + psbt: Psbt, + forcedScriptConfig: BTCScriptConfigWithKeypath | undefined, +): PreparedPsbt { + const ourRootFingerprint = hexToBytes(rootFingerprint); + const isScriptConfigForced = forcedScriptConfig !== undefined; + const scriptConfigs = forcedScriptConfig === undefined ? [] : [forcedScriptConfig]; + const outputScriptConfigs: BTCScriptConfigWithKeypath[] = []; + const supportsSeparateOutputConfigs = atLeast( + parseSemver(info.version), + { major: 9, minor: 22, patch: 0 }, + ); + const ourKeys: OurKey[] = []; + const inputs: TxInput[] = []; + + for (let inputIndex = 0; inputIndex < psbt.txInputs.length; inputIndex += 1) { + const txInput = psbt.txInputs[inputIndex]!; + const psbtInput = psbt.data.inputs[inputIndex]!; + const previous = psbtInput.nonWitnessUtxo === undefined + ? undefined + : parseTransaction(psbtInput.nonWitnessUtxo); + const utxo = spendUtxo(inputIndex, txInput, psbtInput, previous); + const ourKey = findOurKey(ourRootFingerprint, psbtInput); + const scriptConfigIndex = isScriptConfigForced + ? 0 + : findOrAddScriptConfig( + scriptConfigs, + scriptConfigFromUtxo(utxo.script, ourKey.keypath, psbtInput.redeemScript), + ); + inputs.push({ + prevOutHash: txInput.hash, + prevOutIndex: txInput.index, + prevOutValue: utxo.value, + sequence: txInput.sequence ?? BitcoinTransaction.DEFAULT_SEQUENCE, + keypath: ourKey.keypath, + scriptConfigIndex, + ...(previous === undefined ? {} : { prevTx: previousTransaction(previous) }), + }); + ourKeys.push(ourKey); + } + + const outputs: TxOutput[] = []; + const outputScriptConfigIndices: (number | undefined)[] = []; + for (let outputIndex = 0; outputIndex < psbt.txOutputs.length; outputIndex += 1) { + const txOutput = psbt.txOutputs[outputIndex]!; + const psbtOutput = psbt.data.outputs[outputIndex]!; + let ourKey: OurKey | undefined; + try { + ourKey = findOurKey(ourRootFingerprint, psbtOutput); + } catch { + // Missing output key origin information means this is an external output. + } + if (ourKey === undefined) { + const payload = payloadFromPkscript(txOutput.script); + outputs.push({ + kind: 'external', + payload: payload.data, + outputType: payload.outputType, + value: txOutput.value, + }); + outputScriptConfigIndices.push(undefined); + continue; + } + + const scriptConfig = isScriptConfigForced + ? scriptConfigs[0]! + : scriptConfigFromUtxo( + txOutput.script, + ourKey.keypath, + psbtOutput.redeemScript, + ); + if (isSameAccount(scriptConfigs, scriptConfig)) { + outputs.push({ + kind: 'internal', + keypath: ourKey.keypath, + value: txOutput.value, + scriptConfigIndex: findOrAddScriptConfig(scriptConfigs, scriptConfig), + }); + outputScriptConfigIndices.push(undefined); + } else if (supportsSeparateOutputConfigs) { + outputs.push({ + kind: 'internal', + keypath: ourKey.keypath, + value: txOutput.value, + scriptConfigIndex: 0, + }); + outputScriptConfigIndices.push( + findOrAddScriptConfig(outputScriptConfigs, scriptConfig), + ); + } else { + const payload = payloadFromPkscript(txOutput.script); + outputs.push({ + kind: 'external', + payload: payload.data, + outputType: payload.outputType, + value: txOutput.value, + }); + outputScriptConfigIndices.push(undefined); + } + } + + return { + psbt, + transaction: { + scriptConfigs, + version: psbt.version >>> 0, + inputs, + outputs, + locktime: psbt.locktime, + }, + ourKeys, + outputScriptConfigs, + outputScriptConfigIndices, + }; +} + +export function addSignatures( + prepared: PreparedPsbt, + signatures: Uint8Array[], +): string { + if (signatures.length !== prepared.ourKeys.length) { + throw btcSignError('signature count must match transaction inputs'); + } + signatures.forEach((signature, inputIndex) => { + const input = prepared.psbt.data.inputs[inputIndex]!; + const ourKey = prepared.ourKeys[inputIndex]!; + switch (ourKey.kind) { + case 'segwit': { + input.partialSig = [ + ...(input.partialSig ?? []).filter(item => !bytesEqual(item.pubkey, ourKey.pubkey)), + { + pubkey: ourKey.pubkey, + signature: bitcoinScript.signature.encode( + signature, + BitcoinTransaction.SIGHASH_ALL, + ), + }, + ]; + break; + } + case 'taprootInternal': + input.tapKeySig = signature; + break; + case 'taprootScript': + input.tapScriptSig = [ + ...(input.tapScriptSig ?? []).filter(item => + !( + bytesEqual(item.pubkey, ourKey.pubkey) && + bytesEqual(item.leafHash, ourKey.leafHash) + )), + { pubkey: ourKey.pubkey, leafHash: ourKey.leafHash, signature }, + ]; + break; + } + }); + return prepared.psbt.toBase64(); +} diff --git a/src/internal/btc/query.ts b/src/internal/btc/query.ts new file mode 100644 index 0000000..4266886 --- /dev/null +++ b/src/internal/btc/query.ts @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { create } from '@bufbuild/protobuf'; +import { + BTCRequestSchema, + type BTCRequest, + type BTCResponse, + type BTCSignNextResponse, +} from '../../proto/gen/btc_pb.js'; +import { + RequestSchema, + type Request, +} from '../../proto/gen/hww_pb.js'; +import type { EncryptedChannel } from '../pairing.js'; +import { query, unexpectedResponse } from '../proto-query.js'; + +export async function queryBtc( + channel: EncryptedChannel, + btcRequest: BTCRequest['request'], +): Promise { + const wrapped = create(RequestSchema, { + request: { + case: 'btc', + value: create(BTCRequestSchema, { request: btcRequest }), + }, + }); + const response = await query(channel, wrapped); + if (response.response.case !== 'btc') { + throw unexpectedResponse(); + } + if (response.response.value.response.case === undefined) { + throw unexpectedResponse('BitBox returned an empty Bitcoin response'); + } + return response.response.value.response; +} + +export async function queryBtcSignNext( + channel: EncryptedChannel, + request: Request['request'], +): Promise { + const response = await query(channel, create(RequestSchema, { request })); + if (response.response.case !== 'btcSignNext') { + throw unexpectedResponse(); + } + return response.response.value; +} + +export async function queryNestedBtcSignNext( + channel: EncryptedChannel, + request: BTCRequest['request'], +): Promise { + const response = await queryBtc(channel, request); + if (response.case !== 'signNext') { + throw unexpectedResponse(); + } + return response.value; +} diff --git a/src/internal/errors.ts b/src/internal/errors.ts index 98d717f..fe44508 100644 --- a/src/internal/errors.ts +++ b/src/internal/errors.ts @@ -41,12 +41,14 @@ export interface BitBoxError { /** @internal */ export const CODE_BITBOX_NOISE_ENCRYPT = 'bitbox-noise-encrypt'; /** @internal */ export const CODE_BITBOX_NOISE_DECRYPT = 'bitbox-noise-decrypt'; -// PSBT error codes kept here for the public error surface, even before BTC support lands. +// PSBT error codes exposed by Bitcoin signing. /** @internal */ export const CODE_PSBT_SIGN_ERROR = 'psbt-sign-error'; /** @internal */ export const CODE_PSBT_KEY_NOT_UNIQUE = 'psbt-key-not-unique'; /** @internal */ export const CODE_PSBT_KEY_NOT_FOUND = 'psbt-key-not-found'; /** @internal */ export const CODE_PSBT_UNKNOWN_OUTPUT_TYPE = 'psbt-unknown-output-type'; /** @internal */ export const CODE_PSBT_INVALID_OP_RETURN = 'psbt-invalid-op-return'; +/** @internal */ export const CODE_PSBT_INVALID_ACCOUNT_KEYPATH = + 'psbt-invalid-account-keypath'; // TS compatibility codes for methods or lifecycle states without a device roundtrip. /** @internal */ export const CODE_UNSUPPORTED = 'unsupported'; @@ -137,6 +139,21 @@ export function chainIdTooLargeError(chainId: bigint): TypedError { ); } +/** @internal */ +export function btcSignError(detail: string): TypedError { + return makeError(CODE_BTC_SIGN, `Bitcoin transaction signing error: ${detail}`); +} + +/** @internal */ +export function psbtParseError(detail: string): TypedError { + return makeError(CODE_PSBT_PARSE, `PSBT parse error: ${detail}`); +} + +/** @internal */ +export function psbtError(code: string, detail: string): TypedError { + return makeError(code, `PSBT error: ${detail}`); +} + /** @internal */ export function invalidTypeError(detail: string): TypedError { return makeError(CODE_INVALID_TYPE, `invalid JavaScript type: ${detail}`); @@ -199,6 +216,7 @@ const TYPED_PUBLIC_CODES = new Set([ CODE_PSBT_KEY_NOT_FOUND, CODE_PSBT_UNKNOWN_OUTPUT_TYPE, CODE_PSBT_INVALID_OP_RETURN, + CODE_PSBT_INVALID_ACCOUNT_KEYPATH, CODE_UNSUPPORTED, CODE_NOT_IMPLEMENTED, CODE_INVALID_STATE, diff --git a/src/internal/eth/antiklepto.ts b/src/internal/eth/antiklepto.ts index acd5503..243ade6 100644 --- a/src/internal/eth/antiklepto.ts +++ b/src/internal/eth/antiklepto.ts @@ -29,14 +29,11 @@ export function hostCommit(hostNonce: Uint8Array): Uint8Array { return taggedSha256(HOST_COMMIT_TAG, hostNonce); } -export function verifyEcdsa( +function verifyEcdsaInner( hostNonce: Uint8Array, signerCommitment: Uint8Array, signature: Uint8Array, ): void { - if (signature.length !== 65) { - throw antikleptoError('signature must be 65 bytes'); - } let point; try { point = secp256k1.ProjectivePoint.fromHex(signerCommitment); @@ -64,3 +61,25 @@ export function verifyEcdsa( } } } + +export function verifyEcdsa( + hostNonce: Uint8Array, + signerCommitment: Uint8Array, + signature: Uint8Array, +): void { + if (signature.length !== 65) { + throw antikleptoError('signature must be 65 bytes'); + } + verifyEcdsaInner(hostNonce, signerCommitment, signature); +} + +export function verifyEcdsaCompact( + hostNonce: Uint8Array, + signerCommitment: Uint8Array, + signature: Uint8Array, +): void { + if (signature.length !== 64) { + throw antikleptoError('signature must be 64 bytes'); + } + verifyEcdsaInner(hostNonce, signerCommitment, signature); +} diff --git a/test/btc-methods.test.ts b/test/btc-methods.test.ts new file mode 100644 index 0000000..9795702 --- /dev/null +++ b/test/btc-methods.test.ts @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Psbt } from 'bitcoinjs-lib'; +import { describe, expect, it, vi } from 'vitest'; +import { PairedBitBox } from '../src/index.js'; +import type { Info } from '../src/internal/hww.js'; +import type { EncryptedChannel } from '../src/internal/pairing.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const vectors = JSON.parse(readFileSync( + path.join(__dirname, 'data/btc-transaction-test-vectors.json'), + 'utf8', +)) as { vectors: { psbt: { transaction: string } }[] }; +const validPsbt = Psbt.fromHex(vectors.vectors[0]!.psbt.transaction).toBase64(); + +function paired(version = '9.26.0') { + const query = vi.fn(async () => { + throw new Error('unexpected device query'); + }); + const info: Info = { + version, + product: 'bitbox02-multi', + unlocked: true, + initialized: true, + }; + return { + bitbox: new PairedBitBox({ + channel: { query } as EncryptedChannel, + info, + close(): void {}, + }), + query, + }; +} + +describe('Bitcoin host validation', () => { + it('uses the shared keypath parser', async () => { + const { bitbox, query } = paired(); + await expect(bitbox.btcXpub('btc', 'not-a-keypath', 'xpub', false)) + .rejects.toMatchObject({ code: 'keypath-parse' }); + await expect(bitbox.btcAddress( + 'btc', + 'also-not-a-keypath', + { simpleType: 'p2wpkh' }, + false, + )).rejects.toMatchObject({ code: 'keypath-parse' }); + expect(query).not.toHaveBeenCalled(); + }); + + it('keeps invalid BTC discriminants on the invalid-type surface', async () => { + const { bitbox, query } = paired(); + await expect(bitbox.btcXpub( + 'doge' as 'btc', + "m/84'/0'/0'", + 'xpub', + false, + )).rejects.toMatchObject({ code: 'invalid-type' }); + await expect(bitbox.btcAddress( + 'btc', + "m/84'/0'/0'/0/0", + { simpleType: 'legacy' as 'p2wpkh' }, + false, + )).rejects.toMatchObject({ code: 'invalid-type' }); + expect(query).not.toHaveBeenCalled(); + }); + + it('validates the coin in the legacy xpubs fallback with no keypaths', async () => { + const { bitbox, query } = paired('9.23.0'); + await expect(bitbox.btcXpubs('doge' as 'btc', [], 'xpub')) + .rejects.toMatchObject({ code: 'invalid-type' }); + expect(query).not.toHaveBeenCalled(); + }); + + it('rejects non-xpub/tpub versions in script configs', async () => { + const { bitbox, query } = paired(); + await expect(bitbox.btcAddress( + 'btc', + "m/48'/0'/0'/2'/0/0", + { + multisig: { + threshold: 1, + xpubs: [ + 'ypub6WqXiL3fbDK5QNPe3hN4uSVkEvuE8wXoNCcecgggSuKVpU3Kc4fTvhuLgUhtnbAdaTb9gpz5PQdvzcsKPTLgW2CPkF5ZNRzQeKFT4NSc1xN', + ], + ourXpubIndex: 0, + scriptType: 'p2wsh', + }, + }, + false, + )).rejects.toMatchObject({ code: 'invalid-type' }); + expect(query).not.toHaveBeenCalled(); + }); + + it('parses the PSBT before applying the firmware gate', async () => { + const { bitbox, query } = paired('9.14.0'); + await expect(bitbox.btcSignPSBT('btc', 'not a PSBT', undefined, 'default')) + .rejects.toMatchObject({ code: 'psbt-parse' }); + await expect(bitbox.btcSignPSBT('btc', validPsbt, undefined, 'default')) + .rejects.toMatchObject({ + code: 'version', + message: 'firmware version >=9.15.0 required', + }); + expect(query).not.toHaveBeenCalled(); + }); + + it('validates PSBT options before querying the device', async () => { + const { bitbox, query } = paired(); + await expect(bitbox.btcSignPSBT( + 'doge' as 'btc', + validPsbt, + undefined, + 'default', + )).rejects.toMatchObject({ code: 'invalid-type' }); + await expect(bitbox.btcSignPSBT( + 'btc', + validPsbt, + undefined, + 'bits' as 'default', + )).rejects.toMatchObject({ code: 'invalid-type' }); + await expect(bitbox.btcSignPSBT( + 'btc', + validPsbt, + { scriptConfig: { simpleType: 'legacy' as 'p2wpkh' }, keypath: [] }, + 'default', + )).rejects.toMatchObject({ code: 'invalid-type' }); + expect(query).not.toHaveBeenCalled(); + }); + + it('gates message signing before querying the device', async () => { + const { bitbox, query } = paired('9.4.9'); + await expect(bitbox.btcSignMessage( + 'btc', + { + scriptConfig: { simpleType: 'p2wpkh' }, + keypath: "m/84'/0'/0'/0/0", + }, + new TextEncoder().encode('message'), + )).rejects.toMatchObject({ + code: 'version', + message: 'firmware version >=9.5.0 required', + }); + expect(query).not.toHaveBeenCalled(); + }); +}); diff --git a/test/btc-psbt.test.ts b/test/btc-psbt.test.ts new file mode 100644 index 0000000..22ab857 --- /dev/null +++ b/test/btc-psbt.test.ts @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { + address, + initEccLib, + opcodes, + script as bitcoinScript, +} from 'bitcoinjs-lib'; +import { hexToBytes } from '@noble/hashes/utils'; +import * as ecc from 'tiny-secp256k1'; +import { describe, expect, it } from 'vitest'; +import { + parsePsbt, + payloadFromPkscript, +} from '../src/internal/btc/psbt.js'; +import { BTCOutputType } from '../src/proto/gen/btc_pb.js'; + +initEccLib(ecc); + +describe('Bitcoin output scripts', () => { + it.each([ + [ + '1AMZK8xzHJWsuRErpGZTiW4jKz8fdfLUGE', + BTCOutputType.P2PKH, + 20, + ], + [ + '3JFL8CgtV4ZtMFYeP5LgV4JppLkHw5Gw9T', + BTCOutputType.P2SH, + 20, + ], + [ + 'bc1qkl8ms75cq6ajxtny7e88z3u9hkpkvktt5jwh6u', + BTCOutputType.P2WPKH, + 20, + ], + [ + 'bc1q2fhgukymf0caaqrhfxrdju4wm94wwrch2ukntl5fuc0faz8zm49q0h6ss8', + BTCOutputType.P2WSH, + 32, + ], + [ + 'bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr', + BTCOutputType.P2TR, + 32, + ], + ] as const)('parses %s', (encodedAddress, outputType, payloadLength) => { + const payload = payloadFromPkscript(address.toOutputScript(encodedAddress)); + expect(payload.outputType).toBe(outputType); + expect(payload.data).toHaveLength(payloadLength); + }); + + it('parses empty, direct-push, and PUSHDATA1 OP_RETURN outputs', () => { + expect(payloadFromPkscript(hexToBytes('6a00'))).toEqual({ + data: new Uint8Array(), + outputType: BTCOutputType.OP_RETURN, + }); + expect(payloadFromPkscript(hexToBytes('6a03aabbcc'))).toEqual({ + data: hexToBytes('aabbcc'), + outputType: BTCOutputType.OP_RETURN, + }); + const eightyBytes = new Uint8Array(80).fill(0xaa); + expect(payloadFromPkscript(bitcoinScript.compile([ + opcodes.OP_RETURN, + eightyBytes, + ]))).toEqual({ + data: eightyBytes, + outputType: BTCOutputType.OP_RETURN, + }); + }); + + it.each([ + ['6a', 'naked OP_RETURN'], + ['6a6a', 'no data push'], + ['6a0000', 'only one data push'], + ['6a4c03aabbcc', 'failed to parse'], + ])('rejects invalid OP_RETURN script %s', (script, message) => { + try { + payloadFromPkscript(hexToBytes(script)); + throw new Error('expected payload parsing to fail'); + } catch (error) { + expect(error).toMatchObject({ + code: 'psbt-invalid-op-return', + }); + expect((error as Error).message).toContain(message); + } + }); + + it('rejects unknown output scripts', () => { + expect(() => payloadFromPkscript(hexToBytes('51'))).toThrowError(); + try { + payloadFromPkscript(hexToBytes('51')); + } catch (error) { + expect(error).toMatchObject({ code: 'psbt-unknown-output-type' }); + } + + const nonMinimalP2tr = `514c20${'00'.repeat(32)}`; + expect(() => payloadFromPkscript(hexToBytes(nonMinimalP2tr))).toThrowError(); + }); +}); + +describe('parsePsbt', () => { + it('maps bitcoinjs-lib parser failures to psbt-parse', () => { + try { + parsePsbt('not a PSBT'); + throw new Error('expected PSBT parsing to fail'); + } catch (error) { + expect(error).toMatchObject({ code: 'psbt-parse' }); + } + }); +}); diff --git a/test/data/btc-transaction-test-vectors.json b/test/data/btc-transaction-test-vectors.json new file mode 100644 index 0000000..43d1d50 --- /dev/null +++ b/test/data/btc-transaction-test-vectors.json @@ -0,0 +1,4858 @@ +{ + "simulator_seed": "boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide", + "vectors": [ + { + "id": "taproot-key-spend", + "description": "Signs two BIP86 Taproot key-spend inputs and recognizes a BIP86 change output.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100b20200000002f8e60a3e44fa93f5d7a7b57dc5983aac5d50f84071772938b4ddff45ce9bf3270000000000fffffffff8e60a3e44fa93f5d7a7b57dc5983aac5d50f84071772938b4ddff45ce9bf3270100000000ffffffff0200e1f50500000000225120cc9235442b0f4fdf2f3c39516207c389e08fed3ae6cfa90cdb820d791a9d6ba4002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001012b00e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a21166c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e19004c00739d56000080010000800000008000000000000000000117206c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e0001012b00e1f5050000000022512036c6a3ef943986e3fdfb7d5a750ae013f746966f6fe0971d3bdc7360f29db42221169e2a4b40d2c021b903ca48aa305545f56716f5c85e998bdc677d968a4057434319004c00739d56000080010000800000008000000000010000000117209e2a4b40d2c021b903ca48aa305545f56716f5c85e998bdc677d968a4057434300010520c5de774ff4c41546478d7e273827d26d2b0908bcfa86cb5861c29f0b05d1f7852107c5de774ff4c41546478d7e273827d26d2b0908bcfa86cb5861c29f0b05d1f78519004c00739d56000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": false, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 400.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "1.00000000 TBTC", + "fee": "0.80000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 400.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "1.00000000 TBTC", + "fee": "0.80000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 400.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "6c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e", + "sighash": "default" + }, + { + "input_index": 1, + "kind": "taproot_key", + "pubkey": "9e2a4b40d2c021b903ca48aa305545f56716f5c85e998bdc677d968a40574343", + "sighash": "default" + } + ] + }, + { + "id": "mixed-spend", + "description": "Signs P2TR, native P2WPKH and nested P2SH-P2WPKH inputs from one previous transaction and recognizes BIP86 change.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100db0200000003e3ba22f94fa7f97d19e4b6c31853ce54c220cf225952b1ba9082ba0c1ba316260000000000ffffffffe3ba22f94fa7f97d19e4b6c31853ce54c220cf225952b1ba9082ba0c1ba316260100000000ffffffffe3ba22f94fa7f97d19e4b6c31853ce54c220cf225952b1ba9082ba0c1ba316260200000000ffffffff0200e1f50500000000225120cc9235442b0f4fdf2f3c39516207c389e08fed3ae6cfa90cdb820d791a9d6ba4002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001009d020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0300e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a2782094400e1f5050000000017a914f566e83e9f22881747e8308ed56afd5fa0abbc66870000000021166c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e19004c00739d56000080010000800000008000000000000000000117206c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e0001009d020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0300e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a2782094400e1f5050000000017a914f566e83e9f22881747e8308ed56afd5fa0abbc668700000000220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d54000080010000800000008000000000000000000001009d020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0300e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a2782094400e1f5050000000017a914f566e83e9f22881747e8308ed56afd5fa0abbc668700000000010416001468a00bbd6de555d756e92815e523b789def6c0b022060329b834de159b7b1ff5d2bcc4b10ba2611169dff448a59df7f0c84c2c54b9b7d0184c00739d310000800100008000000080000000000000000000010520c5de774ff4c41546478d7e273827d26d2b0908bcfa86cb5861c29f0b05d1f7852107c5de774ff4c41546478d7e273827d26d2b0908bcfa86cb5861c29f0b05d1f78519004c00739d56000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 900.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "2.00000000 TBTC", + "fee": "1.80000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 900.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "2.00000000 TBTC", + "fee": "1.80000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 900.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "6c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e", + "sighash": "default" + }, + { + "input_index": 1, + "kind": "ecdsa", + "pubkey": "03a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3", + "sighash": "all" + }, + { + "input_index": 2, + "kind": "ecdsa", + "pubkey": "0329b834de159b7b1ff5d2bcc4b10ba2611169dff448a59df7f0c84c2c54b9b7d0", + "sighash": "all" + } + ] + }, + { + "id": "op-return", + "description": "Signs a P2WPKH spend with a zero-value OP_RETURN output containing one printable data push and a P2WPKH change output.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01006802000000012b769ad9a4c846c82a6005040bbbc22a05ce02e42a6e1787a8ba8e57977bb7080000000000ffffffff0240aeeb02000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e9500000000000000000d6a0b68656c6c6f20776f726c640000000000010052020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0180f0fa0200000000160014bfbb8f964b61fb712f04587acaebea9e103161bc0000000001011f80f0fa0200000000160014bfbb8f964b61fb712f04587acaebea9e103161bc220602e62431dfc2aa92e24d23118f8f078894ee118271ddc13715c5bd33d7d2ffe229184c00739d540000800100008000000080000000000500000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "unsupported", + "unsupported_version": "9.24.0", + "screens": [] + }, + { + "min_version": "9.24.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "OP_RETURN", + "body": "hello world", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.01000000 TBTC", + "fee": "0.01000000 TBTC", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "02e62431dfc2aa92e24d23118f8f078894ee118271ddc13715c5bd33d7d2ffe229", + "sighash": "all" + } + ] + }, + { + "id": "multisig-p2wsh", + "description": "Signs the device branch of a registered 1-of-2 sortedmulti P2WSH input, recognizes descriptor-derived change, and finalizes the witness.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100890200000001189002448bf29a8299625a5e0bbba99c561bd2ea07c71bb55b72be77e00becd60000000000ffffffff02801d2c04000000002200208dd9dc6a4969db2b4ed0a01a078e16c92328ea56ba3854f7416138132d2e6e48002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005e020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0100e1f505000000002200208b7d9944e24b23ca0e5d02a1233611fbaf2faf95bfc17cd35d68a800309352ec000000000105475121024f9153a64648289b50ba2fdf6a20feaf912d05fb00dbc65d53b0ad1ef84ed20921039fb11f1394842eeeadd04ec165f713c49cb875443da2bc05d994ec118d7d1fde52ae2206024f9153a64648289b50ba2fdf6a20feaf912d05fb00dbc65d53b0ad1ef84ed2090c4707983700000000000000002206039fb11f1394842eeeadd04ec165f713c49cb875443da2bc05d994ec118d7d1fde1c4c00739d300000800100008000000080020000800000000000000000000101475121031bc4da3f6dfefcc269e08ac1edfffb1bf928790b0ed888d6047adbbd0ec27acb2103d84603fefb986e343dc535bb6c24c47fdb6c4052c2c36b29598f1aecf5a1074c52ae2202031bc4da3f6dfefcc269e08ac1edfffb1bf928790b0ed888d6047adbbd0ec27acb1c4c00739d300000800100008000000080020000800100000000000000220203d84603fefb986e343dc535bb6c24c47fdb6c4052c2c36b29598f1aecf5a1074c0c4707983701000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "multisig", + "threshold": 1, + "xpubs": [ + "xpub6EhivkawbkKDRT9Z59WwqhAzZZe7NQhxPgYc67nHbagRxoSbKEiZ6x4P4bE6s48GHvAHaGH8hsQfJ4RSSeNbVdYp7Kii2UGWKn8x2bQDtiR", + "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk" + ], + "our_xpub_index": 0, + "script_type": "p2wsh" + }, + "keypath": "m/48'/1'/0'/2'" + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "1-of-2\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test wsh multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "1-of-2\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test wsh multisig", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "1-of-2\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test wsh multisig", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "multisig", + "threshold": 1, + "xpubs": [ + "xpub6EhivkawbkKDRT9Z59WwqhAzZZe7NQhxPgYc67nHbagRxoSbKEiZ6x4P4bE6s48GHvAHaGH8hsQfJ4RSSeNbVdYp7Kii2UGWKn8x2bQDtiR", + "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk" + ], + "our_xpub_index": 0, + "script_type": "p2wsh" + }, + "keypath": "m/48'/1'/0'/2'", + "name": "test wsh multisig" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "039fb11f1394842eeeadd04ec165f713c49cb875443da2bc05d994ec118d7d1fde", + "sighash": "all" + } + ] + }, + { + "id": "policy-wsh", + "description": "Signs the device branch of a registered BIP388 WSH or-policy and recognizes descriptor-derived change.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff010089020000000117bb7376f1aed39ca76fac738a9eecfb490d3d8e0d9cb47a08b227dd04bb7abc0000000000ffffffff02801d2c0400000000220020593d9bd9f01f27f05e9a8ad4096961eb5f1bcd43a6fed6f2df9020fc82673560002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005e020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0100e1f505000000002200204bff256253c47cfb36eb7d58f4e6b97d484701191075ecde1d3332ecdf8111010000000001054821033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2ac7c21024f9153a64648289b50ba2fdf6a20feaf912d05fb00dbc65d53b0ad1ef84ed209ac9b2206024f9153a64648289b50ba2fdf6a20feaf912d05fb00dbc65d53b0ad1ef84ed2090c4707983700000000000000002206033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b21c4c00739d3000008001000080000000800300008000000000000000000001014821028c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44aac7c2103d84603fefb986e343dc535bb6c24c47fdb6c4052c2c36b29598f1aecf5a1074cac9b2202028c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a1c4c00739d300000800100008000000080030000800100000000000000220203d84603fefb986e343dc535bb6c24c47fdb6c4052c2c36b29598f1aecf5a1074c0c4707983701000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "wsh(or_b(pk(@0/<0;1>/*),s:pk(@1/<0;1>/*)))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk" + } + ] + }, + "keypath": "m/48'/1'/0'/3'" + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test wsh policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "wsh(or_b(pk(@0/<0;1>/*),s:pk(@1/<0;1>/*)))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test wsh policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "wsh(or_b(pk(@0/<0;1>/*),s:pk(@1/<0;1>/*)))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test wsh policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "wsh(or_b(pk(@0/<0;1>/*),s:pk(@1/<0;1>/*)))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "wsh(or_b(pk(@0/<0;1>/*),s:pk(@1/<0;1>/*)))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk" + } + ] + }, + "name": "test wsh policy" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2", + "sighash": "all" + } + ] + }, + { + "id": "policy-tr-keyspend", + "description": "Signs the key-spend path of a registered BIP388 Taproot policy using only a witness UTXO and recognizes descriptor-derived change.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100890200000001c0470cb5928e1e538404293a10b5c47e8d2482e27834ec4fb855d2c9e708a1480000000000ffffffff02801d2c04000000002251202cf99a9e98b0ee07ccb287502f76b2f856af59b726a746cd1ab303adc6749129002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001012b00e1f50500000000225120eb86719b281a383608399cab552dbd40368c66cd958f38bcb42347830e729ef021163a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b21d004c00739d3000008001000080000000800300008000000000000000000117203a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2000105208c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a21078c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a1d004c00739d3000008001000080000000800300008001000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "tr(@0/<0;1>/*)", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + } + ] + }, + "keypath": "m/48'/1'/0'/3'" + } + } + }, + "expected_needs_prevtxs": false, + "expectations": [ + { + "max_version_exclusive": "9.21.0", + "outcome": "unsupported", + "unsupported_version": "9.21.0", + "screens": [] + }, + { + "min_version": "9.21.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n1 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test tr keyspend policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/<0;1>/*)", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/1", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n1 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test tr keyspend policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/<0;1>/*)", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/1", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "tr(@0/<0;1>/*)", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + } + ] + }, + "name": "test tr keyspend policy" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "3a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2", + "sighash": "default" + } + ] + }, + { + "id": "policy-tr-scriptspend", + "description": "Signs the script path owned by the device in a registered BIP388 Taproot policy whose internal key belongs to another signer.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01008902000000014625d1da866927cc28dae6171aabba10fdb6abaee03829bc717fa57c05fc6b1c0000000000ffffffff02801d2c04000000002251206ea53e5447e4771e25f4e3ef3e3c5973e00ed2a4f7680507134a797b7df3c3cc002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001012b00e1f50500000000225120d0692388c73a40274236cad74af812db4d49ecf07b0838e644189693c4a1c87f2215c14f9153a64648289b50ba2fdf6a20feaf912d05fb00dbc65d53b0ad1ef84ed20923203a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2acc021163a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b23d01c3aa2951885f3ddabebfba4eb4873dbdb2c41e287562d542cf8782e2a857071b4c00739d30000080010000800000008003000080000000000000000021164f9153a64648289b50ba2fdf6a20feaf912d05fb00dbc65d53b0ad1ef84ed2090d004707983700000000000000000117204f9153a64648289b50ba2fdf6a20feaf912d05fb00dbc65d53b0ad1ef84ed209011820c3aa2951885f3ddabebfba4eb4873dbdb2c41e287562d542cf8782e2a857071b00010520d84603fefb986e343dc535bb6c24c47fdb6c4052c2c36b29598f1aecf5a1074c01062500c022208c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44aac21078c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a3d01bdda447b6076e3cc5aef2434e05b03e88a74dba86599553f3371fac3e86112b74c00739d3000008001000080000000800300008001000000000000002107d84603fefb986e343dc535bb6c24c47fdb6c4052c2c36b29598f1aecf5a1074c0d004707983701000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "tr(@0/<0;1>/*,pk(@1/<0;1>/*))", + "keys": [ + { + "xpub": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk" + }, + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + } + ] + }, + "keypath": "m/48'/1'/0'/3'" + } + } + }, + "expected_needs_prevtxs": false, + "expectations": [ + { + "max_version_exclusive": "9.21.0", + "outcome": "unsupported", + "unsupported_version": "9.21.0", + "screens": [] + }, + { + "min_version": "9.21.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test tr scriptspend policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/<0;1>/*,pk(@1/<0;1>/*))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test tr scriptspend policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/<0;1>/*,pk(@1/<0;1>/*))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "tr(@0/<0;1>/*,pk(@1/<0;1>/*))", + "keys": [ + { + "xpub": "tpubDFgycCkexSxkdZfeyaasDHityE97kiYM1BeCNoivDHvydGugKtoNobt4vEX6YSHNPy2cqmWQHKjKxciJuocepsGPGxcDZVmiMBnxgA1JKQk" + }, + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + } + ] + }, + "name": "test tr scriptspend policy" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_script", + "pubkey": "3a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2", + "leaf_hash": "c3aa2951885f3ddabebfba4eb4873dbdb2c41e287562d542cf8782e2a857071b", + "sighash": "default" + } + ] + }, + { + "id": "taproot-to-non-taproot-change", + "description": "Signs all-Taproot inputs with P2WPKH change, requiring previous transactions for the added non-Taproot output config.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100a60200000002f8e60a3e44fa93f5d7a7b57dc5983aac5d50f84071772938b4ddff45ce9bf3270000000000fffffffff8e60a3e44fa93f5d7a7b57dc5983aac5d50f84071772938b4ddff45ce9bf3270100000000ffffffff0200e1f505000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef140000000000010089020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0200e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a00e1f5050000000022512036c6a3ef943986e3fdfb7d5a750ae013f746966f6fe0971d3bdc7360f29db4220000000001012b00e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a21166c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e19004c00739d56000080010000800000008000000000000000000117206c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e00010089020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0200e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a00e1f5050000000022512036c6a3ef943986e3fdfb7d5a750ae013f746966f6fe0971d3bdc7360f29db4220000000001012b00e1f5050000000022512036c6a3ef943986e3fdfb7d5a750ae013f746966f6fe0971d3bdc7360f29db42221169e2a4b40d2c021b903ca48aa305545f56716f5c85e998bdc677d968a4057434319004c00739d56000080010000800000008000000000010000000117209e2a4b40d2c021b903ca48aa305545f56716f5c85e998bdc677d968a4057434300220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 400.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "1.00000000 TBTC", + "fee": "0.80000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 400.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "1.00000000 TBTC", + "fee": "0.80000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 400.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "6c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e", + "sighash": "default" + }, + { + "input_index": 1, + "kind": "taproot_key", + "pubkey": "9e2a4b40d2c021b903ca48aa305545f56716f5c85e998bdc677d968a40574343", + "sighash": "default" + } + ] + }, + { + "id": "silent-payment", + "description": "Signs mixed inputs with BIP352 metadata and a device-generated silent-payment output.", + "coin": "btc", + "psbt": { + "transaction": "70736274ff0100b9020000000314f5c987750e136e84be08a812ad8273b6597dbd76ded0806e714c183644cdc40000000000ffffffff14f5c987750e136e84be08a812ad8273b6597dbd76ded0806e714c183644cdc40100000000ffffffff14f5c987750e136e84be08a812ad8273b6597dbd76ded0806e714c183644cdc40200000000ffffffff0200e1f50500000000225120aaf8631d8fffabc65d283faa12aedc768bdc669c1ba6ff1166f356f13eefe089002d31010000000000000000000001009d020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0300e1f5050000000022512041ea70bc6cdc9e38c9faa847f0b9e8ce082b3be15fa6d137fbbab6cb138a294300e1f50500000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b00e1f5050000000017a914ee9a3e5e0bedc5ec5c7e2461c62b5f0b5109c4f9870000000001012b00e1f5050000000022512041ea70bc6cdc9e38c9faa847f0b9e8ce082b3be15fa6d137fbbab6cb138a29432116cd47d5291421bd92a7dd849beb7f0d13f1c7c98067e3c23ff98de62c1ff0952719004c00739d5600008000000080000000800000000000000000011720cd47d5291421bd92a7dd849beb7f0d13f1c7c98067e3c23ff98de62c1ff095270001009d020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0300e1f5050000000022512041ea70bc6cdc9e38c9faa847f0b9e8ce082b3be15fa6d137fbbab6cb138a294300e1f50500000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b00e1f5050000000017a914ee9a3e5e0bedc5ec5c7e2461c62b5f0b5109c4f9870000000001011f00e1f50500000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b220602c6f297dd7bcc0a38a527b3f2289cf91816cae2845a605080fd5fc9af4a945048184c00739d54000080000000800000008000000000000000000001009d020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0300e1f5050000000022512041ea70bc6cdc9e38c9faa847f0b9e8ce082b3be15fa6d137fbbab6cb138a294300e1f50500000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b00e1f5050000000017a914ee9a3e5e0bedc5ec5c7e2461c62b5f0b5109c4f9870000000001012000e1f5050000000017a914ee9a3e5e0bedc5ec5c7e2461c62b5f0b5109c4f9870104160014d14ce318a324bc9eb4ff285d94ee02a297c0496f220603d47c62767f5463ffc008b9250fa6bb7eac11f2aff84804cf19d80ea46922bd73184c00739d31000080000000800000008000000000000000000001052067426cdc697740fe8fcedd942ae76b85b22d73db568fc32d7ced988f387f0ee5210767426cdc697740fe8fcedd942ae76b85b22d73db568fc32d7ced988f387f0ee519004c00739d56000080000000800000008001000000000000000000", + "options": { + "outputs": { + "1": { + "silent_payment_address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + } + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.21.0", + "outcome": "unsupported", + "unsupported_version": "9.21.0", + "screens": [] + }, + { + "min_version": "9.21.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + }, + { + "type": "transaction_fee", + "amount": "2.00000000 BTC", + "fee": "1.80000000 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 900.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "sp1q qgst e7k9 hx0q ftg6 qmwl kqtw uy6c ycya vzmz j85c 6qdf hjdp djtd gqju exzk 6mur w56s uy3e 0rd2 cgqv ycxt tddw svgx e2us fpxu mr70 xc9p kqwv" + }, + { + "type": "transaction_fee", + "amount": "2.00000000 BTC", + "fee": "1.80000000 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 900.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "cd47d5291421bd92a7dd849beb7f0d13f1c7c98067e3c23ff98de62c1ff09527", + "sighash": "default" + }, + { + "input_index": 1, + "kind": "ecdsa", + "pubkey": "02c6f297dd7bcc0a38a527b3f2289cf91816cae2845a605080fd5fc9af4a945048", + "sighash": "all" + }, + { + "input_index": 2, + "kind": "ecdsa", + "pubkey": "03d47c62767f5463ffc008b9250fa6bb7eac11f2aff84804cf19d80ea46922bd73", + "sighash": "all" + } + ], + "expected_generated_outputs": { + "1": "5120d826829cb603fc008e5ef99d0818f2126d3569c3ab8a6cd069f07a20e892bd59" + } + }, + { + "id": "send-self-same-account", + "description": "Covers ownership detection for the input account, suppression of change confirmation and version-specific account labels.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100720200000001b68d0167988df1602974d874a0f28f65525c86a88d66a09c92587a19408a30db0000000000ffffffff0280f0fa020000000017a9147946b34fad2af6a7ed141651188fb6d7082b8ef987002d310100000000160014fc6b321f780a2401be6884e5bf84520a27820944000000000001005e020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0100e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a0000000001012b00e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a21166c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e19004c00739d56000080010000800000008000000000000000000117206c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e0001001600142f4e1eff50d5156d90c5a7d62b7ef3825d5b6db7220203a06059ec858225435fd867601375832479a21ccaef653d6a7920a4cee4797e73184c00739d310000800100008000000080010000000000000000220203a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.22.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "This BitBox02: tb1ql34ny8mcpgjqr0ngsnjmlpzjpgncyz2ygh2gye" + }, + { + "type": "transaction_fee", + "amount": "0.50000000 TBTC", + "fee": "0.30000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.22.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "This BitBox (same account): tb1ql34ny8mcpgjqr0ngsnjmlpzjpgncyz2ygh2gye" + }, + { + "type": "transaction_fee", + "amount": "0.50000000 TBTC", + "fee": "0.30000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "This BitBox (same account): tb1q l34n y8mc pgjq r0ng snjm lpzj pgnc yz2y gh2g ye" + }, + { + "type": "transaction_fee", + "amount": "0.50000000 TBTC", + "fee": "0.30000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "6c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e", + "sighash": "default" + } + ] + }, + { + "id": "send-self-different-account", + "description": "Covers ownership detection for another account and version-specific account labels.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100720200000001b68d0167988df1602974d874a0f28f65525c86a88d66a09c92587a19408a30db0000000000ffffffff0280f0fa020000000017a9147946b34fad2af6a7ed141651188fb6d7082b8ef987002d31010000000016001460f1b576c18bdbece2606cac70378f2974aed9ef000000000001005e020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0100e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a0000000001012b00e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a21166c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e19004c00739d56000080010000800000008000000000000000000117206c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e0001001600142f4e1eff50d5156d90c5a7d62b7ef3825d5b6db7220203a06059ec858225435fd867601375832479a21ccaef653d6a7920a4cee4797e73184c00739d310000800100008000000080010000000000000000220203cee733ae6350507a6c63656d45de6dfc98a252a9f242116cfbe6c6ade2626b0c184c00739d540000800100008001000080000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.22.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1qvrcm2akp30d7ecnqdjk8qdu09962ak005rcp6j" + }, + { + "type": "transaction_fee", + "amount": "0.50000000 TBTC", + "fee": "0.30000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.22.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "This BitBox (account #2): tb1qvrcm2akp30d7ecnqdjk8qdu09962ak005rcp6j" + }, + { + "type": "transaction_fee", + "amount": "0.50000000 TBTC", + "fee": "0.30000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "This BitBox (account #2): tb1q vrcm 2akp 30d7 ecnq djk8 qdu0 9962 ak00 5rcp 6j" + }, + { + "type": "transaction_fee", + "amount": "0.50000000 TBTC", + "fee": "0.30000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "6c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e", + "sighash": "default" + } + ] + }, + { + "id": "payment-request", + "description": "Covers signed SLIP-24 payment-request metadata, merchant and multiline memo screens, address suppression and the pre-v9.24 simulator merchant limitation.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100720200000001b68d0167988df1602974d874a0f28f65525c86a88d66a09c92587a19408a30db0000000000ffffffff0280f0fa020000000017a9147946b34fad2af6a7ed141651188fb6d7082b8ef987002d3101000000001600142d997091b1574170c30720dbba9d06a367d68351000000000001005e020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0100e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a0000000001012b00e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a21166c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e19004c00739d56000080010000800000008000000000000000000117206c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e0001001600142f4e1eff50d5156d90c5a7d62b7ef3825d5b6db7220203a06059ec858225435fd867601375832479a21ccaef653d6a7920a4cee4797e73184c00739d31000080010000800000008001000000000000000000", + "options": { + "outputs": { + "1": { + "payment_request_index": 0 + } + }, + "payment_requests": [ + { + "recipient_name": "Test Merchant", + "total_amount": 20000000, + "memos": [ + { + "type": "text", + "note": "TextMemo line1\nTextMemo line2" + } + ], + "signature": "3e67d38390b3d32938bd3e3c8f239b201efc278fedadf371bea9561c94da89ea450ad806eeae079983babd8158ac9c7c6a3c12e589435e7535a571d562f9f337" + } + ] + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "invalid_input", + "screens": [] + }, + { + "min_version": "9.24.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "Test Merchant" + }, + { + "type": "confirm", + "title": "", + "body": "Memo from\n\nTest Merchant", + "longtouch": false + }, + { + "type": "confirm", + "title": "Memo 1/2", + "body": "TextMemo line1", + "longtouch": false + }, + { + "type": "confirm", + "title": "Memo 2/2", + "body": "TextMemo line2", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.50000000 TBTC", + "fee": "0.30000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 150.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "6c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e", + "sighash": "default" + } + ] + }, + { + "id": "payment-request-owned-output", + "description": "Covers version-specific handling of payment-request metadata attached to an output owned by this device, rejected since v9.26.3.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01007d0200000001b68d0167988df1602974d874a0f28f65525c86a88d66a09c92587a19408a30db0000000000ffffffff02801d2c0400000000225120cc9235442b0f4fdf2f3c39516207c389e08fed3ae6cfa90cdb820d791a9d6ba4002d310100000000160014fc6b321f780a2401be6884e5bf84520a27820944000000000001005e020000000131313131313131313131313131313131313131313131313131313131313131310000000000ffffffff0100e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a0000000001012b00e1f505000000002251205745c9989285350a6aa88c88f6fb3c43e6bcce823446a0920eb3bc6a93995f1a21166c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e19004c00739d56000080010000800000008000000000000000000117206c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e00010520c5de774ff4c41546478d7e273827d26d2b0908bcfa86cb5861c29f0b05d1f7852107c5de774ff4c41546478d7e273827d26d2b0908bcfa86cb5861c29f0b05d1f78519004c00739d560000800100008000000080010000000000000000220203a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000", + "options": { + "outputs": { + "1": { + "payment_request_index": 0 + } + }, + "payment_requests": [ + { + "recipient_name": "Test Merchant", + "total_amount": 20000000, + "memos": [ + { + "type": "text", + "note": "TextMemo line1\nTextMemo line2" + } + ], + "signature": "4e7837fadb6a322439108c295906d50229f5fd79a1624957e12eeb171cf4ceed36b854ba948ed2630ec89b38aec462b06ed236ea32fa74ad9040dadf430712cd" + } + ] + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "invalid_input", + "screens": [] + }, + { + "min_version": "9.24.0", + "max_version_exclusive": "9.26.3", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "Test Merchant" + }, + { + "type": "confirm", + "title": "", + "body": "Memo from\n\nTest Merchant", + "longtouch": false + }, + { + "type": "confirm", + "title": "Memo 1/2", + "body": "TextMemo line1", + "longtouch": false + }, + { + "type": "confirm", + "title": "Memo 2/2", + "body": "TextMemo line2", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.3", + "outcome": "invalid_input", + "screens": [] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "6c9e72468f297c110e46bfedd7ffe83ab20470a1ae6a7fc13ccf94986541292e", + "sighash": "default" + } + ] + }, + { + "id": "multiple-output-types-bitcoin-coin", + "description": "Displays P2PKH, P2SH, P2WPKH and P2WSH recipients, warns about two change outputs, and signs the transaction.", + "coin": "btc", + "psbt": { + "transaction": "70736274ff0100fdfd000100000001c38bf8f182f907c18224bd5f9cbd843e118019c68c288f5045bc4941672d30380000000000ffffffff0600e1f505000000001976a914111111111111111111111111111111111111111188acd20296490000000017a91422222222222222222222222222222222222222228770170000000000001600143333333333333333333333333333333333333333581b000000000000220020444444444444444444444444444444444444444444444444444444444444444480902029000000001600140b664ee927e3e41c6f5acd135d5b31fca4ed8e4a640000000000000016001491d160f1749d830eec2b9402b0b1fcc5ccd52ab1000000000001005201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff018057ff7800000000160014cda0c8fb9f429f6090136946d7c928e77117847f0000000001011f8057ff7800000000160014cda0c8fb9f429f6090136946d7c928e77117847f22060218f980e27a3f46860d15cbeb0aba08ed32bf5de22be5a3b80e26cfec5727eb1d184c00739d54000080000000800a000080000000000500000000000000002202024ea6cfaca7a9f7689293a9b67c77e0a3e5aa8b160b5cfa12474b52767b4b94cf184c00739d54000080000000800a000080010000000300000000220202b8564af1c586c4477df4c233987002147a77987163bdc0535d364569306a74b6184c00739d54000080000000800a000080010000001e00000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "1.00000000 BTC", + "address": "12ZEw5Hcv1hTb6YUQJ69y1V7uhcoDz92PH" + }, + { + "type": "transaction_address", + "amount": "12.34567890 BTC", + "address": "34oVnh4gNviJGMnNvgquMeLAxvXJuaRVMZ" + }, + { + "type": "transaction_address", + "amount": "0.00006000 BTC", + "address": "bc1qxvenxvenxvenxvenxvenxvenxvenxven2ymjt8" + }, + { + "type": "transaction_address", + "amount": "0.00007000 BTC", + "address": "bc1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zqd8sxw4" + }, + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "13.39999900 BTC", + "fee": "0.05419010 BTC", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "1.00000000 BTC", + "address": "12ZE w5Hc v1hT b6YU QJ69 y1V7 uhco Dz92 PH" + }, + { + "type": "transaction_address", + "amount": "12.34567890 BTC", + "address": "34oV nh4g NviJ GMnN vgqu MeLA xvXJ uaRV MZ" + }, + { + "type": "transaction_address", + "amount": "0.00006000 BTC", + "address": "bc1q xven xven xven xven xven xven xven xven 2ymj t8" + }, + { + "type": "transaction_address", + "amount": "0.00007000 BTC", + "address": "bc1q g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zq d8sx w4" + }, + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "13.39999900 BTC", + "fee": "0.05419010 BTC", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "0218f980e27a3f46860d15cbeb0aba08ed32bf5de22be5a3b80e26cfec5727eb1d", + "sighash": "all" + } + ] + }, + { + "id": "multiple-output-types-bitcoin-satoshi", + "description": "Displays P2PKH, P2SH, P2WPKH and P2WSH recipients, warns about two change outputs, and signs the transaction.", + "coin": "btc", + "psbt": { + "transaction": "70736274ff0100fdfd000100000001c38bf8f182f907c18224bd5f9cbd843e118019c68c288f5045bc4941672d30380000000000ffffffff0600e1f505000000001976a914111111111111111111111111111111111111111188acd20296490000000017a91422222222222222222222222222222222222222228770170000000000001600143333333333333333333333333333333333333333581b000000000000220020444444444444444444444444444444444444444444444444444444444444444480902029000000001600140b664ee927e3e41c6f5acd135d5b31fca4ed8e4a640000000000000016001491d160f1749d830eec2b9402b0b1fcc5ccd52ab1000000000001005201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff018057ff7800000000160014cda0c8fb9f429f6090136946d7c928e77117847f0000000001011f8057ff7800000000160014cda0c8fb9f429f6090136946d7c928e77117847f22060218f980e27a3f46860d15cbeb0aba08ed32bf5de22be5a3b80e26cfec5727eb1d184c00739d54000080000000800a000080000000000500000000000000002202024ea6cfaca7a9f7689293a9b67c77e0a3e5aa8b160b5cfa12474b52767b4b94cf184c00739d54000080000000800a000080010000000300000000220202b8564af1c586c4477df4c233987002147a77987163bdc0535d364569306a74b6184c00739d54000080000000800a000080010000001e00000000", + "options": { + "format_unit": "sat" + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "100000000 sat", + "address": "12ZEw5Hcv1hTb6YUQJ69y1V7uhcoDz92PH" + }, + { + "type": "transaction_address", + "amount": "1234567890 sat", + "address": "34oVnh4gNviJGMnNvgquMeLAxvXJuaRVMZ" + }, + { + "type": "transaction_address", + "amount": "6000 sat", + "address": "bc1qxvenxvenxvenxvenxvenxvenxvenxven2ymjt8" + }, + { + "type": "transaction_address", + "amount": "7000 sat", + "address": "bc1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zqd8sxw4" + }, + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "1339999900 sat", + "fee": "5419010 sat", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "100000000 sat", + "address": "12ZE w5Hc v1hT b6YU QJ69 y1V7 uhco Dz92 PH" + }, + { + "type": "transaction_address", + "amount": "1234567890 sat", + "address": "34oV nh4g NviJ GMnN vgqu MeLA xvXJ uaRV MZ" + }, + { + "type": "transaction_address", + "amount": "6000 sat", + "address": "bc1q xven xven xven xven xven xven xven xven 2ymj t8" + }, + { + "type": "transaction_address", + "amount": "7000 sat", + "address": "bc1q g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zq d8sx w4" + }, + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "1339999900 sat", + "fee": "5419010 sat", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "0218f980e27a3f46860d15cbeb0aba08ed32bf5de22be5a3b80e26cfec5727eb1d", + "sighash": "all" + } + ] + }, + { + "id": "multiple-output-types-litecoin-coin", + "description": "Displays P2PKH, P2SH, P2WPKH and P2WSH recipients, warns about two change outputs, and signs the transaction.", + "coin": "ltc", + "psbt": { + "transaction": "70736274ff0100fdfd000100000001df6504f0b9af7c7d6795aa190cb900553f2b56690c26ed0d5b8439086dedb9bf0000000000ffffffff0600e1f505000000001976a914111111111111111111111111111111111111111188acd20296490000000017a91422222222222222222222222222222222222222228770170000000000001600143333333333333333333333333333333333333333581b000000000000220020444444444444444444444444444444444444444444444444444444444444444480902029000000001600146874a290ffa9444d417e413635eb17a610282aef640000000000000016001466d4383af71817173d87697435a67185d787eb0e000000000001005201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff018057ff7800000000160014f3f3b2cac1d6582e72c41c7db73f4d1c8eb642500000000001011f8057ff7800000000160014f3f3b2cac1d6582e72c41c7db73f4d1c8eb64250220602bad6b8353f8ad1cc4cfc775b05ab9f834a1d132ac36e4cafdc43d8c2c99ebc46184c00739d54000080020000800a0000800000000005000000000000000022020394b3d8170d82ca3dab3feaa5846c5e659cf2f970fc535597aa96c921ed747bdd184c00739d54000080020000800a00008001000000030000000022020349e62be3b83b966bb79dfb50aac69ca112df3237b0ec5cbb069321d2dc363d84184c00739d54000080020000800a000080010000001e00000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "1.00000000 LTC", + "address": "LLnCCHbSzfwWquEdaS5TF2Yt7uz5Qb1SZ1" + }, + { + "type": "transaction_address", + "amount": "12.34567890 LTC", + "address": "MB1e6aUeL3Zj4s4H2ZqFBHaaHd7kvvzTco" + }, + { + "type": "transaction_address", + "amount": "0.00006000 LTC", + "address": "ltc1qxvenxvenxvenxvenxvenxvenxvenxvenwcpknh" + }, + { + "type": "transaction_address", + "amount": "0.00007000 LTC", + "address": "ltc1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zqwr7k5s" + }, + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "13.39999900 LTC", + "fee": "0.05419010 LTC", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "1.00000000 LTC", + "address": "LLnC CHbS zfwW quEd aS5T F2Yt 7uz5 Qb1S Z1" + }, + { + "type": "transaction_address", + "amount": "12.34567890 LTC", + "address": "MB1e 6aUe L3Zj 4s4H 2ZqF BHaa Hd7k vvzT co" + }, + { + "type": "transaction_address", + "amount": "0.00006000 LTC", + "address": "ltc1 qxve nxve nxve nxve nxve nxve nxve nxve nwcp knh" + }, + { + "type": "transaction_address", + "amount": "0.00007000 LTC", + "address": "ltc1 qg3z yg3z yg3z yg3z yg3z yg3z yg3z yg3z yg3z yg3z yg3z yg3z yg3z qwr7 k5s" + }, + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "13.39999900 LTC", + "fee": "0.05419010 LTC", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "02bad6b8353f8ad1cc4cfc775b05ab9f834a1d132ac36e4cafdc43d8c2c99ebc46", + "sighash": "all" + } + ] + }, + { + "id": "high-fee-rounding", + "description": "Displays the 18.1% high-fee warning and requires the final longtouch on the warning instead of the fee screen.", + "coin": "btc", + "psbt": { + "transaction": "70736274ff0100fdfd000100000001c38bf8f182f907c18224bd5f9cbd843e118019c68c288f5045bc4941672d30380000000000ffffffff0600e1f505000000001976a914111111111111111111111111111111111111111188acd240aa3d0000000017a91422222222222222222222222222222222222222228770170000000000001600143333333333333333333333333333333333333333581b000000000000220020444444444444444444444444444444444444444444444444444444444444444480902029000000001600140b664ee927e3e41c6f5acd135d5b31fca4ed8e4a640000000000000016001491d160f1749d830eec2b9402b0b1fcc5ccd52ab1000000000001005201000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff018057ff7800000000160014cda0c8fb9f429f6090136946d7c928e77117847f0000000001011f8057ff7800000000160014cda0c8fb9f429f6090136946d7c928e77117847f22060218f980e27a3f46860d15cbeb0aba08ed32bf5de22be5a3b80e26cfec5727eb1d184c00739d54000080000000800a000080000000000500000000000000002202024ea6cfaca7a9f7689293a9b67c77e0a3e5aa8b160b5cfa12474b52767b4b94cf184c00739d54000080000000800a000080010000000300000000220202b8564af1c586c4477df4c233987002147a77987163bdc0535d364569306a74b6184c00739d54000080000000800a000080010000001e00000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 18.1%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "1.00000000 BTC", + "address": "12ZEw5Hcv1hTb6YUQJ69y1V7uhcoDz92PH" + }, + { + "type": "transaction_address", + "amount": "10.34567890 BTC", + "address": "34oVnh4gNviJGMnNvgquMeLAxvXJuaRVMZ" + }, + { + "type": "transaction_address", + "amount": "0.00006000 BTC", + "address": "bc1qxvenxvenxvenxvenxvenxvenxvenxven2ymjt8" + }, + { + "type": "transaction_address", + "amount": "0.00007000 BTC", + "address": "bc1qg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zqd8sxw4" + }, + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "13.39999900 BTC", + "fee": "2.05419010 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 18.1%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "1.00000000 BTC", + "address": "12ZE w5Hc v1hT b6YU QJ69 y1V7 uhco Dz92 PH" + }, + { + "type": "transaction_address", + "amount": "10.34567890 BTC", + "address": "34oV nh4g NviJ GMnN vgqu MeLA xvXJ uaRV MZ" + }, + { + "type": "transaction_address", + "amount": "0.00006000 BTC", + "address": "bc1q xven xven xven xven xven xven xven xven 2ymj t8" + }, + { + "type": "transaction_address", + "amount": "0.00007000 BTC", + "address": "bc1q g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zy g3zq d8sx w4" + }, + { + "type": "confirm", + "title": "Warning", + "body": "There are 2\nchange outputs.\nProceed?", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "13.39999900 BTC", + "fee": "2.05419010 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 18.1%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "0218f980e27a3f46860d15cbeb0aba08ed32bf5de22be5a3b80e26cfec5727eb1d", + "sighash": "all" + } + ] + }, + { + "id": "swap-payment-request", + "description": "Displays and signs a Bitcoin-to-Ethereum coin-purchase payment request.", + "coin": "btc", + "psbt": { + "transaction": "70736274ff01007d0200000001013018e6e997b8e9b80d0244f11e3c68ba7107d8bb9a359a16a9a796d64e85550000000000ffffffff02801d2c04000000001600141133a71526b176aac7b2462bc5bff36abe68a18a002d310100000000225120da28dbf53b86ae59ae156af3745bb326f44ce309dda97345708c8d3dd45755a6000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b0000000001011f00e1f50500000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b220602c6f297dd7bcc0a38a527b3f2289cf91816cae2845a605080fd5fc9af4a945048184c00739d5400008000000080000000800000000000000000002202034fc8251f3b2e939dccf75db7c9edf0e8cbc88ec662c0119e961e586ebebe3731184c00739d54000080000000800000008001000000000000000000", + "options": { + "outputs": { + "1": { + "payment_request_index": 0 + } + }, + "payment_requests": [ + { + "recipient_name": "Test Merchant", + "total_amount": 20000000, + "memos": [ + { + "type": "coin_purchase", + "coin_type": 60, + "amount": "0.25 ETH", + "address": "0x416E88840Eb6353E49252Da2a2c140eA1f969D1a", + "address_keypath": "m/44'/60'/0'/0/0" + } + ], + "signature": "917d3514b2358406730f42952efe0a873255788314a9cf2906474c1e2cefd8cb0e35791ef8211463d37a007e3d9d3a0ec11736250b7e089156c53e5ebf40413d" + } + ] + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "invalid_input", + "screens": [] + }, + { + "min_version": "9.24.0", + "max_version_exclusive": "9.26.0", + "outcome": "invalid_input", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "Test Merchant" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "Test Merchant" + }, + { + "type": "swap", + "title": "Swap", + "from": "0.20000000 BTC", + "to": "0.25 ETH" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 BTC", + "fee": "0.10000000 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "02c6f297dd7bcc0a38a527b3f2289cf91816cae2845a605080fd5fc9af4a945048", + "sighash": "all" + } + ] + }, + { + "id": "swap-payment-request-unsupported-source", + "description": "Rejects a coin-purchase payment request whose source account is Bitcoin testnet.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01007d0200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000ffffffff02801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000", + "options": { + "outputs": { + "1": { + "payment_request_index": 0 + } + }, + "payment_requests": [ + { + "recipient_name": "Test Merchant", + "total_amount": 20000000, + "memos": [ + { + "type": "coin_purchase", + "coin_type": 60, + "amount": "0.25 ETH", + "address": "0x416E88840Eb6353E49252Da2a2c140eA1f969D1a", + "address_keypath": "m/44'/60'/0'/0/0" + } + ], + "signature": "c231191f200c3259f71a509dc644b49ab14332fdde4b63a12aaa19b744f188f872e7c60cc6a7e5823acb1f849659e16664fbf2680749bd2101b33cdb49bb8dce" + } + ] + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "invalid_input", + "screens": [] + }, + { + "min_version": "9.24.0", + "max_version_exclusive": "9.26.0", + "outcome": "invalid_input", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "Test Merchant" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "invalid_input", + "screens": [] + } + ] + }, + { + "id": "p2wpkh-p2sh", + "description": "Signs a nested SegWit input and recognizes nested SegWit change.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01007e020000000118daf26c629c509a7ea9da4fa3a32393c330c526aa6466e2a2e2f880ee9cf50e0000000000ffffffff02801d2c040000000017a9147946b34fad2af6a7ed141651188fb6d7082b8ef987002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005302000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f5050000000017a914f566e83e9f22881747e8308ed56afd5fa0abbc66870000000001012000e1f5050000000017a914f566e83e9f22881747e8308ed56afd5fa0abbc6687010416001468a00bbd6de555d756e92815e523b789def6c0b022060329b834de159b7b1ff5d2bcc4b10ba2611169dff448a59df7f0c84c2c54b9b7d0184c00739d31000080010000800000008000000000000000000001001600142f4e1eff50d5156d90c5a7d62b7ef3825d5b6db7220203a06059ec858225435fd867601375832479a21ccaef653d6a7920a4cee4797e73184c00739d31000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "0329b834de159b7b1ff5d2bcc4b10ba2611169dff448a59df7f0c84c2c54b9b7d0", + "sighash": "all" + } + ] + }, + { + "id": "high-input-address-index", + "description": "Spends an owned input at address index 100000 without treating the high spend path as a receive-address verification request.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01007d02000000014e52523f00352b5da9aca9fcb5dfb4a6c1e0cb8632718e187e0ffefaec0a5eb90000000000ffffffff02801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014c2e296c295380207dfdc535c8872c043d9bf52d30000000001011f00e1f50500000000160014c2e296c295380207dfdc535c8872c043d9bf52d3220603ce6f00e8e7302e323faf1f26ce2584307cb1bcef2a2b2140c6013c67bb9b991b184c00739d54000080010000800000008000000000a086010000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "03ce6f00e8e7302e323faf1f26ce2584307cb1bcef2a2b2140c6013c67bb9b991b", + "sighash": "all" + } + ] + }, + { + "id": "locktime-zero", + "description": "Suppresses the locktime confirmation when locktime is zero even if the sequence signals RBF.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01007d0200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000fdffffff02801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "03a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3", + "sighash": "all" + } + ] + }, + { + "id": "locktime-non-rbf", + "description": "Displays block locktime 10 and its non-rbf sequence semantics.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01007d0200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000feffffff02801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef140a0000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\nTransaction is not RBF", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\nTransaction is not RBF", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\nTransaction is not RBF", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "03a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3", + "sighash": "all" + } + ] + }, + { + "id": "locktime-rbf", + "description": "Displays block locktime 10 and its rbf sequence semantics.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01007d0200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000fdffffff02801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef140a0000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d540000800100008000000080000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\nTransaction is RBF", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\nTransaction is RBF", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\nTransaction is RBF", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "03a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3", + "sighash": "all" + } + ] + }, + { + "id": "locktime-litecoin-non-rbf-sequence", + "description": "Displays a Litecoin block locktime without Bitcoin-specific RBF wording.", + "coin": "ltc", + "psbt": { + "transaction": "70736274ff01007102000000018660b164e26bff1622e4172600f4011bd824211a4ef4201d92c0b0fb290f9b730000000000feffffff02801d2c04000000001600146514c2779c7ee0f8e09ae4f2999c334e5254c70e002d310100000000160014751e76e8199196d454941c45d1b3a323f1433bd60a0000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014da2f19386bd475b275e76917886fc8340cce9f160000000001011f00e1f50500000000160014da2f19386bd475b275e76917886fc8340cce9f162206032780e5412e19ac486f2be57004bc34d0f9957468bfa2b0fa1c8e689819d97df2184c00739d5400008002000080000000800000000000000000002202039537a5fa4fa947baa7b215407914d5fcb3709c554ff5750bf0ba931b3cda24ae184c00739d54000080020000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\n", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 LTC", + "address": "ltc1qw508d6qejxtdg4y5r3zarvary0c5xw7kgmn4n9" + }, + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\n", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 LTC", + "fee": "0.10000000 LTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 LTC", + "address": "ltc1 qw50 8d6q ejxt dg4y 5r3z arva ry0c 5xw7 kgmn 4n9" + }, + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\n", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 LTC", + "fee": "0.10000000 LTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "032780e5412e19ac486f2be57004bc34d0f9957468bfa2b0fa1c8e689819d97df2", + "sighash": "all" + } + ] + }, + { + "id": "locktime-litecoin-rbf-sequence", + "description": "Displays a Litecoin block locktime without Bitcoin-specific RBF wording.", + "coin": "ltc", + "psbt": { + "transaction": "70736274ff01007102000000018660b164e26bff1622e4172600f4011bd824211a4ef4201d92c0b0fb290f9b730000000000fdffffff02801d2c04000000001600146514c2779c7ee0f8e09ae4f2999c334e5254c70e002d310100000000160014751e76e8199196d454941c45d1b3a323f1433bd60a0000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014da2f19386bd475b275e76917886fc8340cce9f160000000001011f00e1f50500000000160014da2f19386bd475b275e76917886fc8340cce9f162206032780e5412e19ac486f2be57004bc34d0f9957468bfa2b0fa1c8e689819d97df2184c00739d5400008002000080000000800000000000000000002202039537a5fa4fa947baa7b215407914d5fcb3709c554ff5750bf0ba931b3cda24ae184c00739d54000080020000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\n", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 LTC", + "address": "ltc1qw508d6qejxtdg4y5r3zarvary0c5xw7kgmn4n9" + }, + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\n", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 LTC", + "fee": "0.10000000 LTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 LTC", + "address": "ltc1 qw50 8d6q ejxt dg4y 5r3z arva ry0c 5xw7 kgmn 4n9" + }, + { + "type": "confirm", + "title": "", + "body": "Locktime on block:\n10\n", + "longtouch": false + }, + { + "type": "transaction_fee", + "amount": "0.30000000 LTC", + "fee": "0.10000000 LTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "032780e5412e19ac486f2be57004bc34d0f9957468bfa2b0fa1c8e689819d97df2", + "sighash": "all" + } + ] + }, + { + "id": "p2tr-output-mainnet", + "description": "Displays and signs a mainnet P2TR recipient output from a native SegWit account.", + "coin": "btc", + "psbt": { + "transaction": "70736274ff01007d0200000001013018e6e997b8e9b80d0244f11e3c68ba7107d8bb9a359a16a9a796d64e85550000000000ffffffff02801d2c04000000001600141133a71526b176aac7b2462bc5bff36abe68a18a002d310100000000225120da28dbf53b86ae59ae156af3745bb326f44ce309dda97345708c8d3dd45755a6000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b0000000001011f00e1f50500000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b220602c6f297dd7bcc0a38a527b3f2289cf91816cae2845a605080fd5fc9af4a945048184c00739d5400008000000080000000800000000000000000002202034fc8251f3b2e939dccf75db7c9edf0e8cbc88ec662c0119e961e586ebebe3731184c00739d54000080000000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "bc1pmg5dhafms6h9nts4dtehgkanym6yeccfmk5hx3ts3jxnm4zh2knqv80ha5" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 BTC", + "fee": "0.10000000 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "bc1p mg5d hafm s6h9 nts4 dteh gkan ym6y eccf mk5h x3ts 3jxn m4zh 2knq v80h a5" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 BTC", + "fee": "0.10000000 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "02c6f297dd7bcc0a38a527b3f2289cf91816cae2845a605080fd5fc9af4a945048", + "sighash": "all" + } + ] + }, + { + "id": "silent-payment-owned-output", + "description": "Covers version-specific handling of silent-payment metadata attached to an output owned by this device, rejected since v9.26.3.", + "coin": "btc", + "psbt": { + "transaction": "70736274ff01007d0200000001a49f3e93836b9131d84d700f422b07ff6f6ffa379499b910c0f153e18f3dac3c0000000000ffffffff02801d2c0400000000225120aaf8631d8fffabc65d283faa12aedc768bdc669c1ba6ff1166f356f13eefe089002d310100000000160014fc8c96dd45eed60d714647d787e74d2b33b5ae9b000000000001005e02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f5050000000022512041ea70bc6cdc9e38c9faa847f0b9e8ce082b3be15fa6d137fbbab6cb138a29430000000001012b00e1f5050000000022512041ea70bc6cdc9e38c9faa847f0b9e8ce082b3be15fa6d137fbbab6cb138a29432116cd47d5291421bd92a7dd849beb7f0d13f1c7c98067e3c23ff98de62c1ff0952719004c00739d5600008000000080000000800000000000000000011720cd47d5291421bd92a7dd849beb7f0d13f1c7c98067e3c23ff98de62c1ff095270001052067426cdc697740fe8fcedd942ae76b85b22d73db568fc32d7ced988f387f0ee5210767426cdc697740fe8fcedd942ae76b85b22d73db568fc32d7ced988f387f0ee519004c00739d560000800000008000000080010000000000000000220202c6f297dd7bcc0a38a527b3f2289cf91816cae2845a605080fd5fc9af4a945048184c00739d540000800000008000000080000000000000000000", + "options": { + "outputs": { + "1": { + "silent_payment_address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + } + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.21.0", + "outcome": "unsupported", + "unsupported_version": "9.21.0", + "screens": [] + }, + { + "min_version": "9.21.0", + "max_version_exclusive": "9.22.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "This BitBox02: sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 BTC", + "fee": "0.10000000 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.22.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "This BitBox (same account): sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 BTC", + "fee": "0.10000000 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "max_version_exclusive": "9.26.3", + "outcome": "success", + "screens": [ + { + "type": "transaction_address", + "amount": "0.20000000 BTC", + "address": "This BitBox (same account): sp1q qgst e7k9 hx0q ftg6 qmwl kqtw uy6c ycya vzmz j85c 6qdf hjdp djtd gqju exzk 6mur w56s uy3e 0rd2 cgqv ycxt tddw svgx e2us fpxu mr70 xc9p kqwv" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 BTC", + "fee": "0.10000000 BTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.3", + "outcome": "invalid_input", + "screens": [] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "cd47d5291421bd92a7dd849beb7f0d13f1c7c98067e3c23ff98de62c1ff09527", + "sighash": "default" + } + ] + }, + { + "id": "op-return-nonascii", + "description": "Displays a non-ASCII OP_RETURN payload as hexadecimal and signs the transaction.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01008d0200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000ffffffff030000000000000000076a050102030405801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d54000080010000800000008000000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "unsupported", + "unsupported_version": "9.24.0", + "screens": [] + }, + { + "min_version": "9.24.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "OP_RETURN\ndata (hex)", + "body": "0102030405", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "OP_RETURN\ndata (hex)", + "body": "0102030405", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "03a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3", + "sighash": "all" + } + ] + }, + { + "id": "op-return-nonzero-value", + "description": "Rejects a nonzero-value OP_RETURN output.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100930200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000ffffffff0364000000000000000d6a0b68656c6c6f20776f726c64801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d54000080010000800000008000000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000" + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "unsupported", + "unsupported_version": "9.24.0", + "screens": [] + }, + { + "min_version": "9.24.0", + "outcome": "invalid_input", + "screens": [] + } + ] + }, + { + "id": "op-return-silent-payment", + "description": "Rejects silent-payment metadata on an OP_RETURN output.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100930200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000ffffffff0300000000000000000d6a0b68656c6c6f20776f726c64801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d54000080010000800000008000000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000", + "options": { + "outputs": { + "0": { + "silent_payment_address": "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv" + } + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "unsupported", + "unsupported_version": "9.24.0", + "screens": [] + }, + { + "min_version": "9.24.0", + "outcome": "invalid_input", + "screens": [] + } + ] + }, + { + "id": "op-return-payment-request", + "description": "Rejects payment-request metadata on an OP_RETURN output.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100930200000001703a366633ea48d0ffd13c1dd1d33c7a0cdcb9b29fb7174ac23842ea36b897c20000000000ffffffff0300000000000000000d6a0b68656c6c6f20776f726c64801d2c04000000001600149e2597ee0b2fbffec6275bd6cab7d2b4737b6e95002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005202000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000160014fc6b321f780a2401be6884e5bf84520a278209440000000001011f00e1f50500000000160014fc6b321f780a2401be6884e5bf84520a27820944220603a9a6f11db052a402ccd0e2a2a31c4ad196b93db5b1ad1a3bdf8f23882d42b3c3184c00739d54000080010000800000008000000000000000000000220202dca870342cf7bc12c2d6c23dc6a0761160b1ff54e820be4b48c8f3ba9a8dc239184c00739d54000080010000800000008001000000000000000000", + "options": { + "outputs": { + "0": { + "payment_request_index": 0 + } + }, + "payment_requests": [ + { + "recipient_name": "Test Merchant", + "total_amount": 1, + "memos": [ + { + "type": "text", + "note": "TextMemo line1\nTextMemo line2" + } + ], + "signature": "93582744d5fab8b70cd2f1686bb7baf76e2797f095a83a6029f04c8c92d11bca4f274a71ef0ac79ce65b87241feb770823a1525d64f85e69104081d6ad397601" + } + ] + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.24.0", + "outcome": "unsupported", + "unsupported_version": "9.24.0", + "screens": [] + }, + { + "min_version": "9.24.0", + "outcome": "invalid_input", + "screens": [] + } + ] + }, + { + "id": "multisig-not-registered", + "description": "Rejects a valid 1-of-2 P2WSH multisig transaction when its account has not been registered.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff010089020000000131816c7584e8fb28ce853874eeb709394562c6e99eb9b6f3987d205dcc6933880000000000ffffffff02801d2c040000000022002049934b3c5c97bbd8e9e77ca525ddbad2addcf929f5dd3b83d2c017ab65e54382002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005e02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000220020276686da70f18aec8090484fc1a58b0808b2f6c7aa9f88191ec47166bd805c9e0000000001012b00e1f50500000000220020276686da70f18aec8090484fc1a58b0808b2f6c7aa9f88191ec47166bd805c9e0105475121031cf0f0f1bf784a99e0c007f162d5df97563915d74004ed3fa2403f2fd1569fad21039fb11f1394842eeeadd04ec165f713c49cb875443da2bc05d994ec118d7d1fde52ae2206031cf0f0f1bf784a99e0c007f162d5df97563915d74004ed3fa2403f2fd1569fad1c0dbd0cc33000008001000080000000800200008000000000000000002206039fb11f1394842eeeadd04ec165f713c49cb875443da2bc05d994ec118d7d1fde1c4c00739d30000080010000800000008002000080000000000000000000010147512102e48ca9c37f3f4b23010171cde63b3e22e752c5f0bda353bb1a443385b76ba38b21031bc4da3f6dfefcc269e08ac1edfffb1bf928790b0ed888d6047adbbd0ec27acb52ae220202e48ca9c37f3f4b23010171cde63b3e22e752c5f0bda353bb1a443385b76ba38b1c0dbd0cc33000008001000080000000800200008001000000000000002202031bc4da3f6dfefcc269e08ac1edfffb1bf928790b0ed888d6047adbbd0ec27acb1c4c00739d3000008001000080000000800200008001000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "multisig", + "threshold": 1, + "xpubs": [ + "xpub6EhivkawbkKDRT9Z59WwqhAzZZe7NQhxPgYc67nHbagRxoSbKEiZ6x4P4bE6s48GHvAHaGH8hsQfJ4RSSeNbVdYp7Kii2UGWKn8x2bQDtiR", + "tpubDFX8u4cajvyZrQGikAfBcRt5EqVj4C9xB1HEQSZfN5weSUq4ULeMra6GXY8fM6hwgBX3WZ8TCP7nvMQu3hM4zvUHqWPhU9oq8HNV4ixboTz" + ], + "our_xpub_index": 0, + "script_type": "p2wsh" + }, + "keypath": "m/48'/1'/0'/2'" + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "outcome": "invalid_input", + "screens": [] + } + ] + }, + { + "id": "multisig-p2wsh-p2sh", + "description": "Signs and finalizes a registered 1-of-2 nested P2SH-P2WSH multisig transaction.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01007e02000000017a26adfe8f25b544220236a6516252b9eb746a620ffd210679634285fc890dd50000000000ffffffff02801d2c040000000017a914c88f04120fca1cbbd8e1a9dac997a5d99d8ca09d87002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005302000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f5050000000017a9143b3a20e6b4121c867e5a8e1bf1c3c85504b1ff13870000000001012000e1f5050000000017a9143b3a20e6b4121c867e5a8e1bf1c3c85504b1ff13870104220020cd9833d9c1ca210e8e5984f8fffa56f40770409811f431ea1c6daa0cd7cfba7f010547512102efb2f859bf983f02f6c17664426a9992985b3bdac83184b0162993d398f8ba0d2103f506dd0e67b1d3f035a6055961a3ac5510dff004069e9d025930338fd8e20c8852ae220602efb2f859bf983f02f6c17664426a9992985b3bdac83184b0162993d398f8ba0d1c0dbd0cc3300000800100008000000080010000800000000000000000220603f506dd0e67b1d3f035a6055961a3ac5510dff004069e9d025930338fd8e20c881c4c00739d3000008001000080000000800100008000000000000000000001002200209a8754cc38326f8daa604b0d1f4d803f6bd714cf428490ed6cb5407b05ea91cd01014751210235294f21973f87de509b16ef8f71bf9c353c0d450db814a428709f80a2a347f32103fd271ddb4968f17ea1aab754c4283c05e58ee5810868e845ad7d2b06ef30a87452ae22020235294f21973f87de509b16ef8f71bf9c353c0d450db814a428709f80a2a347f31c0dbd0cc3300000800100008000000080010000800100000000000000220203fd271ddb4968f17ea1aab754c4283c05e58ee5810868e845ad7d2b06ef30a8741c4c00739d3000008001000080000000800100008001000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "multisig", + "threshold": 1, + "xpubs": [ + "xpub6EhivkawbkKDNQtJHZFjbd79dLGtC1yTcWn7WHPaCdpa8jXS8mU4y9tdYqzhrUVt3w6QR6vjx66nbUX4s2Hw5V5oer76XzKpRVMExC7zpLG", + "tpubDFX8u4cajvyZqUGS5AVgPb7GUpAJJ5nbkVMnw1m2ajrHaZmknbxuWDac2P2zyiB35UcgqwmVAeVzGEVV9qbji6tzYRCTiWwPGvtUULR4DLM" + ], + "our_xpub_index": 0, + "script_type": "p2wsh_p2sh" + }, + "keypath": "m/48'/1'/0'/1'" + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "1-of-2\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test sh-wsh multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "1-of-2\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test sh-wsh multisig", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "1-of-2\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test sh-wsh multisig", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "multisig", + "threshold": 1, + "xpubs": [ + "xpub6EhivkawbkKDNQtJHZFjbd79dLGtC1yTcWn7WHPaCdpa8jXS8mU4y9tdYqzhrUVt3w6QR6vjx66nbUX4s2Hw5V5oer76XzKpRVMExC7zpLG", + "tpubDFX8u4cajvyZqUGS5AVgPb7GUpAJJ5nbkVMnw1m2ajrHaZmknbxuWDac2P2zyiB35UcgqwmVAeVzGEVV9qbji6tzYRCTiWwPGvtUULR4DLM" + ], + "our_xpub_index": 0, + "script_type": "p2wsh_p2sh" + }, + "keypath": "m/48'/1'/0'/1'", + "name": "test sh-wsh multisig" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "03f506dd0e67b1d3f035a6055961a3ac5510dff004069e9d025930338fd8e20c88", + "sighash": "all" + } + ] + }, + { + "id": "multisig-large", + "description": "Adds the seventh signature to a registered 7-of-15 P2WSH multisig transaction whose PSBT already contains six cosigner signatures.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100890200000001cb44be13195cb0246f334d02fc36179e5c8d03e5bb812f97b039c5920413280f0000000000ffffffff02801d2c0400000000220020472cd252fa25c59c9e7f876cca3cbf4585826ff6f66b5e465feddc94cf50a5cc002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005e02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f505000000002200208d69e9431a924d7a5cd26f9cf85b2a3bc9d91dc06266f4a24b2ea13fdd1da1780000000001012b00e1f505000000002200208d69e9431a924d7a5cd26f9cf85b2a3bc9d91dc06266f4a24b2ea13fdd1da17822020266dc638844899b98e0d23a40deb72ec14b4842fbd3e70f65f1a955b6cf9f5e50483045022100bf0316ed68454f814fe83efb4ae013339d044b003336a1c68ed80bf9123d9fb2022045ba0d2a90aadfd3b062036e03ba1f6cf8c2aa2bd5d4c5bf1728d4648b5cd87801220202fdb3657418ffef1ed8f513b9e4d9a886ce9798a6b766e5da018417f981395feb483045022100aa4344593f229829440738816c3ed451191898a1761f1e1157a008e7ba5bfb68022030f17cd5f5006baded37e56ecb2480fd3a05b44aa88fe2f870b4974326a04d52012202031cf0f0f1bf784a99e0c007f162d5df97563915d74004ed3fa2403f2fd1569fad483045022100a62043f3af8c106c3ef3d0aad0f7c36940627fa06792e19b178e5cd39c4fe9850220793fa3990ed0fa526018e835aed5cab8206827ddbea30a86cf2421c9cc3fb686012202039446d050672e4829214199332a974a5ec1ec80ab0cd4dfc395e2e37a79af87f0473044022002958b416ce5a3c9722cdd07c45addbc679ec34d63bd138207583b258031300c02204d36fd84522cd7831fc06dbd5e611e7bccfc23098a752808a5a252ba05060070012202039b319f5be7dcd91450775a73dc9f163c20ad6eb08e611f220342d53fb0d70e204730440220630b0e820c0e2e7940929a3190b374a649c2e0f0aa78077806551aad57d20b7c02202a2448562899d865f43805441331533721827925b386841818df7b5e406989e101220203ca2121456edf5d624b9bed7d7f507e40b088b313d5773f19df45e71b96278421483045022100dc2b897a4ae2ae446fb7fe7f94836f1136c7ad3c5deb4cb4ccd1eccd224c3f7a02205e4506f6edd9981ba7bb7578454b4c4250de3070db29b74c20c21f5d88e599b2010105fd01025721020faf2667c643963b00041bea043d6fd2e91f3db842b8ccfddefd0f5615bfe9b4210266dc638844899b98e0d23a40deb72ec14b4842fbd3e70f65f1a955b6cf9f5e502102bcdd4e3e30bdb4219ef1836d0f573b81574639b189225039deaefb5bd75153152102e73cddb837ae9bcb40d3f6cf72e133d77505daf9fe03a5c359822a45332391a82102eb12fd1be52ba727e325bd880e8b0ac28769c3850301f3c333c98c0d420b65ec2102fdb3657418ffef1ed8f513b9e4d9a886ce9798a6b766e5da018417f981395feb21031cf0f0f1bf784a99e0c007f162d5df97563915d74004ed3fa2403f2fd1569fad2103440396891d7d6c9f623635e4c1a4f66edca6dca79c11debe0dc58a0f10f9079421034a0ad5543e4978b96f5a06959b281298e4079fe2fd71efaa0604a93fa9305891210353983ae233c60e81243818ffa3ce635d65d666238c1e7503e31e9f2186cc714521039446d050672e4829214199332a974a5ec1ec80ab0cd4dfc395e2e37a79af87f021039b319f5be7dcd91450775a73dc9f163c20ad6eb08e611f220342d53fb0d70e2021039fb11f1394842eeeadd04ec165f713c49cb875443da2bc05d994ec118d7d1fde2103bbc0345e402e1f8838e0ea1669fc79f452cfcc7136a3ff91f621833fbc154f6a2103ca2121456edf5d624b9bed7d7f507e40b088b313d5773f19df45e71b962784215fae2206020faf2667c643963b00041bea043d6fd2e91f3db842b8ccfddefd0f5615bfe9b41ce9324a6c30000080010000800000008002000080000000000000000022060266dc638844899b98e0d23a40deb72ec14b4842fbd3e70f65f1a955b6cf9f5e501c73bccd56300000800100008000000080020000800000000000000000220602bcdd4e3e30bdb4219ef1836d0f573b81574639b189225039deaefb5bd75153151cd0bee5ae300000800100008000000080020000800000000000000000220602e73cddb837ae9bcb40d3f6cf72e133d77505daf9fe03a5c359822a45332391a81cc8c72c6c300000800100008000000080020000800000000000000000220602eb12fd1be52ba727e325bd880e8b0ac28769c3850301f3c333c98c0d420b65ec1c0bbe8456300000800100008000000080020000800000000000000000220602fdb3657418ffef1ed8f513b9e4d9a886ce9798a6b766e5da018417f981395feb1cac9a9e9e3000008001000080000000800200008000000000000000002206031cf0f0f1bf784a99e0c007f162d5df97563915d74004ed3fa2403f2fd1569fad1c0dbd0cc3300000800100008000000080020000800000000000000000220603440396891d7d6c9f623635e4c1a4f66edca6dca79c11debe0dc58a0f10f907941c0ed45ef03000008001000080000000800200008000000000000000002206034a0ad5543e4978b96f5a06959b281298e4079fe2fd71efaa0604a93fa93058911c9e1cb09d30000080010000800000008002000080000000000000000022060353983ae233c60e81243818ffa3ce635d65d666238c1e7503e31e9f2186cc71451c7fb8ad413000008001000080000000800200008000000000000000002206039446d050672e4829214199332a974a5ec1ec80ab0cd4dfc395e2e37a79af87f01c054f2f713000008001000080000000800200008000000000000000002206039b319f5be7dcd91450775a73dc9f163c20ad6eb08e611f220342d53fb0d70e201c41559f4f3000008001000080000000800200008000000000000000002206039fb11f1394842eeeadd04ec165f713c49cb875443da2bc05d994ec118d7d1fde1c4c00739d300000800100008000000080020000800000000000000000220603bbc0345e402e1f8838e0ea1669fc79f452cfcc7136a3ff91f621833fbc154f6a1c5d3e97e0300000800100008000000080020000800000000000000000220603ca2121456edf5d624b9bed7d7f507e40b088b313d5773f19df45e71b962784211c1a34b3e5300000800100008000000080020000800000000000000000000101fd01025721021f7e9e9befaa3424398f347b5b2f78d5f893498f6f8ec488dd2f9f22ea18c7bc21029c20cd5acd30db6bf7aeaf7f83cc171adc87cba7498bd7e673304db6e0a86b5b2102b29e8ea9305c52a928175756d9c979db1768ce8643badface43e1c4661aa1fec2102d63ffb041633c0c721c9dd16fabdcd0a413a5d20786fd798e9ea5458a9d0145f2102e48ca9c37f3f4b23010171cde63b3e22e752c5f0bda353bb1a443385b76ba38b21031bc4da3f6dfefcc269e08ac1edfffb1bf928790b0ed888d6047adbbd0ec27acb21031fd8d9ae7b5cdd43427ff940ee8b485d7b0e79e2cbb53c111f736db8309dd6cb2103206d7162cc432f2b79039f839a8eb4b9155b407b9178ebf46fc29de2c23965e421034434b5c28fe2b71f01328b3af87647d7729a9ad1d1d1646d3f0b87cd0dfd06b6210347504a67f3b4bfdef20beaa5790304ce34927e740929f2caddc032ae8d653a7921035c3445de6a048ab9e905c86bc6d481890088fa7d51309a439bfeb4af77fc7650210365a176ac65d10de83778956b560630f341a6f6b43584acc0271083cff36ace35210376b63bd235b30bc9042c7b500be79e7fbebbc4f6e723c8b798aea84769acb1ea21037d33decd1fee9418357a1e419813a898f7aa006b9a59ca241fb46ad29b774fcb2103c69f4d4950ed98135ff37e631731881dd01060db8dae4ccda2f8c0a796f5c4e85fae2202021f7e9e9befaa3424398f347b5b2f78d5f893498f6f8ec488dd2f9f22ea18c7bc1c0bbe84563000008001000080000000800200008001000000000000002202029c20cd5acd30db6bf7aeaf7f83cc171adc87cba7498bd7e673304db6e0a86b5b1c0ed45ef0300000800100008000000080020000800100000000000000220202b29e8ea9305c52a928175756d9c979db1768ce8643badface43e1c4661aa1fec1c1a34b3e5300000800100008000000080020000800100000000000000220202d63ffb041633c0c721c9dd16fabdcd0a413a5d20786fd798e9ea5458a9d0145f1c41559f4f300000800100008000000080020000800100000000000000220202e48ca9c37f3f4b23010171cde63b3e22e752c5f0bda353bb1a443385b76ba38b1c0dbd0cc33000008001000080000000800200008001000000000000002202031bc4da3f6dfefcc269e08ac1edfffb1bf928790b0ed888d6047adbbd0ec27acb1c4c00739d3000008001000080000000800200008001000000000000002202031fd8d9ae7b5cdd43427ff940ee8b485d7b0e79e2cbb53c111f736db8309dd6cb1c73bccd56300000800100008000000080020000800100000000000000220203206d7162cc432f2b79039f839a8eb4b9155b407b9178ebf46fc29de2c23965e41c9e1cb09d3000008001000080000000800200008001000000000000002202034434b5c28fe2b71f01328b3af87647d7729a9ad1d1d1646d3f0b87cd0dfd06b61cac9a9e9e30000080010000800000008002000080010000000000000022020347504a67f3b4bfdef20beaa5790304ce34927e740929f2caddc032ae8d653a791cc8c72c6c3000008001000080000000800200008001000000000000002202035c3445de6a048ab9e905c86bc6d481890088fa7d51309a439bfeb4af77fc76501c7fb8ad4130000080010000800000008002000080010000000000000022020365a176ac65d10de83778956b560630f341a6f6b43584acc0271083cff36ace351c054f2f7130000080010000800000008002000080010000000000000022020376b63bd235b30bc9042c7b500be79e7fbebbc4f6e723c8b798aea84769acb1ea1c5d3e97e03000008001000080000000800200008001000000000000002202037d33decd1fee9418357a1e419813a898f7aa006b9a59ca241fb46ad29b774fcb1ce9324a6c300000800100008000000080020000800100000000000000220203c69f4d4950ed98135ff37e631731881dd01060db8dae4ccda2f8c0a796f5c4e81cd0bee5ae3000008001000080000000800200008001000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "multisig", + "threshold": 7, + "xpubs": [ + "xpub6EhivkawbkKDRT9Z59WwqhAzZZe7NQhxPgYc67nHbagRxoSbKEiZ6x4P4bE6s48GHvAHaGH8hsQfJ4RSSeNbVdYp7Kii2UGWKn8x2bQDtiR", + "tpubDFX8u4cajvyZrQGikAfBcRt5EqVj4C9xB1HEQSZfN5weSUq4ULeMra6GXY8fM6hwgBX3WZ8TCP7nvMQu3hM4zvUHqWPhU9oq8HNV4ixboTz", + "tpubDFCqK5T7J3FUasUXA1xsminXkeaPSxe1id2K4KCEaszuTNKERymaiZHTnUcjGDf4b57XinAHauwSh1uDRmNVMtfhiStJWsf83MZLA1tC5qi", + "tpubDEKEr89i7dBFXrrpkK2KZQy9aMw6Dg8idr1ysbZJ94mWk6uaFAy4DmYMw7LMDSEaeSJdAcgshdLATcbr6T9hQoouRphfHC9aBQqiWAh8wRF", + "tpubDERfkonU8krEq7knZXFPFrS5bnbhsyXhV7wdM9agZuzD6YgM95LgBWKzAkPeZMseTWRj2PPcbw2kEjcrBctJEzLNUu2VLz1Fixehoc5z4aq", + "tpubDECMwLTAdWk5RcgKaaLKaP83R586xMFtBhH5osdzdF6VbEwFpQp3uiiaHrKGPnqmfDUKFtSQE5ForxoBYgJWEHA5B2T9i5GP5ZAuScWKXgH", + "tpubDEuxzYbnkbY2XHQZjzcDV2Vk4DH4oZwLefGso1jXfFsTyhVV5HxZvVUrSdGLc9AVBTNcCDZh5qw7RgPiibMfMVWtHpjzoErqJcvukzFSpbf", + "tpubDEccsjksu3ddEUnx5FkNADMy9YkLi3p6vQGpdgFDSypMfYZs7amRL94ucqyHGZrVDMrBc937ptj4SKihNiYJtnRgiZ742hpxfnAgynaQno5", + "tpubDF1U1F2s28Rv71bukRRTLUN3hiTgKDGjxGVrFAYXYo1qaWFNsUnA4MoyFtKoytQyLHfmE267HDo3VDgDUfk2hQwb3ary3aNojzkT1sTe5oi", + "tpubDF8LvKCNR3JMayQDTRRqBD3PLhVng5wk6bYvCpsrhe5Mffon2sSFrLBz9J3WCdLqsoeUjMiG1gqsiiM8RVNw6eSfqbRyFsSt2sZPd54GoVi", + "tpubDEgfQ9SqcnKyEkVmEEkx9AaQ3pZN1nSZQjZxtCcTVytAk62DggB6FiuH12cFeygFLgDPsYtBYDp8FCQDrdgZLrpp2cFhnsFur55kWid1DYg", + "tpubDEHvZYZJ5N3jGxHTJJ1LSWdoD3SUSSz7zUhUPLrTMC7SoEkthZVzfFz8qSYAYF1Rg6LjKTsBZMeNJivd4iDyjbQQ6JcyZe2eHB3WENiQECP", + "tpubDEjJyDs2yboyzkczJ5ADEmU9yJNqCgmpUPBqW5CVSPCxg8Q59ZwDqRCBwUrDmR29iY8BJGfBa5bq9MBhYpnqjaQvNQdFHqGX99W6ocPX63z", + "tpubDFFcidKjrkaTSz4R4s9Y5ijp3dzHkJ9XZ2JkLanjkUskBBbFvjxfQMeU5L52ANJtgpiMRyjK4WzJ6qkK22KQsScHbiQBEsstjVKLoxZynZm", + "tpubDF3ds3VKs6p4YjvSLY848WwW21fxiaYGdn5cZEUZ5CwKwMveavxSNA3GhJHqZkFkXa5DTeT1ZKjThtFY8KDfNAuvF95SDcXEoGXNq5bs9nT" + ], + "our_xpub_index": 0, + "script_type": "p2wsh" + }, + "keypath": "m/48'/1'/0'/2'" + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "7-of-15\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test large multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "7-of-15\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test large multisig", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "7-of-15\nBTC Testnet multisig", + "longtouch": false + }, + { + "type": "confirm", + "title": "Spend from", + "body": "test large multisig", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "multisig", + "threshold": 7, + "xpubs": [ + "xpub6EhivkawbkKDRT9Z59WwqhAzZZe7NQhxPgYc67nHbagRxoSbKEiZ6x4P4bE6s48GHvAHaGH8hsQfJ4RSSeNbVdYp7Kii2UGWKn8x2bQDtiR", + "tpubDFX8u4cajvyZrQGikAfBcRt5EqVj4C9xB1HEQSZfN5weSUq4ULeMra6GXY8fM6hwgBX3WZ8TCP7nvMQu3hM4zvUHqWPhU9oq8HNV4ixboTz", + "tpubDFCqK5T7J3FUasUXA1xsminXkeaPSxe1id2K4KCEaszuTNKERymaiZHTnUcjGDf4b57XinAHauwSh1uDRmNVMtfhiStJWsf83MZLA1tC5qi", + "tpubDEKEr89i7dBFXrrpkK2KZQy9aMw6Dg8idr1ysbZJ94mWk6uaFAy4DmYMw7LMDSEaeSJdAcgshdLATcbr6T9hQoouRphfHC9aBQqiWAh8wRF", + "tpubDERfkonU8krEq7knZXFPFrS5bnbhsyXhV7wdM9agZuzD6YgM95LgBWKzAkPeZMseTWRj2PPcbw2kEjcrBctJEzLNUu2VLz1Fixehoc5z4aq", + "tpubDECMwLTAdWk5RcgKaaLKaP83R586xMFtBhH5osdzdF6VbEwFpQp3uiiaHrKGPnqmfDUKFtSQE5ForxoBYgJWEHA5B2T9i5GP5ZAuScWKXgH", + "tpubDEuxzYbnkbY2XHQZjzcDV2Vk4DH4oZwLefGso1jXfFsTyhVV5HxZvVUrSdGLc9AVBTNcCDZh5qw7RgPiibMfMVWtHpjzoErqJcvukzFSpbf", + "tpubDEccsjksu3ddEUnx5FkNADMy9YkLi3p6vQGpdgFDSypMfYZs7amRL94ucqyHGZrVDMrBc937ptj4SKihNiYJtnRgiZ742hpxfnAgynaQno5", + "tpubDF1U1F2s28Rv71bukRRTLUN3hiTgKDGjxGVrFAYXYo1qaWFNsUnA4MoyFtKoytQyLHfmE267HDo3VDgDUfk2hQwb3ary3aNojzkT1sTe5oi", + "tpubDF8LvKCNR3JMayQDTRRqBD3PLhVng5wk6bYvCpsrhe5Mffon2sSFrLBz9J3WCdLqsoeUjMiG1gqsiiM8RVNw6eSfqbRyFsSt2sZPd54GoVi", + "tpubDEgfQ9SqcnKyEkVmEEkx9AaQ3pZN1nSZQjZxtCcTVytAk62DggB6FiuH12cFeygFLgDPsYtBYDp8FCQDrdgZLrpp2cFhnsFur55kWid1DYg", + "tpubDEHvZYZJ5N3jGxHTJJ1LSWdoD3SUSSz7zUhUPLrTMC7SoEkthZVzfFz8qSYAYF1Rg6LjKTsBZMeNJivd4iDyjbQQ6JcyZe2eHB3WENiQECP", + "tpubDEjJyDs2yboyzkczJ5ADEmU9yJNqCgmpUPBqW5CVSPCxg8Q59ZwDqRCBwUrDmR29iY8BJGfBa5bq9MBhYpnqjaQvNQdFHqGX99W6ocPX63z", + "tpubDFFcidKjrkaTSz4R4s9Y5ijp3dzHkJ9XZ2JkLanjkUskBBbFvjxfQMeU5L52ANJtgpiMRyjK4WzJ6qkK22KQsScHbiQBEsstjVKLoxZynZm", + "tpubDF3ds3VKs6p4YjvSLY848WwW21fxiaYGdn5cZEUZ5CwKwMveavxSNA3GhJHqZkFkXa5DTeT1ZKjThtFY8KDfNAuvF95SDcXEoGXNq5bs9nT" + ], + "our_xpub_index": 0, + "script_type": "p2wsh" + }, + "keypath": "m/48'/1'/0'/2'", + "name": "test large multisig" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "039fb11f1394842eeeadd04ec165f713c49cb875443da2bc05d994ec118d7d1fde", + "sighash": "all" + } + ] + }, + { + "id": "policy-tr-keyspend-with-script-tree", + "description": "Signs a Taproot policy key path whose tweak commits to a script tree.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100890200000001481b25e956e13b122e0155ee1066ff2620345d94c51e6c2b7889e0daac298db00000000000ffffffff02801d2c0400000000225120916ffdd6fe3d0adf420c6d71702a944b8f22db3277d4b24721f2f1deb96e8a46002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001012b00e1f50500000000225120d06244555fe882d1190e968e20f053018fe20114014338e89149d314ef4352d92215c13a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2232034c5ed02e3e62b122655859db72a9a896aa261e8b56dc26c5ce7459ffcc08de2acc0211634c5ed02e3e62b122655859db72a9a896aa261e8b56dc26c5ce7459ffcc08de23d01d9c7b0c5dc86c4594427111e20854229a2bded47788886c3b3e5093c5d9d9f56c5e4fe1a30000080010000800000008003000080000000000000000021163a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b21d004c00739d3000008001000080000000800300008000000000000000000117203a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2011820d9c7b0c5dc86c4594427111e20854229a2bded47788886c3b3e5093c5d9d9f56000105208c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a01062500c022203df70ec9451b9dbf9b910e974fc8f060abfe662091836e09f3acd9017c04563bac21073df70ec9451b9dbf9b910e974fc8f060abfe662091836e09f3acd9017c04563b3d016a4a33d1169c5d57685ea4668790c7ce35fc9af5f8002aea692c3296287f548fc5e4fe1a30000080010000800000008003000080010000000000000021078c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a1d004c00739d3000008001000080000000800300008001000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "tr(@0/**,pk(@1/**))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y" + } + ] + }, + "keypath": "m/48'/1'/0'/3'" + } + } + }, + "expected_needs_prevtxs": false, + "expectations": [ + { + "max_version_exclusive": "9.21.0", + "outcome": "unsupported", + "unsupported_version": "9.21.0", + "screens": [] + }, + { + "min_version": "9.21.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test tr tweaked keyspend", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/**,pk(@1/**))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test tr tweaked keyspend", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/**,pk(@1/**))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "tr(@0/**,pk(@1/**))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y" + } + ] + }, + "name": "test tr tweaked keyspend" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_key", + "pubkey": "3a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2", + "sighash": "default" + } + ] + }, + { + "id": "policy-tr-unspendable-internal-key", + "description": "Signs a Taproot script path with a provably unspendable internal key and displays that property in the policy review.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff0100890200000001a8346bd79715029f26a2628a872d2299934cfc774bb3c4e52209ef404f07278f0000000000ffffffff02801d2c0400000000225120f191a4cb0a3d8f8b47251bc375159b8ff3aac672264cb0e0c9c5adeba37a09e4002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001012b00e1f50500000000225120c5340ec5fb5662a6ef7290f9dffedf9c727d503c2f69377cb4c8560e9f02e45b4114bc0eab168b76abf60a0a6f3c197ea0f9b7973e27ca1aba50daa09c62de813c68e0ed23b292365b738059122f0e9f6887887b474a1e9593cb55e872c7a0c08558405d9d7da26d848dd9562ab95b348ac6414e1bc28a2f8ccd839b7dd5b452abc513d099a0f0efcd011fdcc9b5f18b3c69cb9122391b751e274a67dc74b61757ab5d2215c01ab64872242fff67a9cc2429d50bd615532e55122665e448ae0e53a3f4a2ce594720bc0eab168b76abf60a0a6f3c197ea0f9b7973e27ca1aba50daa09c62de813c68ac203a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2ba529cc021161ab64872242fff67a9cc2429d50bd615532e55122665e448ae0e53a3f4a2ce590d007c461e5d000000000000000021163a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b23d01e0ed23b292365b738059122f0e9f6887887b474a1e9593cb55e872c7a0c085584c00739d3000008001000080000000800300008000000000000000002116bc0eab168b76abf60a0a6f3c197ea0f9b7973e27ca1aba50daa09c62de813c683d01e0ed23b292365b738059122f0e9f6887887b474a1e9593cb55e872c7a0c0855808f56e293000008001000080000000800300008000000000000000000117201ab64872242fff67a9cc2429d50bd615532e55122665e448ae0e53a3f4a2ce59011820e0ed23b292365b738059122f0e9f6887887b474a1e9593cb55e872c7a0c0855800010520349092f96b1dd44a23e6a05986e7934bae5d6a87b9a050c1e13b38a0ae4e52c101064900c04620dbe4edc0edbcb5f212c9fa1d1bcbdb4b3f6e875a5ea52ac3cafed4057379302bac208c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44aba529c2107349092f96b1dd44a23e6a05986e7934bae5d6a87b9a050c1e13b38a0ae4e52c10d007c461e5d010000000000000021078c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a3d014b40a0f212ea2f428474b933288cec2f2a50121ba995173e8d601dbb1b2d9a784c00739d3000008001000080000000800300008001000000000000002107dbe4edc0edbcb5f212c9fa1d1bcbdb4b3f6e875a5ea52ac3cafed4057379302b3d014b40a0f212ea2f428474b933288cec2f2a50121ba995173e8d601dbb1b2d9a7808f56e293000008001000080000000800300008001000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "tr(@0/<0;1>/*,multi_a(2,@1/<0;1>/*,@2/<0;1>/*))", + "keys": [ + { + "xpub": "tpubD6NzVbkrYhZ4XnyFMCH9qsN7VhcUSwyj5MN4juKrFBbgE9e3qCpEQsHSmq4zHA2FAkSwBQEzFGKjvXtb2xrRLioqRdqAkqN3Y9WrBf7yNj8" + }, + { + "xpub": "tpubDF93VcTYKK2grTowjbqJMst4gS19G6rtWTNKcBnxCpQfs2BaFShNYoBDisxqhTsBWj4YhigxVArhhZN43NyWBH4E3puXS7tCrHsbjb2dvBi" + }, + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + } + ] + }, + "keypath": "m/48'/1'/0'/3'" + } + } + }, + "expected_needs_prevtxs": false, + "expectations": [ + { + "max_version_exclusive": "9.21.0", + "outcome": "unsupported", + "unsupported_version": "9.21.0", + "screens": [] + }, + { + "min_version": "9.21.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n3 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test unspendable policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/<0;1>/*,multi_a(2,@1/<0;1>/*,@2/<0;1>/*))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/3", + "body": "Provably unspendable: tpubD6NzVbkrYhZ4XnyFMCH9qsN7VhcUSwyj5MN4juKrFBbgE9e3qCpEQsHSmq4zHA2FAkSwBQEzFGKjvXtb2xrRLioqRdqAkqN3Y9WrBf7yNj8", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/3", + "body": "tpubDF93VcTYKK2grTowjbqJMst4gS19G6rtWTNKcBnxCpQfs2BaFShNYoBDisxqhTsBWj4YhigxVArhhZN43NyWBH4E3puXS7tCrHsbjb2dvBi", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 3/3", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n3 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test unspendable policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/<0;1>/*,multi_a(2,@1/<0;1>/*,@2/<0;1>/*))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/3", + "body": "Provably unspendable: tpubD6NzVbkrYhZ4XnyFMCH9qsN7VhcUSwyj5MN4juKrFBbgE9e3qCpEQsHSmq4zHA2FAkSwBQEzFGKjvXtb2xrRLioqRdqAkqN3Y9WrBf7yNj8", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/3", + "body": "tpubDF93VcTYKK2grTowjbqJMst4gS19G6rtWTNKcBnxCpQfs2BaFShNYoBDisxqhTsBWj4YhigxVArhhZN43NyWBH4E3puXS7tCrHsbjb2dvBi", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 3/3", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "tr(@0/<0;1>/*,multi_a(2,@1/<0;1>/*,@2/<0;1>/*))", + "keys": [ + { + "xpub": "tpubD6NzVbkrYhZ4XnyFMCH9qsN7VhcUSwyj5MN4juKrFBbgE9e3qCpEQsHSmq4zHA2FAkSwBQEzFGKjvXtb2xrRLioqRdqAkqN3Y9WrBf7yNj8" + }, + { + "xpub": "tpubDF93VcTYKK2grTowjbqJMst4gS19G6rtWTNKcBnxCpQfs2BaFShNYoBDisxqhTsBWj4YhigxVArhhZN43NyWBH4E3puXS7tCrHsbjb2dvBi" + }, + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + } + ] + }, + "name": "test unspendable policy" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_script", + "pubkey": "3a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2", + "leaf_hash": "e0ed23b292365b738059122f0e9f6887887b474a1e9593cb55e872c7a0c08558", + "sighash": "default" + } + ] + }, + { + "id": "policy-tr-unspendable-internal-key-complex", + "description": "Signs the satisfiable branch of a multi-leaf Taproot policy with a provably unspendable internal key, distinct multipaths and a relative-timelock sibling branch.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01008902000000017079ce1d2dffec9e935d2a74cd4a702f722e651939daeb27faf3953f14fa5c3a0000000000ffffffff02801d2c04000000002251203ad5b7757d2dc6cd5e5ec17a0603352d401de5d69fc48f9c6ec1a3ac2b8e194f002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001012b00e1f50500000000225120eaef7e275cce42763c22a441d0e5b254c794d51e8123f5562441346dac40848c4114eeab1c2d28079589eb9e55bbd86765ffce87514a1feca661ee3d88f0e04643c9b2edc93766200490b5d77842a9424fd5686d22c4c14fb13dec8784164160521a404eb58edfa3eec62bf0fccefc4e73dafb7e74009a81e22a24ea6f1dae95e558273eb0f851610819384de73f327e2ffe0d3751a8b785f9db8648d2bb074559eaef4215c159165ae2e78cafa08859f5d42370b903e931f4d0d76247a3ecdcdeffc3eed3309e075b5976239a09b6b08d128ec95a42f328a625c26213f9568d7768d853f4974720eeab1c2d28079589eb9e55bbd86765ffce87514a1feca661ee3d88f0e04643c9ac203a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2ba529cc04215c159165ae2e78cafa08859f5d42370b903e931f4d0d76247a3ecdcdeffc3eed330b2edc93766200490b5d77842a9424fd5686d22c4c14fb13dec8784164160521a4920ca89423a6e0a0673b51bfd05212d8963c14a8db6cd3d7cba5dabaff97d4e3ed8ac201d7d773d42ed516b73555c715929badf8ddb135b25779b475157a49ed946af79ba519d52b2c021163a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b23d01b2edc93766200490b5d77842a9424fd5686d22c4c14fb13dec8784164160521a4c00739d300000800100008000000080030000800000000000000000211659165ae2e78cafa08859f5d42370b903e931f4d0d76247a3ecdcdeffc3eed3300d007c461e5d00000000000000002116eeab1c2d28079589eb9e55bbd86765ffce87514a1feca661ee3d88f0e04643c93d01b2edc93766200490b5d77842a9424fd5686d22c4c14fb13dec8784164160521a4040280c30000080010000800000008002000080000000000000000001172059165ae2e78cafa08859f5d42370b903e931f4d0d76247a3ecdcdeffc3eed3300118207754188d95aa29ae86dcf87406dfb5ed0e1d6da02a9ad5104b95f5f81abf79a4000105209ba26f33424bc7330bf3f683129516387fdc20f5baf09046ea9eaa2ed71516d401069401c0482006048aec624d548af178a98ec862e0efaa5c2fcf8fb07f5146f410918eb21eefac20c621b132507c49d0f974e71d89e3017fe4f4ca8aa7c73c291d7a4edd1a6399a6ba519d52b201c04620ddad6824617b6a1bd330aab5c263f039b7ebe04196792fe90d203752e9c37809ac208c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44aba529c21078c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a3d01d3064571589c8c4e858bc4dd2efe190b4e2d9d3da7d96306eb9702e5401d2f264c00739d30000080010000800000008003000080010000000000000021079ba26f33424bc7330bf3f683129516387fdc20f5baf09046ea9eaa2ed71516d40d007c461e5d01000000000000002107ddad6824617b6a1bd330aab5c263f039b7ebe04196792fe90d203752e9c378093d01d3064571589c8c4e858bc4dd2efe190b4e2d9d3da7d96306eb9702e5401d2f264040280c3000008001000080000000800200008001000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "tr(@0/<0;1>/*,{and_v(v:multi_a(1,@1/<2;3>/*,@2/<2;3>/*),older(2)),multi_a(2,@1/<0;1>/*,@2/<0;1>/*)})", + "keys": [ + { + "xpub": "tpubD6NzVbkrYhZ4YRYppNDw8BpfEz6cSU8qt45Eoqpj8YtPLFXoZsb3dYdAyLFp32GeY3b5ccVdtr45ajBGBDHgapb53xT9kMBcpHSF7yA8kXm" + }, + { + "root_fingerprint": "4040280c", + "keypath": "m/48'/1'/0'/2'", + "xpub": "tpubDEWR7iVng7GH7BhbVBa6avnELDTSWEWHtAethvSrYYLEFm1XB1qT4Fb9xkPGZ9S7zU1FFrpVc6wRSo5BRNYvDAP7Edr5a6FJSjeCt7tWgQR" + }, + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + } + ] + }, + "keypath": "m/48'/1'/0'/3'" + } + } + }, + "expected_needs_prevtxs": false, + "expectations": [ + { + "max_version_exclusive": "9.21.0", + "outcome": "unsupported", + "unsupported_version": "9.21.0", + "screens": [] + }, + { + "min_version": "9.21.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n3 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test complex unspendable", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/<0;1>/*,{and_v(v:multi_a(1,@1/<2;3>/*,@2/<2;3>/*),older(2)),multi_a(2,@1/<0;1>/*,@2/<0;1>/*)})", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/3", + "body": "Provably unspendable: tpubD6NzVbkrYhZ4YRYppNDw8BpfEz6cSU8qt45Eoqpj8YtPLFXoZsb3dYdAyLFp32GeY3b5ccVdtr45ajBGBDHgapb53xT9kMBcpHSF7yA8kXm", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/3", + "body": "[4040280c/48'/1'/0'/2']tpubDEWR7iVng7GH7BhbVBa6avnELDTSWEWHtAethvSrYYLEFm1XB1qT4Fb9xkPGZ9S7zU1FFrpVc6wRSo5BRNYvDAP7Edr5a6FJSjeCt7tWgQR", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 3/3", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n3 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test complex unspendable", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "tr(@0/<0;1>/*,{and_v(v:multi_a(1,@1/<2;3>/*,@2/<2;3>/*),older(2)),multi_a(2,@1/<0;1>/*,@2/<0;1>/*)})", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/3", + "body": "Provably unspendable: tpubD6NzVbkrYhZ4YRYppNDw8BpfEz6cSU8qt45Eoqpj8YtPLFXoZsb3dYdAyLFp32GeY3b5ccVdtr45ajBGBDHgapb53xT9kMBcpHSF7yA8kXm", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/3", + "body": "[4040280c/48'/1'/0'/2']tpubDEWR7iVng7GH7BhbVBa6avnELDTSWEWHtAethvSrYYLEFm1XB1qT4Fb9xkPGZ9S7zU1FFrpVc6wRSo5BRNYvDAP7Edr5a6FJSjeCt7tWgQR", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 3/3", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "tr(@0/<0;1>/*,{and_v(v:multi_a(1,@1/<2;3>/*,@2/<2;3>/*),older(2)),multi_a(2,@1/<0;1>/*,@2/<0;1>/*)})", + "keys": [ + { + "xpub": "tpubD6NzVbkrYhZ4YRYppNDw8BpfEz6cSU8qt45Eoqpj8YtPLFXoZsb3dYdAyLFp32GeY3b5ccVdtr45ajBGBDHgapb53xT9kMBcpHSF7yA8kXm" + }, + { + "root_fingerprint": "4040280c", + "keypath": "m/48'/1'/0'/2'", + "xpub": "tpubDEWR7iVng7GH7BhbVBa6avnELDTSWEWHtAethvSrYYLEFm1XB1qT4Fb9xkPGZ9S7zU1FFrpVc6wRSo5BRNYvDAP7Edr5a6FJSjeCt7tWgQR" + }, + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + } + ] + }, + "name": "test complex unspendable" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "taproot_script", + "pubkey": "3a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2", + "leaf_hash": "b2edc93766200490b5d77842a9424fd5686d22c4c14fb13dec8784164160521a", + "sighash": "default" + } + ] + }, + { + "id": "policy-different-multipath-derivations", + "description": "Signs and finalizes a registered WSH policy whose two keys use different receive and change branches.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff010089020000000149aa85e2c639e52c4984cba528c60ef347f47f35917519ecba279514841928470000000000ffffffff02801d2c0400000000220020d0a6b11cf5a05594e6fad40948646011c6307c84906d318f348f5effaaa97b02002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005e02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000220020e267e95e105bd40e8fbf3bdda795da8bc943949f4accec407d45b469f514b4830000000001012b00e1f50500000000220020e267e95e105bd40e8fbf3bdda795da8bc943949f4accec407d45b469f514b483220203e7d88a65b1ca31ccc898193ab72acdce9b0c0076a9ba8f4ef06bd0ccd4d01d034730440220182d262d790be3d24e15dd8e74057bacf77f678ef0e82bdecce4386ab3252a7c02205651ad4a74158357b08c05bd88dec8c2522bb91013332f963374b45722c842a401010547522102b95c8bf277d3e7d7de1ad4fbb4921c5bd1cefeb3ebf38987bdd4688b9cfcbb722103e7d88a65b1ca31ccc898193ab72acdce9b0c0076a9ba8f4ef06bd0ccd4d01d0352ae220602b95c8bf277d3e7d7de1ad4fbb4921c5bd1cefeb3ebf38987bdd4688b9cfcbb721c4c00739d300000800100008000000080030000800a00000000000000220603e7d88a65b1ca31ccc898193ab72acdce9b0c0076a9ba8f4ef06bd0ccd4d01d031cc5e4fe1a300000800100008000000080030000801400000000000000000101475221034c130edc7e1574821af949c79d64052475e85f7f659cbbbd4b99c79bdf840dc92102627713c28bf52eb7012ac66a69b333fca5f9c8794fb5751ceb0cb50484546b3952ae220202627713c28bf52eb7012ac66a69b333fca5f9c8794fb5751ceb0cb50484546b391cc5e4fe1a3000008001000080000000800300008015000000000000002202034c130edc7e1574821af949c79d64052475e85f7f659cbbbd4b99c79bdf840dc91c4c00739d300000800100008000000080030000800b000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "wsh(multi(2,@0/<10;11>/*,@1/<20;21>/*))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y" + } + ] + }, + "keypath": "m/48'/1'/0'/3'" + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "max_version_exclusive": "9.20.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test multipath policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "wsh(multi(2,@0/<10;11>/*,@1/<20;21>/*))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.20.0", + "max_version_exclusive": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test multipath policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "wsh(multi(2,@0/<10;11>/*,@1/<20;21>/*))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1pff8vkq80pu2cgtu7ttgad2znw62v2lguhw6ptrppwns6nrpqau2qcuz37d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + }, + { + "min_version": "9.26.0", + "outcome": "success", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test multipath policy", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "wsh(multi(2,@0/<10;11>/*,@1/<20;21>/*))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y", + "longtouch": false + }, + { + "type": "transaction_address", + "amount": "0.20000000 TBTC", + "address": "tb1p ff8v kq80 pu2c gtu7 ttga d2zn w62v 2lgu hw6p trpp wns6 nrpq au2q cuz3 7d" + }, + { + "type": "transaction_fee", + "amount": "0.30000000 TBTC", + "fee": "0.10000000 TBTC", + "longtouch": false + }, + { + "type": "confirm", + "title": "High fee", + "body": "The fee is 50.0%\nthe send amount.\nProceed?", + "longtouch": true + }, + { + "type": "status", + "title": "Transaction", + "body": "confirmed" + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "wsh(multi(2,@0/<10;11>/*,@1/<20;21>/*))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y" + } + ] + }, + "name": "test multipath policy" + } + ], + "expected_signatures": [ + { + "input_index": 0, + "kind": "ecdsa", + "pubkey": "02b95c8bf277d3e7d7de1ad4fbb4921c5bd1cefeb3ebf38987bdd4688b9cfcbb72", + "sighash": "all" + } + ] + }, + { + "id": "policy-wrong-account-keypath", + "description": "Rejects a registered policy when the signing account keypath does not match the device key in the policy.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01008902000000011b6e58cffa2f032e498aa516a97410d53fc282bee867fb9653192aa223bd02de0000000000ffffffff02801d2c040000000022002032001963e09213e7387b90c3374944bdbbaf69f554060bb040626f0b428f1850002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005e02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000220020f5531a34c3dfbcded4d3717e6019b88c4878f964df8c9bf54b3f0256d591e7aa0000000001012b00e1f50500000000220020f5531a34c3dfbcded4d3717e6019b88c4878f964df8c9bf54b3f0256d591e7aa0105475221033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2210234c5ed02e3e62b122655859db72a9a896aa261e8b56dc26c5ce7459ffcc08de252ae22060234c5ed02e3e62b122655859db72a9a896aa261e8b56dc26c5ce7459ffcc08de21cc5e4fe1a3000008001000080000000800300008000000000000000002206033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b21c4c00739d300000800100008000000080030000800000000000000000000101475221028c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a21023df70ec9451b9dbf9b910e974fc8f060abfe662091836e09f3acd9017c04563b52ae2202023df70ec9451b9dbf9b910e974fc8f060abfe662091836e09f3acd9017c04563b1cc5e4fe1a3000008001000080000000800300008001000000000000002202028c35dddf5bd951db2bf053d944200d11f029f37af9b5255025338f7eb9aae44a1c4c00739d3000008001000080000000800300008001000000000000000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "wsh(multi(2,@0/**,@1/**))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y" + } + ] + }, + "keypath": "m/48'/1'/0'/4'" + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "outcome": "invalid_input", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test policy account", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "wsh(multi(2,@0/**,@1/**))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y", + "longtouch": false + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "wsh(multi(2,@0/**,@1/**))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y" + } + ] + }, + "name": "test policy account" + } + ] + }, + { + "id": "policy-change-index-too-high", + "description": "Rejects a registered policy change output at address index 10000.", + "coin": "tbtc", + "psbt": { + "transaction": "70736274ff01008902000000011b6e58cffa2f032e498aa516a97410d53fc282bee867fb9653192aa223bd02de0000000000ffffffff02801d2c0400000000220020bd6a0b3bc28769ca2a7ef1aa12f7c052beb977fd61a1c55a9e5a3d6cecce0cbb002d3101000000002251204a4ecb00ef0f15842f9e5ad1d6a8537694c57d1cbbb4158c2174e1a98c20ef14000000000001005e02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff00ffffffff0100e1f50500000000220020f5531a34c3dfbcded4d3717e6019b88c4878f964df8c9bf54b3f0256d591e7aa0000000001012b00e1f50500000000220020f5531a34c3dfbcded4d3717e6019b88c4878f964df8c9bf54b3f0256d591e7aa0105475221033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b2210234c5ed02e3e62b122655859db72a9a896aa261e8b56dc26c5ce7459ffcc08de252ae22060234c5ed02e3e62b122655859db72a9a896aa261e8b56dc26c5ce7459ffcc08de21cc5e4fe1a3000008001000080000000800300008000000000000000002206033a1712b41eb509147fbbd80c3fac4af4ad0f0eaeaef36e6e08918002afd8e9b21c4c00739d3000008001000080000000800300008000000000000000000001014752210252631b8aca4b08e95acacae7daf3bfdad38a8651821a1009008fdd04a90d17bc210275db5c17fe98b071caf5e724752d268263106509e2bc2d3fa468168702329e9452ae22020252631b8aca4b08e95acacae7daf3bfdad38a8651821a1009008fdd04a90d17bc1c4c00739d30000080010000800000008003000080010000001027000022020275db5c17fe98b071caf5e724752d268263106509e2bc2d3fa468168702329e941cc5e4fe1a3000008001000080000000800300008001000000102700000000", + "options": { + "force_script_config": { + "script_config": { + "type": "policy", + "policy": "wsh(multi(2,@0/**,@1/**))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y" + } + ] + }, + "keypath": "m/48'/1'/0'/3'" + } + } + }, + "expected_needs_prevtxs": true, + "expectations": [ + { + "outcome": "invalid_input", + "screens": [ + { + "type": "confirm", + "title": "Spend from", + "body": "BTC Testnet\npolicy with\n2 keys", + "longtouch": false + }, + { + "type": "confirm", + "title": "Name", + "body": "test policy account", + "longtouch": false + }, + { + "type": "confirm", + "title": "", + "body": "Show policy\ndetails?", + "longtouch": false + }, + { + "type": "confirm", + "title": "Policy", + "body": "wsh(multi(2,@0/**,@1/**))", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 1/2", + "body": "This device: [4c00739d/48'/1'/0'/3']tpubDF5MSzQdK2GfjmkNvrCZzpJhFt3if1HmrAdimugmGqWDCXYpkjxHpFZYuDxYYDAnnFMLMjLkMGvij2XV8pLtHBejgGy5RvNW4875nFGBDWv", + "longtouch": false + }, + { + "type": "confirm", + "title": "Key 2/2", + "body": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y", + "longtouch": false + } + ] + } + ], + "registrations": [ + { + "script_config": { + "type": "policy", + "policy": "wsh(multi(2,@0/**,@1/**))", + "keys": [ + { + "root_fingerprint": "4c00739d", + "keypath": "m/48'/1'/0'/3'", + "xpub": "xpub6EhivkawbkKDTyZXUfDUntyKvkx4fcniJY3RRMdcvvkKTEt7gX7CfotifBsm1iDYzLpRk9pspB6g2r2sjxVePkCScgDmkPiGUAWg2ESMKNA" + }, + { + "xpub": "tpubDFfyBnQAERJZWHv4W5rrhkpKJnPjycGzBpv8H1WgFjpkK7bVpuKw6JkeNvXqJix6csrQP3QgPZ19Yed2kbc7sniVHtNiQkX3cHsBxEqnz7Y" + } + ] + }, + "name": "test policy account" + } + ] + } + ] +} diff --git a/test/error-codes.test.ts b/test/error-codes.test.ts index 1891fd2..b026d6f 100644 --- a/test/error-codes.test.ts +++ b/test/error-codes.test.ts @@ -27,6 +27,7 @@ import { CODE_NOT_IMPLEMENTED, CODE_PAIRING_REJECTED, CODE_PROTOBUF_DECODE, + CODE_PSBT_INVALID_ACCOUNT_KEYPATH, CODE_PSBT_INVALID_OP_RETURN, CODE_PSBT_KEY_NOT_FOUND, CODE_PSBT_KEY_NOT_UNIQUE, @@ -146,6 +147,10 @@ const EXPECTED = { code: CODE_PSBT_INVALID_OP_RETURN, message: 'PSBT error: Invalid OP_RETURN script: ', }, + psbtInvalidAccountKeypath: { + code: CODE_PSBT_INVALID_ACCOUNT_KEYPATH, + message: 'PSBT error: Account script configs must contain a BIP44 account keypath.', + }, } satisfies Record; function publicShape(err: { code: string; message: string }): ExpectedError { @@ -192,6 +197,7 @@ describe('error code fixture', () => { EXPECTED.psbtKeyNotFound, EXPECTED.psbtUnknownOutputType, EXPECTED.psbtInvalidOpReturn, + EXPECTED.psbtInvalidAccountKeypath, ]).toEqual([ { code: CODE_UNKNOWN, message: 'unknown error' }, { code: CODE_NOISE, message: 'noise channel error' }, @@ -209,6 +215,10 @@ describe('error code fixture', () => { { code: CODE_PSBT_KEY_NOT_FOUND, message: 'PSBT error: Could not find our key in an input.' }, { code: CODE_PSBT_UNKNOWN_OUTPUT_TYPE, message: 'PSBT error: Unrecognized/unsupported output type.' }, { code: CODE_PSBT_INVALID_OP_RETURN, message: 'PSBT error: Invalid OP_RETURN script: ' }, + { + code: CODE_PSBT_INVALID_ACCOUNT_KEYPATH, + message: 'PSBT error: Account script configs must contain a BIP44 account keypath.', + }, ]); }); @@ -282,6 +292,14 @@ describe('toPublicError', () => { CODE_ANTIKLEPTO, CODE_ETH_TYPED_MESSAGE, CODE_CHAIN_ID_TOO_LARGE, + CODE_BTC_SIGN, + CODE_PSBT_PARSE, + CODE_PSBT_SIGN_ERROR, + CODE_PSBT_KEY_NOT_UNIQUE, + CODE_PSBT_KEY_NOT_FOUND, + CODE_PSBT_UNKNOWN_OUTPUT_TYPE, + CODE_PSBT_INVALID_OP_RETURN, + CODE_PSBT_INVALID_ACCOUNT_KEYPATH, CODE_UNSUPPORTED, CODE_NOT_IMPLEMENTED, CODE_INVALID_STATE, diff --git a/test/lifecycle.test.ts b/test/lifecycle.test.ts index 3d4ce50..5c3c961 100644 --- a/test/lifecycle.test.ts +++ b/test/lifecycle.test.ts @@ -302,7 +302,9 @@ describe('PairedBitBox lifecycle', () => { expect(paired.product()).toBe('bitbox02-multi'); await expect(paired.changePassword()).rejects.toMatchObject({ code: 'not-implemented' }); - await expect(paired.btcXpub('btc', [0], 'xpub', false)).rejects.toMatchObject({ code: 'unsupported' }); + await expect(paired.btcXpub('btc', [0], 'xpub', false)).rejects.toMatchObject({ + code: 'unexpected-response', + }); paired.close(); expect(close).toHaveBeenCalledTimes(1); diff --git a/test/simulator-btc.test.ts b/test/simulator-btc.test.ts new file mode 100644 index 0000000..70e9193 --- /dev/null +++ b/test/simulator-btc.test.ts @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { bytesToHex, concatBytes } from '@noble/hashes/utils'; +import { + crypto as bitcoinCrypto, + initEccLib, + networks, + payments, + Psbt, + script as bitcoinScript, + Transaction as BitcoinTransaction, +} from 'bitcoinjs-lib'; +import * as ecc from 'tiny-secp256k1'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { + PairedBitBox, + type BtcScriptConfig, + type BtcScriptConfigWithKeypath, + type Error as BitBoxError, +} from '../src/index.js'; +import { connectSimulator } from '../src/internal/connect-simulator.js'; +import { atLeast, parseSemver } from '../src/internal/hww.js'; +import { NoiseConfigNoCache } from '../src/internal/noise-config.js'; +import { completePairing, performHandshake } from '../src/internal/pairing.js'; +import { restoreFromMnemonic } from '../src/internal/restore.js'; +import { + SimulatorServer, + ensureSimulator, + simulatorCases, + simulatorSupported, + type SimulatorScreen, +} from './simulator-util.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +initEccLib(ecc); +const vectorFile = JSON.parse(readFileSync( + path.join(__dirname, 'data/btc-transaction-test-vectors.json'), + 'utf8', +)) as TestVectorFile; + +const ENABLED = simulatorSupported() && process.env.SKIP_SIMULATOR !== '1'; +type Semver = ReturnType; + +type VectorScriptConfig = + | { type: 'simple'; script_type: 'p2wpkh' | 'p2wpkh_p2sh' | 'p2tr' } + | { + type: 'multisig'; + threshold: number; + xpubs: string[]; + our_xpub_index: number; + script_type: 'p2wsh' | 'p2wsh_p2sh'; + } + | { + type: 'policy'; + policy: string; + keys: { + root_fingerprint?: string; + keypath?: string; + xpub: string; + }[]; + }; + +type SignatureSlot = { + input_index: number; + kind: 'ecdsa' | 'taproot_key' | 'taproot_script'; + pubkey?: string; + leaf_hash?: string; + sighash: 'all' | 'default'; +}; + +type VectorScreen = SimulatorScreen & { longtouch?: boolean }; + +interface VersionExpectation { + min_version?: string; + max_version_exclusive?: string; + outcome: 'success' | 'unsupported' | 'invalid_input'; + unsupported_version?: string; + screens: VectorScreen[]; +} + +interface TestVector { + id: string; + description: string; + coin: 'btc' | 'tbtc' | 'ltc'; + psbt: { + transaction: string; + options?: { + force_script_config?: { + script_config: VectorScriptConfig; + keypath: string; + }; + outputs?: Record; + payment_requests?: unknown[]; + format_unit?: 'default' | 'sat'; + }; + }; + expectations: VersionExpectation[]; + registrations?: { + script_config: VectorScriptConfig; + keypath?: string; + name: string; + }[]; + expected_signatures?: SignatureSlot[]; +} + +interface TestVectorFile { + vectors: TestVector[]; +} + +async function connectRestored(): Promise { + const session = await connectSimulator(undefined, undefined, new NoiseConfigNoCache()); + try { + const pairing = await performHandshake(session.hww, session.config); + const channel = await completePairing(pairing); + await restoreFromMnemonic(channel); + return new PairedBitBox({ channel, info: session.hww.info, close: session.close }); + } catch (error) { + session.close(); + throw error; + } +} + +function bitcoinMessageHash(message: Uint8Array): Uint8Array { + // bitcoinjs-lib does not include the legacy signed-message hash helper. + if (message.length >= 0xfd) { + throw new Error('test message is too long for a one-byte CompactSize'); + } + return bitcoinCrypto.hash256(concatBytes( + new TextEncoder().encode(networks.bitcoin.messagePrefix), + Uint8Array.of(message.length), + message, + )); +} + +function scriptConfig(config: VectorScriptConfig): BtcScriptConfig { + switch (config.type) { + case 'simple': + return { + simpleType: { + p2wpkh: 'p2wpkh', + p2wpkh_p2sh: 'p2wpkhP2sh', + p2tr: 'p2tr', + }[config.script_type] as 'p2wpkh' | 'p2wpkhP2sh' | 'p2tr', + }; + case 'multisig': + return { + multisig: { + threshold: config.threshold, + xpubs: config.xpubs, + ourXpubIndex: config.our_xpub_index, + scriptType: config.script_type === 'p2wsh' ? 'p2wsh' : 'p2wshP2sh', + }, + }; + case 'policy': + return { + policy: { + policy: config.policy, + keys: config.keys.map(key => ({ + xpub: key.xpub, + ...(key.root_fingerprint === undefined + ? {} + : { rootFingerprint: key.root_fingerprint }), + ...(key.keypath === undefined ? {} : { keypath: key.keypath }), + })), + }, + }; + } +} + +function scriptConfigWithKeypath( + config: NonNullable['force_script_config'], +): BtcScriptConfigWithKeypath | undefined { + if (config === undefined) { + return undefined; + } + return { scriptConfig: scriptConfig(config.script_config), keypath: config.keypath }; +} + +function expectationFor( + expectations: VersionExpectation[], + version: Semver, +): VersionExpectation { + const found = expectations.find((expectation) => { + const afterMinimum = expectation.min_version === undefined || + atLeast(version, parseSemver(expectation.min_version)); + const beforeMaximum = expectation.max_version_exclusive === undefined || + !atLeast(version, parseSemver(expectation.max_version_exclusive)); + return afterMinimum && beforeMaximum; + }); + if (found === undefined) { + throw new Error('no vector expectation matches the firmware version'); + } + return found; +} + +function expectedScreens(screens: VectorScreen[]): SimulatorScreen[] { + return screens.map(({ longtouch: _longtouch, ...screen }) => screen); +} + +function signatureSlots(psbt: Psbt): SignatureSlot[] { + const slots: SignatureSlot[] = []; + psbt.data.inputs.forEach((input, inputIndex) => { + for (const partialSig of input.partialSig ?? []) { + const decoded = bitcoinScript.signature.decode(partialSig.signature); + slots.push({ + input_index: inputIndex, + kind: 'ecdsa', + pubkey: bytesToHex(partialSig.pubkey), + sighash: decoded.hashType === BitcoinTransaction.SIGHASH_ALL ? 'all' : 'default', + }); + } + if (input.tapKeySig !== undefined) { + slots.push({ + input_index: inputIndex, + kind: 'taproot_key', + ...(input.tapInternalKey === undefined + ? {} + : { pubkey: bytesToHex(input.tapInternalKey) }), + sighash: input.tapKeySig.length === 64 ? 'default' : 'all', + }); + } + for (const tapScriptSig of input.tapScriptSig ?? []) { + slots.push({ + input_index: inputIndex, + kind: 'taproot_script', + pubkey: bytesToHex(tapScriptSig.pubkey), + leaf_hash: bytesToHex(tapScriptSig.leafHash), + sighash: tapScriptSig.signature.length === 64 ? 'default' : 'all', + }); + } + }); + return slots.sort((left, right) => slotKey(left).localeCompare(slotKey(right))); +} + +function slotKey(slot: SignatureSlot): string { + return [ + slot.input_index, + slot.kind, + slot.pubkey ?? '', + slot.leaf_hash ?? '', + slot.sighash, + ].join(':'); +} + +function assertSignatureInsertions( + vector: TestVector, + before: SignatureSlot[], + after: SignatureSlot[], +): void { + const beforeKeys = new Set(before.map(slotKey)); + const afterKeys = new Set(after.map(slotKey)); + for (const key of beforeKeys) { + expect(afterKeys.has(key), `${vector.id}: signing removed a signature slot`).toBe(true); + } + const inserted = after.filter(slot => !beforeKeys.has(slotKey(slot))); + const expected = [...(vector.expected_signatures ?? [])] + .sort((left, right) => slotKey(left).localeCompare(slotKey(right))); + expect(inserted, `${vector.id}: inserted signature slots differ`).toEqual(expected); +} + +function verifyAndFinalize(vector: TestVector, signed: Psbt): void { + const fundingOutputs = signed.data.inputs.map((input, inputIndex) => { + if (input.witnessUtxo !== undefined) { + return input.witnessUtxo; + } + if (input.nonWitnessUtxo === undefined) { + throw new Error(`${vector.id}: input ${inputIndex} has no funding output`); + } + const output = BitcoinTransaction.fromBuffer(input.nonWitnessUtxo) + .outs[signed.txInputs[inputIndex]!.index]; + if (output === undefined) { + throw new Error(`${vector.id}: funding output index is out of bounds`); + } + return output; + }); + expect(signed.validateSignaturesOfAllInputs((pubkey, hash, signature) => + pubkey.length === 32 + ? ecc.verifySchnorr(hash, pubkey, signature) + : ecc.verify(hash, pubkey, signature), + ), `${vector.id}: transaction signature is invalid`).toBe(true); + + const finalized = signed.clone(); + signed.data.inputs.forEach((input, inputIndex) => { + if (input.witnessUtxo === undefined) { + finalized.updateInput(inputIndex, { witnessUtxo: fundingOutputs[inputIndex]! }); + } + }); + try { + finalized.finalizeAllInputs(); + } catch (error) { + // bitcoinjs-lib has no miniscript satisfier. Signatures above are still checked + // cryptographically for policy vectors that it cannot finalize. + if (vector.psbt.options?.force_script_config?.script_config.type === 'policy') { + return; + } + throw error; + } + const transaction = finalized.extractTransaction(true); + expect(transaction.ins).toHaveLength(signed.txInputs.length); + expect(transaction.outs).toHaveLength(signed.txOutputs.length); + for (const input of transaction.ins) { + expect(input.script.length > 0 || input.witness.length > 0).toBe(true); + } +} + +function shouldSkip(vector: TestVector): boolean { + const outputs = vector.psbt.options?.outputs ?? {}; + const paymentRequests = vector.psbt.options?.payment_requests ?? []; + return Object.keys(outputs).length > 0 || paymentRequests.length > 0; +} + +async function registerScriptConfigs( + bitbox: PairedBitBox, + vector: TestVector, +): Promise { + for (const registration of vector.registrations ?? []) { + const config = scriptConfig(registration.script_config); + const registered = await bitbox.btcIsScriptConfigRegistered( + vector.coin, + config, + registration.keypath, + ); + if (!registered) { + await bitbox.btcRegisterScriptConfig( + vector.coin, + config, + registration.keypath, + 'autoXpubTpub', + registration.name, + ); + } + } +} + +function assertUnsupported( + vector: TestVector, + version: Semver, + expectation: VersionExpectation, + error: BitBoxError, +): void { + const minimum = expectation.unsupported_version; + expect(minimum, `${vector.id}: missing unsupported firmware version`).toBeDefined(); + expect(atLeast(version, parseSemver(minimum!))).toBe(false); + expect( + ['version', 'bitbox-invalid-input', 'bitbox-disabled'], + `${vector.id}: unexpected unsupported error ${error.code}`, + ).toContain(error.code); +} + +async function runVector( + bitbox: PairedBitBox, + server: SimulatorServer, + vector: TestVector, +): Promise { + const version = parseSemver(bitbox.version()); + const expectation = expectationFor(vector.expectations, version); + const original = Psbt.fromHex(vector.psbt.transaction); + const base64 = original.toBase64(); + const before = signatureSlots(original); + + const setupCheckpoint = server.stdout.checkpoint(); + let setupError: BitBoxError | undefined; + try { + await registerScriptConfigs(bitbox, vector); + } catch (error) { + setupError = error as BitBoxError; + } + await server.stdout.waitUntilStable(setupCheckpoint); + if (setupError !== undefined) { + if (expectation.outcome === 'unsupported') { + assertUnsupported(vector, version, expectation, setupError); + } else if (expectation.outcome === 'invalid_input') { + expect(setupError.code).toBe('bitbox-invalid-input'); + } else { + throw setupError; + } + expect(expectedScreens(expectation.screens)).toEqual([]); + expect(signatureSlots(original)).toEqual(before); + return; + } + + const transactionCheckpoint = server.stdout.checkpoint(); + let signedBase64: string | undefined; + let signError: BitBoxError | undefined; + try { + signedBase64 = await bitbox.btcSignPSBT( + vector.coin, + base64, + scriptConfigWithKeypath(vector.psbt.options?.force_script_config), + vector.psbt.options?.format_unit ?? 'default', + ); + } catch (error) { + signError = error as BitBoxError; + } + const snapshot = expectation.outcome === 'success' + ? await server.stdout.waitForTerminalScreen(transactionCheckpoint) + : await server.stdout.waitUntilStable(transactionCheckpoint); + const actualScreens = snapshot.screens(); + if (process.env.UPDATE_BTC_VECTOR_SCREENS === '1') { + process.stdout.write( + `screens for ${vector.id} on firmware ${bitbox.version()}: ${JSON.stringify(actualScreens)}\n`, + ); + } else { + expect(actualScreens, `${vector.id}: simulator screens differ`).toEqual( + expectedScreens(expectation.screens), + ); + } + + switch (expectation.outcome) { + case 'success': { + if (signError !== undefined || signedBase64 === undefined) { + throw signError ?? new Error(`${vector.id}: signing returned no PSBT`); + } + const signed = Psbt.fromBase64(signedBase64); + assertSignatureInsertions(vector, before, signatureSlots(signed)); + verifyAndFinalize(vector, signed); + break; + } + case 'unsupported': + expect(signError).toBeDefined(); + assertUnsupported(vector, version, expectation, signError!); + expect(signatureSlots(original)).toEqual(before); + break; + case 'invalid_input': + expect(signError?.code).toBe('bitbox-invalid-input'); + expect(signatureSlots(original)).toEqual(before); + break; + } +} + +describe.skipIf(!ENABLED).sequential.each(simulatorCases())('simulator BTC $name', (simulator) => { + let server: SimulatorServer | undefined; + let binary = ''; + + beforeAll(async () => { + binary = await ensureSimulator(simulator); + }, 120_000); + + beforeEach(() => { + server = new SimulatorServer(binary); + }); + + afterEach(async () => { + await server?.stop(); + server = undefined; + }, 30_000); + + it('supports xpubs, addresses, and message signing', async () => { + const bitbox = await connectRestored(); + try { + await expect(bitbox.btcXpub( + 'tbtc', + "m/49'/1'/0'", + 'ypub', + false, + )).resolves.toBe( + 'ypub6WqXiL3fbDK5QNPe3hN4uSVkEvuE8wXoNCcecgggSuKVpU3Kc4fTvhuLgUhtnbAdaTb9gpz5PQdvzcsKPTLgW2CPkF5ZNRzQeKFT4NSc1xN', + ); + + await expect(bitbox.btcXpubs( + 'tbtc', + ["m/49'/1'/0'", "m/84'/1'/0'", "m/86'/1'/0'"], + 'tpub', + )).resolves.toEqual([ + 'tpubDCNtvuCS9oj3psPNfXZXuGjcQ5rSBi3MzigjBqqwQohWWetoRdLzT5v2uJq6KBTwxj1FYvuPTr7RoWkN4cmubDy5wW8SU3q9xYnDRpQepiT', + 'tpubDCYNsKenq7Cuuf4fHsu2fsWA7Wb5cTD2qRUrw6uHbNNYQoNkEoJk4hgNhxbnGss5gnEe2MpqN2qbRVqWJGmuofAWmwFFi4CZ9Tg1LHKJDhF', + 'tpubDDc6eecoyYxL4g3WKYpbbinyUmnfVikQCzHTPd6rJQivaPqGKBFiueQqWoAYonB8hAEXGM1ak7LqrnwczH24EbW7jbG5bNK5rncmRXtv7nG', + ]); + + await expect(bitbox.btcAddress( + 'tbtc', + "m/84'/1'/0'/1/10", + { simpleType: 'p2wpkh' }, + false, + )).resolves.toBe('tb1qq064dxjgl9h9wzgsmzy6t6306qew42w9ka02u3'); + + const messageKeypath = "m/49'/0'/0'/0/10"; + const messageAddress = await bitbox.btcAddress( + 'btc', + messageKeypath, + { simpleType: 'p2wpkhP2sh' }, + false, + ); + const message = new TextEncoder().encode('message'); + const signature = await bitbox.btcSignMessage( + 'btc', + { + scriptConfig: { simpleType: 'p2wpkhP2sh' }, + keypath: messageKeypath, + }, + message, + ); + const messageHash = bitcoinMessageHash(message); + const compactSignature = Uint8Array.from(signature.sig); + const publicKey = ecc.recover( + messageHash, + compactSignature, + signature.recid as ecc.RecoveryIdType, + true, + ); + if (publicKey === null) { + throw new Error('failed to recover Bitcoin message signing key'); + } + expect(ecc.verify( + messageHash, + publicKey, + compactSignature, + )).toBe(true); + expect(payments.p2sh({ + redeem: payments.p2wpkh({ pubkey: publicKey }), + }).address).toBe(messageAddress); + expect(signature.electrumSig65).toEqual([ + 31 + signature.recid, + ...signature.sig, + ]); + } finally { + bitbox.close(); + } + }, 60_000); + + it('signs the firmware BTC transaction vectors', async () => { + expect(vectorFile.vectors).toHaveLength(41); + expect(vectorFile.vectors.filter(vector => !shouldSkip(vector))).toHaveLength(33); + const bitbox = await connectRestored(); + try { + await server!.stdout.waitUntilStable(server!.stdout.checkpoint()); + for (const vector of vectorFile.vectors) { + if (!shouldSkip(vector)) { + await runVector(bitbox, server!, vector); + } + } + } finally { + bitbox.close(); + } + }, 15 * 60_000); +}); diff --git a/test/simulator-util.ts b/test/simulator-util.ts index 3ba0959..d94f791 100644 --- a/test/simulator-util.ts +++ b/test/simulator-util.ts @@ -20,6 +20,237 @@ export interface SimulatorCase { binaryPath: string; } +export type SimulatorScreen = + | { type: 'confirm'; title: string; body: string } + | { type: 'transaction_address'; amount: string; address: string } + | { type: 'transaction_fee'; amount: string; fee: string } + | { type: 'status'; title: string; body: string } + | { type: 'swap'; title: string; from: string; to: string }; + +function screenEndMarker(start: string): string | undefined { + switch (start) { + case 'CONFIRM SCREEN START': + return 'CONFIRM SCREEN END'; + case 'CONFIRM TRANSACTION ADDRESS SCREEN START': + return 'CONFIRM TRANSACTION ADDRESS SCREEN END'; + case 'CONFIRM TRANSACTION FEE SCREEN START': + return 'CONFIRM TRANSACTION FEE SCREEN END'; + case 'STATUS SCREEN START': + return 'STATUS SCREEN END'; + case 'CONFIRM SWAP SCREEN START': + return 'CONFIRM SWAP SCREEN END'; + default: + return undefined; + } +} + +function isScreenMarker(line: string): boolean { + return line.endsWith(' SCREEN START') || line.endsWith(' SCREEN END'); +} + +function parseScreenFields( + lines: string[], + blockLine: number, + fields: string[], +): string[] { + const result: string[] = []; + let lineIndex = 0; + for (let fieldIndex = 0; fieldIndex < fields.length; fieldIndex += 1) { + const field = fields[fieldIndex]!; + const line = lines[lineIndex]; + if (line === undefined) { + throw new Error(`simulator stdout line ${blockLine}: missing ${field} field`); + } + const prefix = `${field}: `; + if (!line.startsWith(prefix)) { + throw new Error( + `simulator stdout line ${blockLine}: expected ${JSON.stringify(prefix)}, got ${JSON.stringify(line)}`, + ); + } + const valueLines = [line.slice(prefix.length)]; + lineIndex += 1; + const nextField = fields[fieldIndex + 1]; + if (nextField === undefined) { + valueLines.push(...lines.slice(lineIndex)); + lineIndex = lines.length; + } else { + const nextPrefix = `${nextField}: `; + while (lineIndex < lines.length && !lines[lineIndex]!.startsWith(nextPrefix)) { + valueLines.push(lines[lineIndex]!); + lineIndex += 1; + } + } + result.push(valueLines.join('\n')); + } + return result; +} + +function parseScreenBlock( + start: string, + lines: string[], + blockLine: number, +): SimulatorScreen { + switch (start) { + case 'CONFIRM SCREEN START': { + const [title, body] = parseScreenFields(lines, blockLine, ['TITLE', 'BODY']); + return { type: 'confirm', title: title!, body: body! }; + } + case 'CONFIRM TRANSACTION ADDRESS SCREEN START': { + const [amount, address] = parseScreenFields(lines, blockLine, ['AMOUNT', 'ADDRESS']); + return { type: 'transaction_address', amount: amount!, address: address! }; + } + case 'CONFIRM TRANSACTION FEE SCREEN START': { + const [amount, fee] = parseScreenFields(lines, blockLine, ['AMOUNT', 'FEE']); + return { type: 'transaction_fee', amount: amount!, fee: fee! }; + } + case 'STATUS SCREEN START': { + const titleLine = lines[0]; + if (titleLine === undefined || !titleLine.startsWith('TITLE: ')) { + throw new Error(`simulator stdout line ${blockLine}: missing TITLE field`); + } + return { + type: 'status', + title: titleLine.slice('TITLE: '.length), + body: lines.slice(1).join('\n'), + }; + } + case 'CONFIRM SWAP SCREEN START': { + const [title, from, to] = parseScreenFields( + lines, + blockLine, + ['TITLE', 'FROM', 'TO'], + ); + return { type: 'swap', title: title!, from: from!, to: to! }; + } + default: + throw new Error(`simulator stdout line ${blockLine}: unknown screen marker ${start}`); + } +} + +export function parseSimulatorScreens(output: string): SimulatorScreen[] { + const lines = output.split(/\r?\n/); + if (lines.at(-1) === '') { + lines.pop(); + } + const result: SimulatorScreen[] = []; + let index = 0; + while (index < lines.length) { + const start = lines[index]!; + const endMarker = screenEndMarker(start); + if (endMarker === undefined) { + if (isScreenMarker(start)) { + throw new Error( + `simulator stdout line ${index + 1}: unknown screen marker ${JSON.stringify(start)}`, + ); + } + index += 1; + continue; + } + let end = index + 1; + while (end < lines.length && lines[end] !== endMarker) { + if (screenEndMarker(lines[end]!) !== undefined) { + throw new Error( + `simulator stdout line ${end + 1}: screen starting on line ${index + 1} contains a nested screen`, + ); + } + if (isScreenMarker(lines[end]!)) { + throw new Error( + `simulator stdout line ${end + 1}: unexpected marker ${JSON.stringify(lines[end])}`, + ); + } + end += 1; + } + if (end === lines.length) { + throw new Error(`simulator stdout line ${index + 1}: missing ${endMarker}`); + } + result.push(parseScreenBlock(start, lines.slice(index + 1, end), index + 1)); + index = end + 1; + } + return result; +} + +export class SimulatorStdoutSnapshot { + constructor(private readonly lines: string[]) {} + + raw(): string { + return this.lines.length === 0 ? '' : `${this.lines.join('\n')}\n`; + } + + screens(): SimulatorScreen[] { + return parseSimulatorScreens(this.raw()); + } +} + +export class SimulatorStdout { + private readonly lines: string[] = []; + private lastUpdate = Date.now(); + private closed = false; + + recordLine(line: string): void { + this.lines.push(line); + this.lastUpdate = Date.now(); + } + + markClosed(): void { + this.closed = true; + } + + checkpoint(): number { + return this.lines.length; + } + + snapshot(checkpoint: number): SimulatorStdoutSnapshot { + if (checkpoint > this.lines.length) { + throw new Error('simulator stdout checkpoint is out of bounds'); + } + return new SimulatorStdoutSnapshot(this.lines.slice(checkpoint)); + } + + async waitUntilStable( + checkpoint: number, + stableForMs = 50, + timeoutMs = 5_000, + ): Promise { + const started = Date.now(); + for (;;) { + const now = Date.now(); + const lastUpdate = this.lines.length > checkpoint ? this.lastUpdate : started; + if (now - lastUpdate >= stableForMs || this.closed) { + return this.snapshot(checkpoint); + } + if (now - started >= timeoutMs) { + const snapshot = this.snapshot(checkpoint); + throw new Error(`waiting for stable simulator stdout timed out\n${snapshot.raw()}`); + } + await new Promise(resolve => setTimeout(resolve, 10)); + } + } + + async waitForTerminalScreen( + checkpoint: number, + timeoutMs = 5_000, + ): Promise { + const started = Date.now(); + for (;;) { + const snapshot = this.snapshot(checkpoint); + try { + if (snapshot.screens().at(-1)?.type === 'status') { + return snapshot; + } + } catch { + // A screen block may still be arriving; parse again after the next poll. + } + if (this.closed) { + throw new Error(`simulator stdout closed before terminal screen\n${snapshot.raw()}`); + } + if (Date.now() - started >= timeoutMs) { + throw new Error(`waiting for terminal simulator screen timed out\n${snapshot.raw()}`); + } + await new Promise(resolve => setTimeout(resolve, 10)); + } + } +} + function simulatorsJsonPath(): string { return path.join(__dirname, 'simulators.json'); } @@ -159,6 +390,7 @@ export async function ensureSimulator(simulator: SimulatorCase): Promise */ export class SimulatorServer { private readonly child: ChildProcess; + readonly stdout = new SimulatorStdout(); readonly exited: Promise; constructor(binaryPath: string) { @@ -168,8 +400,17 @@ export class SimulatorServer { this.child = spawn('stdbuf', ['-oL', binaryPath], { stdio: ['ignore', 'pipe', 'pipe'] }); this.child.stdout?.setEncoding('utf8'); this.child.stderr?.setEncoding('utf8'); - this.child.stdout?.on('data', (chunk: string) => forward(chunk, process.stdout, '[sim]')); - this.child.stderr?.on('data', (chunk: string) => forward(chunk, process.stderr, '[sim!]')); + const stdoutLines = new LineForwarder(process.stdout, '[sim]', (line) => { + this.stdout.recordLine(line); + }); + const stderrLines = new LineForwarder(process.stderr, '[sim!]'); + this.child.stdout?.on('data', (chunk: string) => stdoutLines.write(chunk)); + this.child.stderr?.on('data', (chunk: string) => stderrLines.write(chunk)); + this.child.stdout?.on('end', () => { + stdoutLines.end(); + this.stdout.markClosed(); + }); + this.child.stderr?.on('end', () => stderrLines.end()); this.exited = new Promise((resolve) => { this.child.once('exit', () => resolve()); }); @@ -201,10 +442,34 @@ export class SimulatorServer { } } -function forward(chunk: string, out: NodeJS.WritableStream, prefix: string): void { - for (const line of chunk.split(/\r?\n/)) { - if (line.length > 0) { - out.write(`\t\t${prefix} ${line}\n`); +class LineForwarder { + private remainder = ''; + + constructor( + private readonly out: NodeJS.WritableStream, + private readonly prefix: string, + private readonly onLine?: (line: string) => void, + ) {} + + write(chunk: string): void { + const lines = `${this.remainder}${chunk}`.split(/\r?\n/); + this.remainder = lines.pop() ?? ''; + for (const line of lines) { + this.forward(line); + } + } + + end(): void { + if (this.remainder.length > 0) { + this.forward(this.remainder); + this.remainder = ''; + } + } + + private forward(line: string): void { + this.onLine?.(line); + if (line.length > 0 && process.env.UPDATE_BTC_VECTOR_SCREENS !== '1') { + this.out.write(`\t\t${this.prefix} ${line}\n`); } } }
Keypath: {keypath}
{keypath}
Account keypath: {keypath}
Policy: {policy}
{policy}