Skip to content

Commit c20fac1

Browse files
authored
Merge pull request #7179 from BitGo/BTC-2634-convert-to-supported-prefixes
fix: convert unsupported utxolib prefixes
2 parents 0b42fb4 + 3c5b4ba commit c20fac1

File tree

2 files changed

+59
-8
lines changed

2 files changed

+59
-8
lines changed

modules/abstract-lightning/src/lightning/lightningUtils.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,19 @@ export function addIPCaveatToMacaroon(macaroonBase64: string, ip: string): strin
114114
return bytesToBase64(macaroon.exportBinary());
115115
}
116116

117-
const PURPOSE_WRAPPED_P2WKH = 49;
118-
const PURPOSE_P2WKH = 84;
119-
const PURPOSE_P2TR = 86;
120-
const PURPOSE_ALL_OTHERS = 1017;
117+
export const PURPOSE_WRAPPED_P2WKH = 49;
118+
export const PURPOSE_P2WKH = 84;
119+
export const PURPOSE_P2TR = 86;
120+
export const PURPOSE_ALL_OTHERS = 1017;
121121

122-
type ExtendedKeyPurpose =
122+
export type ExtendedKeyPurpose =
123123
| typeof PURPOSE_WRAPPED_P2WKH
124124
| typeof PURPOSE_P2WKH
125125
| typeof PURPOSE_P2TR
126126
| typeof PURPOSE_ALL_OTHERS;
127127

128+
export type ExtendedKeyAddressPurpose = typeof PURPOSE_WRAPPED_P2WKH | typeof PURPOSE_P2WKH | typeof PURPOSE_P2TR;
129+
128130
/**
129131
* Converts an extended public key (xpub) to the appropriate prefix (ypub, vpub, etc.) based on its purpose and network.
130132
*/
@@ -151,6 +153,43 @@ function convertXpubPrefix(xpub: string, purpose: ExtendedKeyPurpose, isMainnet:
151153
return bs58check.encode(data);
152154
}
153155

156+
/**
157+
* Converts a prefix related to purpose and network (ypub, vpub, etc.) to extended public key (xpub).
158+
*/
159+
export function revertXpubPrefix(xpub: string, purpose: ExtendedKeyPurpose, isMainnet: boolean): string {
160+
// If the purpose is P2TR or ALL_OTHERS, the key is already in the standard format (xpub/tpub),
161+
// so we return it unmodified. This is the same bypass condition.
162+
if (purpose === PURPOSE_P2TR || purpose === PURPOSE_ALL_OTHERS) {
163+
return xpub;
164+
}
165+
166+
// 1. Decode the extended public key to get the raw bytes
167+
const data = bs58check.decode(xpub);
168+
169+
let versionBytes: Buffer;
170+
171+
// 2. Determine the standard prefix (xpub/tpub) based on the network
172+
switch (purpose) {
173+
case PURPOSE_WRAPPED_P2WKH:
174+
case PURPOSE_P2WKH:
175+
// All standard, non-SegWit-specific keys use the same prefix (xpub for mainnet, tpub for testnet)
176+
versionBytes = isMainnet
177+
? Buffer.from([0x04, 0x88, 0xb2, 0x1e]) // xpub
178+
: Buffer.from([0x04, 0x35, 0x87, 0xcf]); // tpub
179+
break;
180+
default:
181+
// This case should ideally not be hit if the input key is one of the converted types
182+
throw new Error('Unsupported purpose for reversal');
183+
}
184+
185+
// 3. Overwrite the existing prefix (ypub, zpub, upub, or vpub)
186+
// with the standard xpub or tpub prefix.
187+
versionBytes.copy(data, 0, 0, 4);
188+
189+
// 4. Encode the modified byte data back to a Base58Check string
190+
return bs58check.encode(data);
191+
}
192+
154193
/**
155194
* Derives watch-only accounts from the master HD node for the given purposes and network.
156195
*/

modules/abstract-lightning/src/lightning/parseWithdrawPsbt.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2-
import { Psbt } from '@bitgo/utxo-lib';
2+
import { isMainnet, Psbt } from '@bitgo/utxo-lib';
33
import { WatchOnlyAccount, WithdrawBaseOutputUTXO } from '../codecs';
44
import { LightningOnchainRecipient } from '@bitgo/public-types';
55
import { Bip32Derivation } from 'bip174/src/lib/interfaces';
6+
import {
7+
ExtendedKeyAddressPurpose,
8+
PURPOSE_P2TR,
9+
PURPOSE_P2WKH,
10+
PURPOSE_WRAPPED_P2WKH,
11+
revertXpubPrefix,
12+
} from './lightningUtils';
613

714
function parseDerivationPath(derivationPath: string): {
8-
purpose: number;
15+
purpose: ExtendedKeyAddressPurpose;
916
change: number;
1017
addressIndex: number;
1118
} {
1219
const pathSegments = derivationPath.split('/');
1320
const purpose = Number(pathSegments[1].replace(/'/g, ''));
1421
const change = Number(pathSegments[pathSegments.length - 2]);
1522
const addressIndex = Number(pathSegments[pathSegments.length - 1]);
23+
if (purpose !== PURPOSE_WRAPPED_P2WKH && purpose !== PURPOSE_P2WKH && purpose !== PURPOSE_P2TR) {
24+
throw new Error(`Unsupported purpose in derivation path: ${purpose}`);
25+
}
1626
return { purpose, change, addressIndex };
1727
}
1828

@@ -65,8 +75,10 @@ function verifyChangeAddress(
6575
throw new Error(`Account not found for purpose: ${purpose}`);
6676
}
6777

78+
// convert upub, vpub, etc prefixes to xpub as utxolib doesn't support these
79+
const convertedXpub = revertXpubPrefix(account.xpub, purpose, isMainnet(network));
6880
// Create a BIP32 node from the xpub
69-
const xpubNode = utxolib.bip32.fromBase58(account.xpub, network);
81+
const xpubNode = utxolib.bip32.fromBase58(convertedXpub, network);
7082

7183
// Derive the public key from the xpub using the change and address index
7284
const derivedPubkey = xpubNode.derive(change).derive(addressIndex).publicKey;

0 commit comments

Comments
 (0)