Skip to content

Commit 38f830f

Browse files
OttoAllmendingerllm-git
andcommitted
feat(abstract-utxo): replace replay protection output scripts with pubkeys
Replace the implementation of replay protection to use pubkeys instead of output scripts directly. This allows more flexibility in how the scripts are created and addresses are encoded. Add tests to verify that the pubkeys correctly map to the expected addresses across different networks and address formats. Issue: BTC-2806 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 1b44b1d commit 38f830f

File tree

6 files changed

+103
-16
lines changed

6 files changed

+103
-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: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
1-
import * as wasmUtxo from '@bitgo/wasm-utxo';
21
import * as utxolib from '@bitgo/utxo-lib';
2+
import { Descriptor, utxolibCompat } from '@bitgo/wasm-utxo';
33

4-
export function getReplayProtectionAddresses(network: utxolib.Network): string[] {
4+
// 33p1q7mTGyeM5UnZERGiMcVUkY12SCsatA
5+
// bitcoincash:pqt5x9w0m6z0f3znjkkx79wl3l7ywrszesemp8xgpf
6+
const pubkeyProd = Buffer.from('0255b9f71ac2c78fffd83e3e37b9e17ae70d5437b7f56d0ed2e93b7de08015aa59', 'hex');
7+
8+
// 2MuMnPoSDgWEpNWH28X2nLtYMXQJCyT61eY
9+
// bchtest:pqtjmnzwqffkrk2349g3cecfwwjwxusvnq87n07cal
10+
const pubkeyTestnet = Buffer.from('0219da48412c2268865fe8c126327d1b12eee350a3b69eb09e3323cc9a11828945', 'hex');
11+
12+
export function getReplayProtectionPubkeys(network: utxolib.Network): Buffer[] {
513
switch (network) {
614
case utxolib.networks.bitcoincash:
715
case utxolib.networks.bitcoinsv:
8-
return ['33p1q7mTGyeM5UnZERGiMcVUkY12SCsatA'];
9-
case utxolib.networks.bitcoincashTestnet:
16+
return [pubkeyProd];
1017
case utxolib.networks.bitcoinsvTestnet:
11-
return ['2MuMnPoSDgWEpNWH28X2nLtYMXQJCyT61eY'];
18+
case utxolib.networks.bitcoincashTestnet:
19+
return [pubkeyTestnet];
1220
}
13-
1421
return [];
1522
}
1623

17-
export function getReplayProtectionOutputScripts(network: utxolib.Network): Buffer[] {
18-
return getReplayProtectionAddresses(network).map((address) =>
19-
Buffer.from(wasmUtxo.utxolibCompat.toOutputScript(address, network))
20-
);
24+
export function createReplayProtectionOutputScript(pubkey: Buffer): Buffer {
25+
const descriptor = Descriptor.fromString(`sh(pk(${pubkey.toString('hex')}))`, 'definite');
26+
return Buffer.from(descriptor.scriptPubkey());
27+
}
28+
29+
const replayProtectionScriptsProd = [createReplayProtectionOutputScript(pubkeyProd)];
30+
const replayProtectionScriptsTestnet = [createReplayProtectionOutputScript(pubkeyTestnet)];
31+
32+
export function getReplayProtectionAddresses(
33+
network: utxolib.Network,
34+
format: 'default' | 'cashaddr' = 'default'
35+
): string[] {
36+
switch (network) {
37+
case utxolib.networks.bitcoincash:
38+
case utxolib.networks.bitcoinsv:
39+
return replayProtectionScriptsProd.map((script) => utxolibCompat.fromOutputScript(script, network, format));
40+
case utxolib.networks.bitcoinsvTestnet:
41+
case utxolib.networks.bitcoincashTestnet:
42+
return replayProtectionScriptsTestnet.map((script) => utxolibCompat.fromOutputScript(script, network, format));
43+
default:
44+
return [];
45+
}
2146
}
2247

2348
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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import assert from 'node:assert/strict';
2+
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
5+
import {
6+
getReplayProtectionPubkeys,
7+
getReplayProtectionAddresses,
8+
} from '../../../../src/transaction/fixedScript/replayProtection';
9+
10+
describe('replayProtection', function () {
11+
for (const network of utxolib.getNetworkList()) {
12+
const networkName = utxolib.getNetworkName(network);
13+
assert(networkName, 'network name is required');
14+
15+
describe(`${networkName}`, function () {
16+
if (
17+
utxolib.getMainnet(network) === utxolib.networks.bitcoincash ||
18+
utxolib.getMainnet(network) === utxolib.networks.bitcoinsv
19+
) {
20+
it('should have keys that correspond to addresses via p2shP2pk', function () {
21+
const actualAddressesDefault = getReplayProtectionAddresses(network, 'default');
22+
23+
switch (network) {
24+
case utxolib.networks.bitcoincash:
25+
case utxolib.networks.bitcoinsv:
26+
assert.deepStrictEqual(actualAddressesDefault, ['33p1q7mTGyeM5UnZERGiMcVUkY12SCsatA']);
27+
break;
28+
case utxolib.networks.bitcoincashTestnet:
29+
case utxolib.networks.bitcoinsvTestnet:
30+
assert.deepStrictEqual(actualAddressesDefault, ['2MuMnPoSDgWEpNWH28X2nLtYMXQJCyT61eY']);
31+
break;
32+
default:
33+
throw new Error(`illegal state`);
34+
}
35+
36+
if (utxolib.getMainnet(network) !== utxolib.networks.bitcoincash) {
37+
return;
38+
}
39+
40+
const actualAddressesCashaddr = getReplayProtectionAddresses(network, 'cashaddr');
41+
switch (network) {
42+
case utxolib.networks.bitcoincash:
43+
assert.deepStrictEqual(actualAddressesCashaddr, [
44+
'bitcoincash:pqt5x9w0m6z0f3znjkkx79wl3l7ywrszesemp8xgpf',
45+
]);
46+
break;
47+
case utxolib.networks.bitcoincashTestnet:
48+
assert.deepStrictEqual(actualAddressesCashaddr, ['bchtest:pqtjmnzwqffkrk2349g3cecfwwjwxusvnq87n07cal']);
49+
break;
50+
default:
51+
throw new Error(`illegal state`);
52+
}
53+
});
54+
} else {
55+
it('should have no replay protection', function () {
56+
assert.deepEqual(getReplayProtectionPubkeys(network), []);
57+
assert.deepEqual(getReplayProtectionAddresses(network), []);
58+
});
59+
}
60+
});
61+
}
62+
});

0 commit comments

Comments
 (0)