Skip to content

Commit 8feac80

Browse files
feat(wasm-utxo): expose requiresPrevTxForP2sh prevTx-inclusion policy
Add a pure-JS prevTx-inclusion predicate to @bitgo/wasm-utxo so all callers share one source of truth for whether a p2sh PSBT input needs non_witness_utxo (full prevTx) or can be signed from witness_utxo-only. requiresPrevTxForP2sh(coinName) answers only the coin-level question for an input the caller has already determined is p2sh (non-segwit) and whose tx format includes prevTx (e.g. "psbt", not "psbt-lite"). It returns false for value-committing coins whose sighash commits the input amount, making prevTx cryptographically pointless for signing p2sh inputs: - 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/eCash, 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. The value-committing mainnets {zec, bch, bcha, bsv, btg} are matched via a switch on getMainnet(coinName), so testnets are covered. The module is pure JS — no WASM initialization — so the predicate is cheap to evaluate without loading wasm-utxo. Exported both namespaced (fixedScriptWallet.requiresPrevTxForP2sh) and as a top-level named export (alongside CoinName/getMainnet). Refs: T1-3654
1 parent c744b64 commit 8feac80

4 files changed

Lines changed: 132 additions & 0 deletions

File tree

packages/wasm-utxo/js/fixedScriptWallet/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export {
1414
} from "./scriptType.js";
1515
export { ChainCode, chainCodes, assertChainCode, type Scope } from "./chains.js";
1616

17+
// Previous-transaction inclusion policy (pure JS, no WASM init)
18+
export { requiresPrevTxForP2sh } from "./prevTx.js";
19+
1720
// Bitcoin-like PSBT (for all non-Zcash networks)
1821
export {
1922
BitGoPsbt,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Previous-transaction inclusion policy for fixed-script wallet PSBT inputs.
3+
*
4+
* Decides whether a p2sh input requires the full previous transaction
5+
* (PSBT_IN_NON_WITNESS_UTXO) or can be signed from witness_utxo-only.
6+
*
7+
* This is a pure-JS module (no WASM initialization) so callers can evaluate
8+
* the policy cheaply without loading the wasm-utxo module.
9+
*/
10+
import { type CoinName, getMainnet } from "../coinName.js";
11+
12+
/**
13+
* Whether a p2sh input requires the full previous transaction
14+
* (PSBT_IN_NON_WITNESS_UTXO). Callers are expected to have already
15+
* confirmed the input is p2sh (non-segwit) and that the tx format
16+
* includes prevTx (e.g. "psbt", not "psbt-lite"); this predicate only
17+
* answers the coin-level question.
18+
*
19+
* Returns false for value-committing coins whose sighash commits the
20+
* input amount, making `non_witness_utxo` (full prevTx) cryptographically
21+
* pointless for signing p2sh inputs — `witness_utxo` (value +
22+
* scriptPubKey) suffices:
23+
*
24+
* - Zcash (`zec`/`tzec`): ZIP-243 transparent sighash commits the amount.
25+
* Including prevTx also crashes wasm-utxo, whose consensus::deserialize
26+
* rejects Zcash overwintered transactions.
27+
* - BCH family (`bch`/`bcha`/`bsv`/`btg` + testnets): replay-protected
28+
* BIP-143 sighash (SIGHASH_FORKID, the default for the whole family)
29+
* commits the 8-byte value as preimage item #6. eCash is `bcha`/`tbcha`.
30+
* For the BCH family, skipping prevTx is an optimization (no DB fetch)
31+
* plus defense-in-depth, with the same fee-validation risk that the
32+
* existing `psbt-lite` path already accepts for all coins.
33+
*
34+
* Testnets are normalized via `getMainnet` before the switch. True
35+
* otherwise.
36+
*/
37+
export function requiresPrevTxForP2sh(coinName: CoinName): boolean {
38+
switch (getMainnet(coinName)) {
39+
case "zec": // Zcash (ZIP-243)
40+
case "bch": // Bitcoin Cash (BIP-143/FORKID)
41+
case "bcha": // eCash (BIP-143/FORKID)
42+
case "bsv": // Bitcoin SV (BIP-143/FORKID)
43+
case "btg": // Bitcoin Gold (BIP-143/FORKID)
44+
return false;
45+
default:
46+
return true;
47+
}
48+
}

packages/wasm-utxo/js/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function getWasmUtxoVersion(): WasmUtxoVersionInfo {
2828
}
2929

3030
export { type CoinName, getMainnet, isMainnet, isTestnet, isCoinName } from "./coinName.js";
31+
export { requiresPrevTxForP2sh } from "./fixedScriptWallet/prevTx.js";
3132
export type { Triple } from "./triple.js";
3233
export type { AddressFormat } from "./address.js";
3334
export type { TapLeafScript, PreparedInscriptionRevealData } from "./inscriptions.js";
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import assert from "node:assert";
2+
3+
import { requiresPrevTxForP2sh, type CoinName } from "../../js/index.js";
4+
5+
describe("prevTx policy", function () {
6+
describe("requiresPrevTxForP2sh", function () {
7+
// Callers gate on script type (p2sh) and tx format themselves; this
8+
// predicate only answers the coin-level question for a p2sh input.
9+
const cases: { coin: CoinName; expected: boolean; note: string }[] = [
10+
// Value-committing coins: skip prevTx (sighash commits the amount)
11+
{
12+
coin: "zec",
13+
expected: false,
14+
note: "zec p2sh — skip prevTx (ZIP-243, the fix)",
15+
},
16+
{
17+
coin: "tzec",
18+
expected: false,
19+
note: "tzec p2sh — skip prevTx (ZIP-243, the fix)",
20+
},
21+
{
22+
coin: "bch",
23+
expected: false,
24+
note: "bch p2sh — skip prevTx (FORKID commits value)",
25+
},
26+
{
27+
coin: "tbch",
28+
expected: false,
29+
note: "tbch p2sh — skip prevTx (FORKID commits value)",
30+
},
31+
{
32+
coin: "bcha",
33+
expected: false,
34+
note: "bcha (eCash) p2sh — skip prevTx (FORKID commits value)",
35+
},
36+
{
37+
coin: "bsv",
38+
expected: false,
39+
note: "bsv p2sh — skip prevTx (FORKID commits value)",
40+
},
41+
{
42+
coin: "btg",
43+
expected: false,
44+
note: "btg p2sh — skip prevTx (FORKID commits value)",
45+
},
46+
// Non-value-committing coins: prevTx still required (unchanged)
47+
{
48+
coin: "btc",
49+
expected: true,
50+
note: "btc p2sh — unchanged (needs prevTx)",
51+
},
52+
{
53+
coin: "tbtc",
54+
expected: true,
55+
note: "tbtc p2sh — unchanged (needs prevTx)",
56+
},
57+
{
58+
coin: "ltc",
59+
expected: true,
60+
note: "ltc p2sh — unchanged (needs prevTx)",
61+
},
62+
{
63+
coin: "doge",
64+
expected: true,
65+
note: "doge p2sh — unchanged (needs prevTx)",
66+
},
67+
{
68+
coin: "dash",
69+
expected: true,
70+
note: "dash p2sh — unchanged (needs prevTx)",
71+
},
72+
];
73+
74+
for (const c of cases) {
75+
it(`returns ${c.expected} for ${c.note}`, function () {
76+
assert.strictEqual(requiresPrevTxForP2sh(c.coin), c.expected);
77+
});
78+
}
79+
});
80+
});

0 commit comments

Comments
 (0)