Skip to content

Commit f769dd5

Browse files
committed
refactor: harden output script detection
1 parent 3367370 commit f769dd5

7 files changed

Lines changed: 112 additions & 14 deletions

File tree

lib/swap/SwapDetector.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,44 @@ type DetectedSwap<T> = {
2525
vout: number;
2626
} & (T extends Transaction ? TransactionOutput : LiquidTxOutput);
2727

28+
const scriptForType: Record<OutputType, (input: Uint8Array) => Uint8Array> = {
29+
[OutputType.Legacy]: p2shOutput,
30+
[OutputType.Compatibility]: p2shP2wshOutput,
31+
[OutputType.Bech32]: p2wshOutput,
32+
[OutputType.Taproot]: p2trOutput,
33+
};
34+
2835
/**
2936
* Detects a swap output with the matching redeem script or tweaked key in a transaction
37+
*
38+
* @param redeemScriptOrTweakedKey redeem script or tweaked key of the swap
39+
* @param transaction transaction to scan for the swap output
40+
* @param expectedOutput the advertised output wrapper to detect; either an
41+
* {@link OutputType} or the exact expected output script. Detection is bound to
42+
* this wrapper so a lockup can never be matched against a different one.
3043
*/
3144
export const detectSwap = <T extends Transaction | LiquidTransaction>(
3245
redeemScriptOrTweakedKey: Uint8Array,
3346
transaction: T,
47+
expectedOutput: OutputType | Uint8Array,
3448
): DetectedSwap<T> | undefined => {
35-
const scripts: [OutputType, Uint8Array][] = [
36-
[OutputType.Legacy, p2shOutput(redeemScriptOrTweakedKey)],
37-
[OutputType.Compatibility, p2shP2wshOutput(redeemScriptOrTweakedKey)],
38-
[OutputType.Bech32, p2wshOutput(redeemScriptOrTweakedKey)],
39-
[OutputType.Taproot, p2trOutput(redeemScriptOrTweakedKey)],
40-
];
49+
const scripts = (
50+
[
51+
OutputType.Legacy,
52+
OutputType.Compatibility,
53+
OutputType.Bech32,
54+
OutputType.Taproot,
55+
] as OutputType[]
56+
)
57+
.map((type): [OutputType, Uint8Array] => [
58+
type,
59+
scriptForType[type](redeemScriptOrTweakedKey),
60+
])
61+
.filter(([type, script]) =>
62+
typeof expectedOutput === 'number'
63+
? type === expectedOutput
64+
: equalBytes(script, expectedOutput),
65+
);
4166

4267
const findMatch = (
4368
vout: number,

test/integration/Utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const sendFundsToOutput = async <
116116
: LiquidTransaction.fromHex(txHex);
117117

118118
return {
119-
...detectSwap(redeemScriptOrTweakedKey, transaction)!,
119+
...detectSwap(redeemScriptOrTweakedKey, transaction, outputType)!,
120120
blindingPrivateKey,
121121
type: outputType,
122122
transactionId,

test/integration/fundingAddressTree/FundingAddressTreeRefund.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const createFundingAddressOutput = async (
3939
const txHex = await bitcoinClient.getRawTransaction(transactionId);
4040
const transaction = Transaction.fromRaw(hex.decode(txHex));
4141

42-
const detected = detectSwap(tweakedPubKey, transaction)!;
42+
const detected = detectSwap(tweakedPubKey, transaction, OutputType.Taproot)!;
4343

4444
const utxo = {
4545
...detected,

test/integration/liquid/fundingAddressTree/FundingAddressTreeRefund.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ const createFundingAddressOutput = async (
8181
const txHex = await elementsClient.getRawTransaction(transactionId);
8282
const transaction = Transaction.fromHex(txHex);
8383

84-
const detected = detectSwap(tweakedPubKey, transaction)!;
84+
const detected = detectSwap(tweakedPubKey, transaction, OutputType.Taproot)!;
8585

8686
const utxo = {
8787
...detected,

test/integration/liquid/reverseSwapTree/ReverseSwapTreeCovenantClaim.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ describe.each`
105105
),
106106
);
107107

108-
const output = detectSwap(Buffer.from(tweakedMusig.aggPubkey), tx)!;
108+
const output = detectSwap(
109+
Buffer.from(tweakedMusig.aggPubkey),
110+
tx,
111+
OutputType.Taproot,
112+
)!;
109113

110114
return {
111115
tree,

test/unit/liquid/swap/SwapDetector.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('Liquid SwapDetector', () => {
5353
nonce,
5454
);
5555

56-
const output = detectSwap(redeemScript, transaction)!;
56+
const output = detectSwap(redeemScript, transaction, type)!;
5757

5858
expect(output).not.toBeUndefined();
5959
expect(output.vout).toEqual(1);

test/unit/swap/SwapDetector.spec.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe('SwapDetector', () => {
4848
script: Script.encode(['RETURN']),
4949
});
5050

51-
const output = detectSwap(redeemScript, transaction)!;
51+
const output = detectSwap(redeemScript, transaction, type)!;
5252

5353
expect(output).not.toBeUndefined();
5454
expect(output.vout).toEqual(1);
@@ -77,7 +77,7 @@ describe('SwapDetector', () => {
7777
amount: 312n,
7878
});
7979

80-
const output = detectSwap(tweakedKeys, transaction)!;
80+
const output = detectSwap(tweakedKeys, transaction, OutputType.Taproot)!;
8181

8282
expect(output).not.toBeUndefined();
8383
expect(output.vout).toEqual(1);
@@ -86,6 +86,75 @@ describe('SwapDetector', () => {
8686
expect(output.script).toEqual(p2trOutput(tweakedKeys));
8787
});
8888

89+
test('should restrict detection to an expected OutputType', () => {
90+
const publicKey = secp256k1.getPublicKey(secp256k1.utils.randomSecretKey());
91+
const redeemScript = swapScript(sha256(publicKey), publicKey, publicKey, 1);
92+
93+
// A decoy output paying to the same redeem script under a different wrapper
94+
const decoy = outputFunctionForType(OutputType.Bech32)!(redeemScript);
95+
const advertised = outputFunctionForType(OutputType.Compatibility)!(
96+
redeemScript,
97+
);
98+
99+
const transaction = new Transaction({ allowUnknownOutputs: true });
100+
transaction.addOutput({ amount: 42n, script: decoy });
101+
transaction.addOutput({ amount: 21n, script: advertised });
102+
103+
// The decoy wrapper at vout 0 is ignored; only the advertised one is detected
104+
const output = detectSwap(
105+
redeemScript,
106+
transaction,
107+
OutputType.Compatibility,
108+
)!;
109+
110+
expect(output.vout).toEqual(1);
111+
expect(output.amount).toEqual(21n);
112+
expect(output.type).toEqual(OutputType.Compatibility);
113+
expect(output.script).toEqual(advertised);
114+
});
115+
116+
test('should restrict detection to an expected output script', () => {
117+
const publicKey = secp256k1.getPublicKey(secp256k1.utils.randomSecretKey());
118+
const redeemScript = swapScript(sha256(publicKey), publicKey, publicKey, 1);
119+
120+
const decoy = outputFunctionForType(OutputType.Bech32)!(redeemScript);
121+
const advertised = outputFunctionForType(OutputType.Compatibility)!(
122+
redeemScript,
123+
);
124+
125+
const transaction = new Transaction({ allowUnknownOutputs: true });
126+
transaction.addOutput({ amount: 42n, script: decoy });
127+
transaction.addOutput({ amount: 21n, script: advertised });
128+
129+
const output = detectSwap(redeemScript, transaction, advertised)!;
130+
131+
expect(output.vout).toEqual(1);
132+
expect(output.amount).toEqual(21n);
133+
expect(output.type).toEqual(OutputType.Compatibility);
134+
expect(output.script).toEqual(advertised);
135+
});
136+
137+
test('should return undefined when no output matches the expectation', () => {
138+
const publicKey = secp256k1.getPublicKey(secp256k1.utils.randomSecretKey());
139+
const redeemScript = swapScript(sha256(publicKey), publicKey, publicKey, 1);
140+
141+
const transaction = new Transaction({ allowUnknownOutputs: true });
142+
transaction.addOutput({
143+
amount: 42n,
144+
script: outputFunctionForType(OutputType.Bech32)!(redeemScript),
145+
});
146+
147+
// The advertised wrapper is not present in the transaction
148+
expect(
149+
detectSwap(redeemScript, transaction, OutputType.Taproot),
150+
).toBeUndefined();
151+
152+
// An expected script that is not a valid wrapper of the key is rejected
153+
expect(
154+
detectSwap(redeemScript, transaction, randomBytes(34)),
155+
).toBeUndefined();
156+
});
157+
89158
test('should return undefined no swap can be found', () => {
90159
const publicKey = secp256k1.getPublicKey(secp256k1.utils.randomSecretKey());
91160
const transaction = new Transaction({
@@ -100,7 +169,7 @@ describe('SwapDetector', () => {
100169
amount: 312n,
101170
});
102171

103-
const output = detectSwap(randomBytes(32), transaction);
172+
const output = detectSwap(randomBytes(32), transaction, OutputType.Bech32);
104173

105174
expect(output).toBeUndefined();
106175
});

0 commit comments

Comments
 (0)