From 4417f1a5a5c356575891424e13c704a437605dc2 Mon Sep 17 00:00:00 2001 From: Ranjna Ganesh Ram Date: Thu, 22 Oct 2026 19:13:03 +0530 Subject: [PATCH] feat(sdk-coin-zec): update wasm-utxo and zcash recovery defaults for NU7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NU7 activates on mainnet at 3543405 (ZIP-259, ~2026-11-05). Bump @bitgo/wasm-utxo to 5.15.0 across the utxo modules — the first release carrying the NU7 mainnet height and branch ID 0x77190ad9. The recovery default heights were NU6.1-era (zec 3146400, tzec 3536500): they stamped a branch ID invalid since NU6.2/NU6.3, silently producing broadcast-dead v4 recovery transactions in the emergency recovery path. Default to the NU7 activation heights so a no-height v4 build fails loudly at build time per ZIP-2003 instead of being signed and queued. Pin the recovery fixture tests to their historical v4-era generation heights (fixture bytes unchanged), add mainnet v6 decode and v4-rejection tests at the NU7 boundary, and fix a stale utxo-lib NU6 comment. Ticket: CSHLD-2024 --- modules/abstract-utxo/package.json | 2 +- modules/abstract-utxo/src/recovery/psbt.ts | 11 +++++-- .../test/unit/impl/zec/psbtDecode.ts | 30 +++++++++++++++++++ .../test/unit/recovery/backupKeyRecovery.ts | 6 ++++ .../backupKeyRecoveryUnspentGathering.ts | 4 +++ modules/utxo-bin/package.json | 2 +- modules/utxo-core/package.json | 2 +- modules/utxo-descriptors/package.json | 2 +- .../src/bitgo/zcash/ZcashTransaction.ts | 6 ++-- modules/utxo-ord/package.json | 2 +- modules/utxo-staking/package.json | 2 +- yarn.lock | 8 ++--- 12 files changed, 62 insertions(+), 15 deletions(-) diff --git a/modules/abstract-utxo/package.json b/modules/abstract-utxo/package.json index 7e6cf4f1dd2..c3a166ddac3 100644 --- a/modules/abstract-utxo/package.json +++ b/modules/abstract-utxo/package.json @@ -66,7 +66,7 @@ "@bitgo/utxo-core": "^1.41.6", "@bitgo/utxo-descriptors": "^2.2.0", "@bitgo/utxo-ord": "^1.34.6", - "@bitgo/wasm-utxo": "^5.11.0", + "@bitgo/wasm-utxo": "^5.15.0", "@types/lodash": "^4.14.121", "@types/superagent": "4.1.15", "bignumber.js": "^9.0.2", diff --git a/modules/abstract-utxo/src/recovery/psbt.ts b/modules/abstract-utxo/src/recovery/psbt.ts index bb28f751028..abbd445d820 100644 --- a/modules/abstract-utxo/src/recovery/psbt.ts +++ b/modules/abstract-utxo/src/recovery/psbt.ts @@ -49,12 +49,17 @@ function isZcash(coinName: CoinName): coinName is 'zec' | 'tzec' { /** * Default block heights for Zcash networks if not provided. - * These should be set to a height after the latest network upgrade. + * These are the latest network upgrade's activation heights (NU7, ZIP-259): + * zec 3543405 (mainnet, ~2026-11-05) and tzec 4465026 (testnet, 2026-10-06). + * Version 4 transactions are invalid at/after NU7 per ZIP-2003, so a v4 build at + * these heights is rejected by @bitgo/wasm-utxo at build time rather than signed + * and left unbroadcastable; recovering post-NU7 funds requires constructing a v6 + * (Ironwood) transaction instead. * TODO(BTC-2901): get the height from blockchair API instead of hardcoding. */ const ZCASH_DEFAULT_BLOCK_HEIGHTS: Record<'zec' | 'tzec', number> = { - zec: 3146400, - tzec: 3536500, + zec: 3543405, + tzec: 4465026, }; /** diff --git a/modules/abstract-utxo/test/unit/impl/zec/psbtDecode.ts b/modules/abstract-utxo/test/unit/impl/zec/psbtDecode.ts index 2b458a58c9f..9b87b838c76 100644 --- a/modules/abstract-utxo/test/unit/impl/zec/psbtDecode.ts +++ b/modules/abstract-utxo/test/unit/impl/zec/psbtDecode.ts @@ -34,6 +34,17 @@ describe('Zec PSBT decode (v4 + v6 Ironwood)', function () { return psbt; } + function buildMainnetV6Psbt(): fixedScriptWallet.ZcashIronwoodBitGoPsbt { + // NU7 mainnet activation height (ZIP-259, ~2026-11-05), carried by + // @bitgo/wasm-utxo 5.15.0. + const psbt = fixedScriptWallet.ZcashIronwoodBitGoPsbt.createEmpty('zec', walletKeys, { blockHeight: 3543405 }); + psbt.addWalletInput({ txid: '55'.repeat(32), vout: 0, value: 100000n }, walletKeys, { + scriptId: { chain: 0, index: 0 }, + }); + psbt.addWalletOutput(walletKeys, { chain: 1, index: 0, value: 90000n }); + return psbt; + } + it('decodes a v4 (Sapling-shaped) PSBT as a ZcashBitGoPsbt', function () { const decoded = zec.decodeTransaction(Buffer.from(buildV4Psbt().serialize())); assert.ok(decoded instanceof fixedScriptWallet.ZcashBitGoPsbt); @@ -64,6 +75,25 @@ describe('Zec PSBT decode (v4 + v6 Ironwood)', function () { assert.ok(decoded instanceof fixedScriptWallet.ZcashIronwoodBitGoPsbt); }); + it('decodes a v6 (Ironwood) mainnet PSBT at/after NU7 activation with the NU7 branch id', function () { + // From mainnet NU7 activation (3,543,405, ZIP-259) version 4 is invalid per + // ZIP-2003 and only v5/v6 transactions are valid; the NU7 consensus branch id + // 0x77190AD9 personalizes the ZIP-244 digests. + const psbt = buildMainnetV6Psbt(); + assert.strictEqual(psbt.consensusBranchId, 0x77190ad9); + const decoded = zec.decodeTransaction(Buffer.from(psbt.serialize())); + assert.ok(decoded instanceof fixedScriptWallet.ZcashIronwoodBitGoPsbt); + assert.strictEqual(decoded.getVersion(), 6); + assert.strictEqual(decoded.consensusBranchId, 0x77190ad9); + }); + + it('rejects a v4 mainnet build at/after NU7 activation per ZIP-2003', function () { + assert.throws( + () => fixedScriptWallet.ZcashBitGoPsbt.createEmpty('zec', walletKeys, { blockHeight: 3543405 }), + /NU7/ + ); + }); + it('throws the legacy-format error for a non-PSBT transaction', function () { assert.throws(() => zec.decodeTransaction(Buffer.alloc(32)), /txFormat=legacy is deprecated/); }); diff --git a/modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts b/modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts index b897c42ddf8..541fb32ae59 100644 --- a/modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts +++ b/modules/abstract-utxo/test/unit/recovery/backupKeyRecovery.ts @@ -87,6 +87,12 @@ function run( keys: signingKeys, recoveryDestination, feeRateSatVB: defaultFeeRateSatB, + // Zcash: pin the historical pre-NU7 heights the recorded fixtures + // were generated with (NU6.1-era branch ids). Production defaults + // to the NU7 activation heights, at which a v4 build is rejected + // per ZIP-2003; these tests exercise v4 recovery mechanics, not + // broadcastability. + blockHeight: coin.getChain() === 'zec' ? 3146400 : coin.getChain() === 'tzec' ? 3536500 : undefined, }, recoverUnspents ); diff --git a/modules/abstract-utxo/test/unit/recovery/backupKeyRecoveryUnspentGathering.ts b/modules/abstract-utxo/test/unit/recovery/backupKeyRecoveryUnspentGathering.ts index c7c926600ec..2960005bb7d 100644 --- a/modules/abstract-utxo/test/unit/recovery/backupKeyRecoveryUnspentGathering.ts +++ b/modules/abstract-utxo/test/unit/recovery/backupKeyRecoveryUnspentGathering.ts @@ -131,6 +131,10 @@ describe('Backup Key Recovery - Unspent Gathering', function () { ...keysFullSignedRecovery, recoveryProvider: mockProvider, feeRate: defaultFeeRateSatB, + // Zcash: exercise v4 recovery mechanics at a historical pre-NU7 height + // (ignored by non-Zcash coins). The production default is the NU7 + // activation height, at which a v4 build is rejected per ZIP-2003. + blockHeight: 3146400, }); // Verify recovery succeeded (transaction hex indicates unspents were gathered) diff --git a/modules/utxo-bin/package.json b/modules/utxo-bin/package.json index d148b442fb4..b22218190bb 100644 --- a/modules/utxo-bin/package.json +++ b/modules/utxo-bin/package.json @@ -31,7 +31,7 @@ "@bitgo/unspents": "^0.51.10", "@bitgo/utxo-core": "^1.41.6", "@bitgo/utxo-lib": "^11.24.4", - "@bitgo/wasm-utxo": "^5.11.0", + "@bitgo/wasm-utxo": "^5.15.0", "@noble/curves": "1.8.1", "archy": "^1.0.0", "bech32": "^2.0.0", diff --git a/modules/utxo-core/package.json b/modules/utxo-core/package.json index 997c0d2f1ab..2becd612770 100644 --- a/modules/utxo-core/package.json +++ b/modules/utxo-core/package.json @@ -81,7 +81,7 @@ "@bitgo/secp256k1": "^1.11.1", "@bitgo/unspents": "^0.51.10", "@bitgo/utxo-lib": "^11.24.4", - "@bitgo/wasm-utxo": "^5.11.0", + "@bitgo/wasm-utxo": "^5.15.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", "fast-sha256": "^1.3.0" }, diff --git a/modules/utxo-descriptors/package.json b/modules/utxo-descriptors/package.json index 00f72a63541..85bcf3f0cbd 100644 --- a/modules/utxo-descriptors/package.json +++ b/modules/utxo-descriptors/package.json @@ -60,7 +60,7 @@ }, "dependencies": { "@bitgo/utxo-core": "^1.41.6", - "@bitgo/wasm-utxo": "^5.11.0" + "@bitgo/wasm-utxo": "^5.15.0" }, "devDependencies": { "@stacks/bitcoin-staking": "7.6.0" diff --git a/modules/utxo-lib/src/bitgo/zcash/ZcashTransaction.ts b/modules/utxo-lib/src/bitgo/zcash/ZcashTransaction.ts index 858f9aa24ac..db8997bf919 100644 --- a/modules/utxo-lib/src/bitgo/zcash/ZcashTransaction.ts +++ b/modules/utxo-lib/src/bitgo/zcash/ZcashTransaction.ts @@ -52,8 +52,10 @@ export function getDefaultVersionGroupIdForVersion(version: number): number { } export function getDefaultConsensusBranchIdForVersion(network: ZcashNetwork, version: number): number { - // Testnet has not yet activated NU6.2 (testnet activation block 4052000), so keep - // defaulting to NU6.1 there. Mainnet activated NU6.2 at block 3364600. + // Deprecated legacy path: this utxolib backend is not height-aware and is frozen at + // NU6.1-era testnet / NU6.2-era mainnet defaults. Height-correct Zcash construction + // (NU6.2, NU6.3/Ironwood, NU7) lives in @bitgo/wasm-utxo. Testnet has long since + // activated NU6.2 (block 4052000) and NU7 (block 4465026). if (network === networks.zcashTest) { if (version === 4 || version === 5) { return NU6_1_BRANCH_ID; diff --git a/modules/utxo-ord/package.json b/modules/utxo-ord/package.json index e5c1b86520e..56aa26c96a0 100644 --- a/modules/utxo-ord/package.json +++ b/modules/utxo-ord/package.json @@ -45,7 +45,7 @@ "directory": "modules/utxo-ord" }, "dependencies": { - "@bitgo/wasm-utxo": "^5.11.0" + "@bitgo/wasm-utxo": "^5.15.0" }, "devDependencies": { "@bitgo/utxo-lib": "^11.24.4" diff --git a/modules/utxo-staking/package.json b/modules/utxo-staking/package.json index 68ecd512461..263b778c09f 100644 --- a/modules/utxo-staking/package.json +++ b/modules/utxo-staking/package.json @@ -64,7 +64,7 @@ "@bitgo/utxo-core": "^1.41.6", "@bitgo/utxo-descriptors": "^2.2.0", "@bitgo/utxo-lib": "^11.24.4", - "@bitgo/wasm-utxo": "^5.11.0", + "@bitgo/wasm-utxo": "^5.15.0", "bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4", "bip322-js": "^2.0.0", "bitcoinjs-lib": "^6.1.7", diff --git a/yarn.lock b/yarn.lock index a6026b89b41..d58172efc73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1076,10 +1076,10 @@ resolved "https://registry.npmjs.org/@bitgo/wasm-ton/-/wasm-ton-1.1.1.tgz" integrity sha512-Y4x2V2ZcYWlmx42v7dlrKDtT2DuUt8smk8E98mh7RhpiifJhLk2v5RmXDwBl0A3v9TzUOU6qMOnSS/iZ8Pq52w== -"@bitgo/wasm-utxo@^5.11.0": - version "5.11.1" - resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-5.11.1.tgz#cc556b22b4746746067917c80034c69d796b7468" - integrity sha512-H7LRpyouRVefl+xBoR0ejVYJd2E6FWsZXeI1h47HBnTzqUtB+m7NcttxeyZbQugrYlSXeugDi8DpywWueepkCg== +"@bitgo/wasm-utxo@^5.15.0": + version "5.15.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-utxo/-/wasm-utxo-5.15.0.tgz#bd6d7e1d10e4779ff1d3432446ea704de691919d" + integrity sha512-UoNoXnFt/7VbPSr1I6i0SauSK7YQwmylORjrTmWbHCEaYEM9u2lsxGAb9latm/ctyENWQZxTIgXZzUKstnyQ7Q== "@brandonblack/musig@^0.0.1-alpha.0": version "0.0.1-alpha.1"