Skip to content

Commit 72db9aa

Browse files
OttoAllmendingerllm-git
andcommitted
fix(abstract-utxo): check optional params for allowExternalChangeAddress
Fix the way we handle `allowExternalChangeAddress` when verifying transactions with external change addresses. Use the new ExpectedOutput interface to mark the change address as optional, as it's not required to exist in the transaction. Issue: BTC-2962 Co-authored-by: llm-git <llm-git@ttll.de>
1 parent 710e60b commit 72db9aa

File tree

3 files changed

+43
-19
lines changed

3 files changed

+43
-19
lines changed

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

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ import * as utxolib from '@bitgo/utxo-lib';
77
import type { AbstractUtxoCoin, ParseTransactionOptions } from '../../abstractUtxoCoin';
88
import type { FixedScriptWalletOutput, Output, ParsedTransaction } from '../types';
99
import { fetchKeychains, getKeySignatures, toKeychainTriple, UtxoKeychain, UtxoNamedKeychains } from '../../keychains';
10-
import { ComparableOutput, outputDifference } from '../outputDifference';
1110
import {
1211
assertValidTransactionRecipient,
1312
fromExtendedAddressFormatToScript,
1413
isScriptRecipient,
1514
toExtendedAddressFormat,
15+
toOutputScript,
1616
} from '../recipient';
17+
import { ComparableOutput, ExpectedOutput, outputDifference } from '../outputDifference';
1718

1819
import type { TransactionExplanation } from './explainTransaction';
1920
import { CustomChangeOptions, parseOutput } from './parseOutput';
2021

21-
export type ComparableOutputWithExternal<TValue> = ComparableOutput<TValue> & {
22+
export type ComparableOutputWithExternal<TValue> = (ComparableOutput<TValue> | ExpectedOutput) & {
2223
external: boolean | undefined;
2324
};
2425

@@ -76,21 +77,37 @@ function toExpectedOutputs(
7677
allowExternalChangeAddress?: boolean;
7778
changeAddress?: string;
7879
}
79-
): Output[] {
80+
): ExpectedOutput[] {
8081
// verify that each recipient from txParams has their own output
81-
const expectedOutputs = (txParams.recipients ?? []).flatMap((output) => {
82+
const expectedOutputs: ExpectedOutput[] = (txParams.recipients ?? []).flatMap((output) => {
8283
if (output.address === undefined) {
84+
assert('script' in output, 'script is required for non-encodeable scriptPubkeys');
8385
if (output.amount.toString() !== '0') {
8486
throw new Error(`Only zero amounts allowed for non-encodeable scriptPubkeys: ${output}`);
8587
}
86-
return [output];
88+
return [
89+
{
90+
script: toOutputScript(output, coin.name),
91+
value: output.amount === 'max' ? 'max' : BigInt(output.amount),
92+
},
93+
];
8794
}
88-
return [{ ...output, address: coin.canonicalAddress(output.address) }];
95+
return [
96+
{
97+
script: fromExtendedAddressFormatToScript(output.address, coin.name),
98+
value: output.amount === 'max' ? 'max' : BigInt(output.amount),
99+
},
100+
];
89101
});
90102
if (txParams.allowExternalChangeAddress && txParams.changeAddress) {
91-
// when an external change address is explicitly specified, count all outputs going towards that
103+
// When an external change address is explicitly specified, count all outputs going towards that
92104
// address in the expected outputs (regardless of the output amount)
93-
expectedOutputs.push({ address: coin.canonicalAddress(txParams.changeAddress), amount: 'max' });
105+
// Note that the output is not required to exist, so we filter it out later.
106+
expectedOutputs.push({
107+
script: toOutputScript(txParams.changeAddress, coin.name),
108+
value: 'max',
109+
optional: true,
110+
});
94111
}
95112
return expectedOutputs;
96113
}
@@ -206,15 +223,9 @@ export async function parseTransaction<TNumber extends bigint | number>(
206223
}));
207224
}
208225

209-
const missingOutputs = outputDifference(
210-
toComparableOutputsWithExternal(expectedOutputs),
211-
toComparableOutputsWithExternal(allOutputs)
212-
);
226+
const missingOutputs = outputDifference(expectedOutputs, toComparableOutputsWithExternal(allOutputs));
213227

214-
const implicitOutputs = outputDifference(
215-
toComparableOutputsWithExternal(allOutputDetails),
216-
toComparableOutputsWithExternal(expectedOutputs)
217-
);
228+
const implicitOutputs = outputDifference(toComparableOutputsWithExternal(allOutputDetails), expectedOutputs);
218229
const explicitOutputs = outputDifference(toComparableOutputsWithExternal(allOutputDetails), implicitOutputs);
219230

220231
// these are all the non-wallet outputs that had been originally explicitly specified in recipients
@@ -244,7 +255,7 @@ export async function parseTransaction<TNumber extends bigint | number>(
244255
coin.amountType
245256
);
246257

247-
function toOutputs(outputs: ComparableOutputWithExternal<bigint | 'max'>[]): Output[] {
258+
function toOutputs(outputs: ExpectedOutput[] | ComparableOutputWithExternal<bigint | 'max'>[]): Output[] {
248259
return outputs.map((output) => ({
249260
address: toExtendedAddressFormat(output.script, coin.name),
250261
amount: output.value.toString(),

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,19 @@ export function fromExtendedAddressFormatToScript(extendedAddress: string, coinN
3333
return utxolib.addressFormat.toOutputScriptTryFormats(result.address, network);
3434
}
3535

36+
export function toOutputScript(v: string | { address: string } | { script: string }, coinName: UtxoCoinName): Buffer {
37+
if (typeof v === 'string') {
38+
return fromExtendedAddressFormatToScript(v, coinName);
39+
}
40+
if ('script' in v) {
41+
return Buffer.from(v.script, 'hex');
42+
}
43+
if ('address' in v) {
44+
return fromExtendedAddressFormatToScript(v.address, coinName);
45+
}
46+
throw new Error('invalid input');
47+
}
48+
3649
/**
3750
* Convert a script or address to the extended address format.
3851
* @param script

modules/abstract-utxo/test/unit/transaction/outputDifference.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import {
1010
} from '../../../src/transaction/outputDifference';
1111

1212
describe('outputDifference', function () {
13-
function output(script: string, value: bigint | number): ActualOutput;
14-
function output(script: string, value: 'max'): ExpectedOutput;
13+
function output(script: string, value: bigint | number, optional?: boolean): ActualOutput;
14+
function output(script: string, value: 'max', optional?: boolean): ExpectedOutput;
1515
function output(script: string, value: bigint | number | 'max', optional?: boolean): ActualOutput | ExpectedOutput {
1616
const scriptBuffer = Buffer.from(script, 'hex');
1717
if (scriptBuffer.toString('hex') !== script) {

0 commit comments

Comments
 (0)