Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
49 changes: 49 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/prevTx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* 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";

/**
* Mainnet coins whose signature hash commits the input amount, making
* `non_witness_utxo` (full prevTx) cryptographically pointless for
* signing p2sh inputs — `witness_utxo` (value + scriptPubKey) suffices.
*
* - `zec`: ZIP-243 transparent sighash commits the amount.
* - `bch`/`bcha`/`bsv`/`btg`: replay-protected BIP-143 sighash
* (SIGHASH_FORKID, the default for the whole BCH family) commits the
* 8-byte value as preimage item #6. eCash is `bcha`/`tbcha`.
*
* Note: only Zcash *also* crashes the wasm-utxo parser when prevTx is
* included (Zcash overwintered txs aren't Bitcoin-consensus-encodable).
* 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.
*/
const VALUE_COMMITTING_MAINNETS: ReadonlySet<CoinName> = new Set([
"zec",
"bch",
"bcha",
"bsv",
"btg",
]);

/**
* 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 (Zcash ZIP-243, BCH-family
* BIP-143/FORKID) whose sighash commits the input amount, making prevTx
* cryptographically pointless for signing. True otherwise.
*/
export function requiresPrevTxForP2sh(coinName: CoinName): boolean {
return !VALUE_COMMITTING_MAINNETS.has(getMainnet(coinName));
}
1 change: 1 addition & 0 deletions packages/wasm-utxo/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
80 changes: 80 additions & 0 deletions packages/wasm-utxo/test/fixedScript/prevTx.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
});
});
Loading