diff --git a/modules/abstract-utxo/src/transaction/explainTransaction.ts b/modules/abstract-utxo/src/transaction/explainTransaction.ts index 57c3b0b14b..35711b5651 100644 --- a/modules/abstract-utxo/src/transaction/explainTransaction.ts +++ b/modules/abstract-utxo/src/transaction/explainTransaction.ts @@ -6,7 +6,7 @@ import { getDescriptorMapFromWallet, isDescriptorWallet } from '../descriptor'; import { toBip32Triple } from '../keychains'; import { getPolicyForEnv } from '../descriptor/validatePolicy'; -import { getReplayProtectionOutputScripts } from './fixedScript/replayProtection'; +import { getReplayProtectionPubkeys } from './fixedScript/replayProtection'; import type { TransactionExplanationUtxolibLegacy, TransactionExplanationUtxolibPsbt, @@ -63,7 +63,7 @@ export function explainTx( } return fixedScript.explainPsbtWasm(tx, walletXpubs, { replayProtection: { - outputScripts: getReplayProtectionOutputScripts(network), + publicKeys: getReplayProtectionPubkeys(network), }, }); } else { diff --git a/modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts b/modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts index 0f884a3fd5..40f7a659c1 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts @@ -44,7 +44,7 @@ export function explainPsbtWasm( params: { replayProtection: { checkSignature?: boolean; - outputScripts: Buffer[]; + publicKeys: Buffer[]; }; customChangeWalletXpubs?: Triple; } diff --git a/modules/abstract-utxo/src/transaction/fixedScript/replayProtection.ts b/modules/abstract-utxo/src/transaction/fixedScript/replayProtection.ts index 96bf1c9113..b554ab001d 100644 --- a/modules/abstract-utxo/src/transaction/fixedScript/replayProtection.ts +++ b/modules/abstract-utxo/src/transaction/fixedScript/replayProtection.ts @@ -1,23 +1,48 @@ -import * as wasmUtxo from '@bitgo/wasm-utxo'; import * as utxolib from '@bitgo/utxo-lib'; +import { Descriptor, utxolibCompat } from '@bitgo/wasm-utxo'; -export function getReplayProtectionAddresses(network: utxolib.Network): string[] { +// 33p1q7mTGyeM5UnZERGiMcVUkY12SCsatA +// bitcoincash:pqt5x9w0m6z0f3znjkkx79wl3l7ywrszesemp8xgpf +const pubkeyProd = Buffer.from('0255b9f71ac2c78fffd83e3e37b9e17ae70d5437b7f56d0ed2e93b7de08015aa59', 'hex'); + +// 2MuMnPoSDgWEpNWH28X2nLtYMXQJCyT61eY +// bchtest:pqtjmnzwqffkrk2349g3cecfwwjwxusvnq87n07cal +const pubkeyTestnet = Buffer.from('0219da48412c2268865fe8c126327d1b12eee350a3b69eb09e3323cc9a11828945', 'hex'); + +export function getReplayProtectionPubkeys(network: utxolib.Network): Buffer[] { switch (network) { case utxolib.networks.bitcoincash: case utxolib.networks.bitcoinsv: - return ['33p1q7mTGyeM5UnZERGiMcVUkY12SCsatA']; - case utxolib.networks.bitcoincashTestnet: + return [pubkeyProd]; case utxolib.networks.bitcoinsvTestnet: - return ['2MuMnPoSDgWEpNWH28X2nLtYMXQJCyT61eY']; + case utxolib.networks.bitcoincashTestnet: + return [pubkeyTestnet]; } - return []; } -export function getReplayProtectionOutputScripts(network: utxolib.Network): Buffer[] { - return getReplayProtectionAddresses(network).map((address) => - Buffer.from(wasmUtxo.utxolibCompat.toOutputScript(address, network)) - ); +export function createReplayProtectionOutputScript(pubkey: Buffer): Buffer { + const descriptor = Descriptor.fromString(`sh(pk(${pubkey.toString('hex')}))`, 'definite'); + return Buffer.from(descriptor.scriptPubkey()); +} + +const replayProtectionScriptsProd = [createReplayProtectionOutputScript(pubkeyProd)]; +const replayProtectionScriptsTestnet = [createReplayProtectionOutputScript(pubkeyTestnet)]; + +export function getReplayProtectionAddresses( + network: utxolib.Network, + format: 'default' | 'cashaddr' = 'default' +): string[] { + switch (network) { + case utxolib.networks.bitcoincash: + case utxolib.networks.bitcoinsv: + return replayProtectionScriptsProd.map((script) => utxolibCompat.fromOutputScript(script, network, format)); + case utxolib.networks.bitcoinsvTestnet: + case utxolib.networks.bitcoincashTestnet: + return replayProtectionScriptsTestnet.map((script) => utxolibCompat.fromOutputScript(script, network, format)); + default: + return []; + } } export function isReplayProtectionUnspent( diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts index d38467a81e..822ea0a7c6 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts @@ -65,7 +65,7 @@ function describeTransactionWith(acidTest: testutil.AcidTest) { const wasmExplanation = explainPsbtWasm(wasmPsbt, walletXpubs, { replayProtection: { - outputScripts: [acidTest.getReplayProtectionOutputScript()], + publicKeys: [acidTest.getReplayProtectionPublicKey()], }, }); @@ -95,7 +95,7 @@ function describeTransactionWith(acidTest: testutil.AcidTest) { it('returns custom change outputs when parameter is set', function () { const wasmExplanation = explainPsbtWasm(wasmPsbt, walletXpubs, { replayProtection: { - outputScripts: [acidTest.getReplayProtectionOutputScript()], + publicKeys: [acidTest.getReplayProtectionPublicKey()], }, customChangeWalletXpubs, }); diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts index c1404d2666..f5db82dccd 100644 --- a/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts @@ -134,7 +134,7 @@ function describeParseTransactionWith( acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple, { replayProtection: { - outputScripts: [acidTest.getReplayProtectionOutputScript()], + publicKeys: [acidTest.getReplayProtectionPublicKey()], }, } ); diff --git a/modules/abstract-utxo/test/unit/transaction/fixedScript/replayProtection.ts b/modules/abstract-utxo/test/unit/transaction/fixedScript/replayProtection.ts new file mode 100644 index 0000000000..55545e2d0e --- /dev/null +++ b/modules/abstract-utxo/test/unit/transaction/fixedScript/replayProtection.ts @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; + +import * as utxolib from '@bitgo/utxo-lib'; + +import { + getReplayProtectionPubkeys, + getReplayProtectionAddresses, +} from '../../../../src/transaction/fixedScript/replayProtection'; + +describe('replayProtection', function () { + for (const network of utxolib.getNetworkList()) { + const networkName = utxolib.getNetworkName(network); + assert(networkName, 'network name is required'); + + describe(`${networkName}`, function () { + if ( + utxolib.getMainnet(network) === utxolib.networks.bitcoincash || + utxolib.getMainnet(network) === utxolib.networks.bitcoinsv + ) { + it('should have keys that correspond to addresses via p2shP2pk', function () { + const actualAddressesDefault = getReplayProtectionAddresses(network, 'default'); + + switch (network) { + case utxolib.networks.bitcoincash: + case utxolib.networks.bitcoinsv: + assert.deepStrictEqual(actualAddressesDefault, ['33p1q7mTGyeM5UnZERGiMcVUkY12SCsatA']); + break; + case utxolib.networks.bitcoincashTestnet: + case utxolib.networks.bitcoinsvTestnet: + assert.deepStrictEqual(actualAddressesDefault, ['2MuMnPoSDgWEpNWH28X2nLtYMXQJCyT61eY']); + break; + default: + throw new Error(`illegal state`); + } + + if (utxolib.getMainnet(network) !== utxolib.networks.bitcoincash) { + return; + } + + const actualAddressesCashaddr = getReplayProtectionAddresses(network, 'cashaddr'); + switch (network) { + case utxolib.networks.bitcoincash: + assert.deepStrictEqual(actualAddressesCashaddr, [ + 'bitcoincash:pqt5x9w0m6z0f3znjkkx79wl3l7ywrszesemp8xgpf', + ]); + break; + case utxolib.networks.bitcoincashTestnet: + assert.deepStrictEqual(actualAddressesCashaddr, ['bchtest:pqtjmnzwqffkrk2349g3cecfwwjwxusvnq87n07cal']); + break; + default: + throw new Error(`illegal state`); + } + }); + } else { + it('should have no replay protection', function () { + assert.deepEqual(getReplayProtectionPubkeys(network), []); + assert.deepEqual(getReplayProtectionAddresses(network), []); + }); + } + }); + } +}); diff --git a/modules/utxo-lib/src/testutil/psbt.ts b/modules/utxo-lib/src/testutil/psbt.ts index 7e4bbb185a..0a6285257b 100644 --- a/modules/utxo-lib/src/testutil/psbt.ts +++ b/modules/utxo-lib/src/testutil/psbt.ts @@ -340,8 +340,12 @@ export class AcidTest { return `${networkName} ${this.signStage} ${this.txFormat}`; } + getReplayProtectionPublicKey(): Buffer { + return this.rootWalletKeys.user.publicKey; + } + getReplayProtectionOutputScript(): Buffer { - const { scriptPubKey } = createOutputScriptP2shP2pk(this.rootWalletKeys.user.publicKey); + const { scriptPubKey } = createOutputScriptP2shP2pk(this.getReplayProtectionPublicKey()); assert(scriptPubKey); return scriptPubKey; }