diff --git a/packages/wasm-utxo/js/fixedScriptWallet/index.ts b/packages/wasm-utxo/js/fixedScriptWallet/index.ts index e4c1ca6172f..7cc3879f119 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/index.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/index.ts @@ -14,6 +14,9 @@ export { } from "./scriptType.js"; export { ChainCode, chainCodes, assertChainCode, type Scope } from "./chains.js"; +// Previous-transaction inclusion policy (pure JS, no WASM init) +export { requiresPrevTxForP2sh } from "./prevTx.js"; + // Bitcoin-like PSBT (for all non-Zcash networks) export { BitGoPsbt, diff --git a/packages/wasm-utxo/js/fixedScriptWallet/prevTx.ts b/packages/wasm-utxo/js/fixedScriptWallet/prevTx.ts new file mode 100644 index 00000000000..c45a1cce512 --- /dev/null +++ b/packages/wasm-utxo/js/fixedScriptWallet/prevTx.ts @@ -0,0 +1,48 @@ +/** + * Previous-transaction inclusion policy for fixed-script wallet PSBT inputs. + * + * Decides whether a p2sh input requires the full previous transaction + * (PSBT_IN_NON_WITNESS_UTXO) or can be signed from witness_utxo-only. + * + * This is a pure-JS module (no WASM initialization) so callers can evaluate + * the policy cheaply without loading the wasm-utxo module. + */ +import { type CoinName, getMainnet } from "../coinName.js"; + +/** + * Whether a p2sh input requires the full previous transaction + * (PSBT_IN_NON_WITNESS_UTXO). Callers are expected to have already + * confirmed the input is p2sh (non-segwit) and that the tx format + * includes prevTx (e.g. "psbt", not "psbt-lite"); this predicate only + * answers the coin-level question. + * + * Returns false for value-committing coins whose sighash commits the + * input amount, making `non_witness_utxo` (full prevTx) cryptographically + * pointless for signing p2sh inputs — `witness_utxo` (value + + * scriptPubKey) suffices: + * + * - Zcash (`zec`/`tzec`): ZIP-243 transparent sighash commits the amount. + * Including prevTx also crashes wasm-utxo, whose consensus::deserialize + * rejects Zcash overwintered transactions. + * - BCH family (`bch`/`bcha`/`bsv`/`btg` + testnets): replay-protected + * BIP-143 sighash (SIGHASH_FORKID, the default for the whole family) + * commits the 8-byte value as preimage item #6. eCash is `bcha`/`tbcha`. + * For the BCH family, skipping prevTx is an optimization (no DB fetch) + * plus defense-in-depth, with the same fee-validation risk that the + * existing `psbt-lite` path already accepts for all coins. + * + * Testnets are normalized via `getMainnet` before the switch. True + * otherwise. + */ +export function requiresPrevTxForP2sh(coinName: CoinName): boolean { + switch (getMainnet(coinName)) { + case "zec": // Zcash (ZIP-243) + case "bch": // Bitcoin Cash (BIP-143/FORKID) + case "bcha": // eCash (BIP-143/FORKID) + case "bsv": // Bitcoin SV (BIP-143/FORKID) + case "btg": // Bitcoin Gold (BIP-143/FORKID) + return false; + default: + return true; + } +} diff --git a/packages/wasm-utxo/js/index.ts b/packages/wasm-utxo/js/index.ts index 331e64b8118..bd56d90b95e 100644 --- a/packages/wasm-utxo/js/index.ts +++ b/packages/wasm-utxo/js/index.ts @@ -28,6 +28,7 @@ export function getWasmUtxoVersion(): WasmUtxoVersionInfo { } export { type CoinName, getMainnet, isMainnet, isTestnet, isCoinName } from "./coinName.js"; +export { requiresPrevTxForP2sh } from "./fixedScriptWallet/prevTx.js"; export type { Triple } from "./triple.js"; export type { AddressFormat } from "./address.js"; export type { TapLeafScript, PreparedInscriptionRevealData } from "./inscriptions.js"; diff --git a/packages/wasm-utxo/test/fixedScript/prevTx.ts b/packages/wasm-utxo/test/fixedScript/prevTx.ts new file mode 100644 index 00000000000..c1bb3646153 --- /dev/null +++ b/packages/wasm-utxo/test/fixedScript/prevTx.ts @@ -0,0 +1,80 @@ +import assert from "node:assert"; + +import { requiresPrevTxForP2sh, type CoinName } from "../../js/index.js"; + +describe("prevTx policy", function () { + describe("requiresPrevTxForP2sh", function () { + // Callers gate on script type (p2sh) and tx format themselves; this + // predicate only answers the coin-level question for a p2sh input. + const cases: { coin: CoinName; expected: boolean; note: string }[] = [ + // Value-committing coins: skip prevTx (sighash commits the amount) + { + coin: "zec", + expected: false, + note: "zec p2sh — skip prevTx (ZIP-243, the fix)", + }, + { + coin: "tzec", + expected: false, + note: "tzec p2sh — skip prevTx (ZIP-243, the fix)", + }, + { + coin: "bch", + expected: false, + note: "bch p2sh — skip prevTx (FORKID commits value)", + }, + { + coin: "tbch", + expected: false, + note: "tbch p2sh — skip prevTx (FORKID commits value)", + }, + { + coin: "bcha", + expected: false, + note: "bcha (eCash) p2sh — skip prevTx (FORKID commits value)", + }, + { + coin: "bsv", + expected: false, + note: "bsv p2sh — skip prevTx (FORKID commits value)", + }, + { + coin: "btg", + expected: false, + note: "btg p2sh — skip prevTx (FORKID commits value)", + }, + // Non-value-committing coins: prevTx still required (unchanged) + { + coin: "btc", + expected: true, + note: "btc p2sh — unchanged (needs prevTx)", + }, + { + coin: "tbtc", + expected: true, + note: "tbtc p2sh — unchanged (needs prevTx)", + }, + { + coin: "ltc", + expected: true, + note: "ltc p2sh — unchanged (needs prevTx)", + }, + { + coin: "doge", + expected: true, + note: "doge p2sh — unchanged (needs prevTx)", + }, + { + coin: "dash", + expected: true, + note: "dash p2sh — unchanged (needs prevTx)", + }, + ]; + + for (const c of cases) { + it(`returns ${c.expected} for ${c.note}`, function () { + assert.strictEqual(requiresPrevTxForP2sh(c.coin), c.expected); + }); + } + }); +});