Skip to content

Commit b2c8c47

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): refactor replay protection to use pubkeys
Refactor replay protection to use the public keys directly instead of output scripts. The output scripts are now derived from the public keys, making the code more maintainable and easier to understand. Add tests for replay protection to verify that the output scripts match those computed from descriptors. Issue: BTC-2806 Co-authored-by: llm-git <[email protected]>
1 parent 724af70 commit b2c8c47

File tree

6 files changed

+66
-16
lines changed

6 files changed

+66
-16
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+
});

0 commit comments

Comments
 (0)