Skip to content

Commit bc295d8

Browse files
Merge pull request #7696 from BitGo/BTC-2806.RP.1
feat(utxo): refactor replay protection to use public keys
2 parents 1b3ea83 + b2c8c47 commit bc295d8

File tree

7 files changed

+71
-17
lines changed

7 files changed

+71
-17
lines changed

modules/abstract-utxo/src/transaction/explainTransaction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getDescriptorMapFromWallet, isDescriptorWallet } from '../descriptor';
66
import { toBip32Triple } from '../keychains';
77
import { getPolicyForEnv } from '../descriptor/validatePolicy';
88

9-
import { getReplayProtectionOutputScripts } from './fixedScript/replayProtection';
9+
import { getReplayProtectionPubkeys } from './fixedScript/replayProtection';
1010
import type {
1111
TransactionExplanationUtxolibLegacy,
1212
TransactionExplanationUtxolibPsbt,
@@ -63,7 +63,7 @@ export function explainTx<TNumber extends number | bigint>(
6363
}
6464
return fixedScript.explainPsbtWasm(tx, walletXpubs, {
6565
replayProtection: {
66-
outputScripts: getReplayProtectionOutputScripts(network),
66+
publicKeys: getReplayProtectionPubkeys(network),
6767
},
6868
});
6969
} else {

modules/abstract-utxo/src/transaction/fixedScript/explainPsbtWasm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function explainPsbtWasm(
4444
params: {
4545
replayProtection: {
4646
checkSignature?: boolean;
47-
outputScripts: Buffer[];
47+
publicKeys: Buffer[];
4848
};
4949
customChangeWalletXpubs?: Triple<string>;
5050
}

modules/abstract-utxo/src/transaction/fixedScript/replayProtection.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,45 @@
1-
import * as wasmUtxo from '@bitgo/wasm-utxo';
21
import * as utxolib from '@bitgo/utxo-lib';
2+
import { utxolibCompat } from '@bitgo/wasm-utxo';
33

4-
export function getReplayProtectionAddresses(network: utxolib.Network): string[] {
4+
export const pubkeyProd = Buffer.from('0255b9f71ac2c78fffd83e3e37b9e17ae70d5437b7f56d0ed2e93b7de08015aa59', 'hex');
5+
6+
export const pubkeyTestnet = Buffer.from('0219da48412c2268865fe8c126327d1b12eee350a3b69eb09e3323cc9a11828945', 'hex');
7+
8+
export function getReplayProtectionPubkeys(network: utxolib.Network): Buffer[] {
59
switch (network) {
610
case utxolib.networks.bitcoincash:
711
case utxolib.networks.bitcoinsv:
8-
return ['33p1q7mTGyeM5UnZERGiMcVUkY12SCsatA'];
9-
case utxolib.networks.bitcoincashTestnet:
12+
return [pubkeyProd];
1013
case utxolib.networks.bitcoinsvTestnet:
11-
return ['2MuMnPoSDgWEpNWH28X2nLtYMXQJCyT61eY'];
14+
case utxolib.networks.bitcoincashTestnet:
15+
return [pubkeyTestnet];
1216
}
13-
1417
return [];
1518
}
1619

17-
export function getReplayProtectionOutputScripts(network: utxolib.Network): Buffer[] {
18-
return getReplayProtectionAddresses(network).map((address) =>
19-
Buffer.from(wasmUtxo.utxolibCompat.toOutputScript(address, network))
20-
);
20+
// sh(pk(pubkeyProd))
21+
// 33p1q7mTGyeM5UnZERGiMcVUkY12SCsatA
22+
// bitcoincash:pqt5x9w0m6z0f3znjkkx79wl3l7ywrszesemp8xgpf
23+
const replayProtectionScriptsProd = [Buffer.from('a914174315cfde84f4c45395ac6f15df8ffc470e02cc87', 'hex')];
24+
// sh(pk(pubkeyTestnet))
25+
// 2MuMnPoSDgWEpNWH28X2nLtYMXQJCyT61eY
26+
// bchtest:pqtjmnzwqffkrk2349g3cecfwwjwxusvnq87n07cal
27+
const replayProtectionScriptsTestnet = [Buffer.from('a914172dcc4e025361d951a9511c670973a4e3720c9887', 'hex')];
28+
29+
export function getReplayProtectionAddresses(
30+
network: utxolib.Network,
31+
format: 'default' | 'cashaddr' = 'default'
32+
): string[] {
33+
switch (network) {
34+
case utxolib.networks.bitcoincash:
35+
case utxolib.networks.bitcoinsv:
36+
return replayProtectionScriptsProd.map((script) => utxolibCompat.fromOutputScript(script, network, format));
37+
case utxolib.networks.bitcoinsvTestnet:
38+
case utxolib.networks.bitcoincashTestnet:
39+
return replayProtectionScriptsTestnet.map((script) => utxolibCompat.fromOutputScript(script, network, format));
40+
default:
41+
return [];
42+
}
2143
}
2244

2345
export function isReplayProtectionUnspent<TNumber extends number | bigint>(

modules/abstract-utxo/test/unit/transaction/fixedScript/explainPsbt.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function describeTransactionWith(acidTest: testutil.AcidTest) {
6565

6666
const wasmExplanation = explainPsbtWasm(wasmPsbt, walletXpubs, {
6767
replayProtection: {
68-
outputScripts: [acidTest.getReplayProtectionOutputScript()],
68+
publicKeys: [acidTest.getReplayProtectionPublicKey()],
6969
},
7070
});
7171

@@ -95,7 +95,7 @@ function describeTransactionWith(acidTest: testutil.AcidTest) {
9595
it('returns custom change outputs when parameter is set', function () {
9696
const wasmExplanation = explainPsbtWasm(wasmPsbt, walletXpubs, {
9797
replayProtection: {
98-
outputScripts: [acidTest.getReplayProtectionOutputScript()],
98+
publicKeys: [acidTest.getReplayProtectionPublicKey()],
9999
},
100100
customChangeWalletXpubs,
101101
});

modules/abstract-utxo/test/unit/transaction/fixedScript/parsePsbt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ function describeParseTransactionWith(
134134
acidTest.rootWalletKeys.triple.map((k) => k.neutered().toBase58()) as Triple<string>,
135135
{
136136
replayProtection: {
137-
outputScripts: [acidTest.getReplayProtectionOutputScript()],
137+
publicKeys: [acidTest.getReplayProtectionPublicKey()],
138138
},
139139
}
140140
);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import assert from 'node:assert/strict';
2+
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
import { Descriptor, utxolibCompat } from '@bitgo/wasm-utxo';
5+
6+
import {
7+
getReplayProtectionAddresses,
8+
pubkeyProd,
9+
pubkeyTestnet,
10+
} from '../../../../src/transaction/fixedScript/replayProtection';
11+
12+
function createReplayProtectionOutputScript(pubkey: Buffer): Buffer {
13+
const descriptor = Descriptor.fromString(`sh(pk(${pubkey.toString('hex')}))`, 'definite');
14+
return Buffer.from(descriptor.scriptPubkey());
15+
}
16+
17+
describe('replayProtection', function () {
18+
it('should have scriptPubKeys that match descriptor computation', function () {
19+
for (const pubkey of [pubkeyProd, pubkeyTestnet]) {
20+
const network = pubkey === pubkeyProd ? utxolib.networks.bitcoincash : utxolib.networks.bitcoincashTestnet;
21+
const expectedScript = createReplayProtectionOutputScript(pubkey);
22+
const actualAddresses = getReplayProtectionAddresses(network);
23+
assert.equal(actualAddresses.length, 1);
24+
const actualScript = Buffer.from(utxolibCompat.toOutputScript(actualAddresses[0], network));
25+
assert.deepStrictEqual(actualScript, expectedScript);
26+
}
27+
});
28+
});

modules/utxo-lib/src/testutil/psbt.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,12 @@ export class AcidTest {
340340
return `${networkName} ${this.signStage} ${this.txFormat}`;
341341
}
342342

343+
getReplayProtectionPublicKey(): Buffer {
344+
return this.rootWalletKeys.user.publicKey;
345+
}
346+
343347
getReplayProtectionOutputScript(): Buffer {
344-
const { scriptPubKey } = createOutputScriptP2shP2pk(this.rootWalletKeys.user.publicKey);
348+
const { scriptPubKey } = createOutputScriptP2shP2pk(this.getReplayProtectionPublicKey());
345349
assert(scriptPubKey);
346350
return scriptPubKey;
347351
}

0 commit comments

Comments
 (0)