Skip to content

Commit

Permalink
Merge pull request #5533 from BitGo/BTC-1835-update-lightning-wallet
Browse files Browse the repository at this point in the history
feat(sdk-core): move lightning specific wallet functions
  • Loading branch information
saravanan7mani authored Feb 12, 2025
2 parents a3f98fe + e63129d commit d052b0c
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 240 deletions.
122 changes: 93 additions & 29 deletions modules/bitgo/test/v2/unit/lightning/lightningWallets.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as assert from 'assert';
import { TestBitGo } from '@bitgo/sdk-test';
import * as nock from 'nock';
import { BaseCoin, getLightningAuthKeychains, getLightningKeychain } from '@bitgo/sdk-core';
import { BaseCoin } from '@bitgo/sdk-core';

import { BitGo, common, GenerateLightningWalletOptions, Wallet, Wallets } from '../../../../src';

Expand Down Expand Up @@ -219,19 +219,19 @@ describe('Lightning wallets', function () {
};

it('should get lightning key', async function () {
const wallet = new Wallet(bitgo, basecoin, walletData);
const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2();

const keyNock = nock(bgUrl)
.get('/api/v2/' + coinName + '/key/abc')
.reply(200, userKeyData);

const key = await getLightningKeychain(wallet);
const key = await wallet.getLightningKeychain();
assert.deepStrictEqual(key, userKeyData);
keyNock.done();
});

it('should get lightning auth keys', async function () {
const wallet = new Wallet(bitgo, basecoin, walletData);
const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2();

const userAuthKeyNock = nock(bgUrl)
.get('/api/v2/' + coinName + '/key/def')
Expand All @@ -240,57 +240,41 @@ describe('Lightning wallets', function () {
.get('/api/v2/' + coinName + '/key/ghi')
.reply(200, nodeAuthKeyData);

const { userAuthKey, nodeAuthKey } = await getLightningAuthKeychains(wallet);
const { userAuthKey, nodeAuthKey } = await wallet.getLightningAuthKeychains();
assert.deepStrictEqual(userAuthKey, userAuthKeyData);
assert.deepStrictEqual(nodeAuthKey, nodeAuthKeyData);
userAuthKeyNock.done();
nodeAuthKeyNock.done();
});

it('should fail to get lightning key for invalid coin', async function () {
const wallet = new Wallet(bitgo, bitgo.coin('tltc'), walletData);
await assert.rejects(
async () => await getLightningKeychain(wallet),
/Error: Invalid coin to get lightning Keychain: ltc/
);
});

it('should fail to get lightning auth keys for invalid coin', async function () {
const wallet = new Wallet(bitgo, bitgo.coin('tltc'), walletData);
await assert.rejects(
async () => await getLightningAuthKeychains(wallet),
/Error: Invalid coin to get lightning auth keychains: ltc/
);
});

it('should fail to get lightning key for invalid number of keys', async function () {
const wallet = new Wallet(bitgo, basecoin, { ...walletData, keys: [] });
const wallet = new Wallet(bitgo, basecoin, { ...walletData, keys: [] }).lightningV2();
await assert.rejects(
async () => await getLightningKeychain(wallet),
async () => await wallet.getLightningKeychain(),
/Error: Invalid number of key in lightning wallet: 0/
);
});

it('should fail to get lightning auth keys for invalid number of keys', async function () {
const wallet = new Wallet(bitgo, basecoin, { ...walletData, coinSpecific: { keys: ['def'] } });
const wallet = new Wallet(bitgo, basecoin, { ...walletData, coinSpecific: { keys: ['def'] } }).lightningV2();
await assert.rejects(
async () => await getLightningAuthKeychains(wallet),
async () => await wallet.getLightningAuthKeychains(),
/Error: Invalid number of auth keys in lightning wallet: 1/
);
});

it('should fail to get lightning key for invalid response', async function () {
const wallet = new Wallet(bitgo, basecoin, walletData);
const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2();

nock(bgUrl)
.get('/api/v2/' + coinName + '/key/abc')
.reply(200, { ...userKeyData, source: 'backup' });

await assert.rejects(async () => await getLightningKeychain(wallet), /Error: Invalid user key/);
await assert.rejects(async () => await wallet.getLightningKeychain(), /Error: Invalid user key/);
});

it('should fail to get lightning auth keys for invalid response', async function () {
const wallet = new Wallet(bitgo, basecoin, walletData);
const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2();

nock(bgUrl)
.get('/api/v2/' + coinName + '/key/def')
Expand All @@ -301,9 +285,89 @@ describe('Lightning wallets', function () {
.reply(200, nodeAuthKeyData);

await assert.rejects(
async () => await getLightningAuthKeychains(wallet),
async () => await wallet.getLightningAuthKeychains(),
/Error: Invalid lightning auth key: def/
);
});
});

describe('Update lightning wallet coin specific', function () {
const walletData = {
id: 'fakeid',
coin: coinName,
keys: ['abc'],
coinSpecific: { keys: ['def', 'ghi'] },
};

const userAuthKey = {
id: 'def',
pub: 'xpub661MyMwAqRbcGYjYsnsDj1SHdiXynWEXNnfNgMSpokN54FKyMqbu7rWEfVNDs6uAJmz86UVFtq4sefhQpXZhSAzQcL9zrEPtiLNNZoeSxCG',
encryptedPrv:
'{"iv":"zYhhaNdW0wPfJEoBjZ4pvg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"tgAMua9jjhw=","ct":"HcrbxQvNlWG5tLMndYzdNCYa1l+1h7o+vSsweA0+q1le3tWt6jLUJSEjZN+JI8lTZ2KPFQgLulQQhsUa+ytUCBi0vSgjF7x7CprT7l2Cfjkew00XsEd7wnmtJUsrQk8m69Co7tIRA3oEgzrnYwy4qOM81lbNNyQ="}',
source: 'user',
coinSpecific: {
tlnbtc: {
purpose: 'userAuth',
},
},
};

const nodeAuthKey = {
id: 'ghi',
pub: 'xpub661MyMwAqRbcG9xnTnAnRbJPo3MAHyRtH4zeehN8exYk4VFz5buepUzebhix33BKhS5Eb4V3LEfW5pYiSR8qmaEnyrpeghhKY8JfzAsUDpq',
encryptedPrv:
'{"iv":"bH6eGbnl9x8PZECPrgvcng==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"o8yknV6nTI8=","ct":"nGyzAToIzYkQeIdcVafoWHtMx7+Fgj0YldCme3WA1yxJAA0QulZVhblMZN/7efCRIumA0NNmpH7dxH6n8cVlz/Z+RUgC2q9lgvZKUoJcYNTjWUfkmkJutXX2tr8yVxm+eC/hnRiyfVLZ2qPxctvDlBVBfgLuPyc="}',
source: 'user',
coinSpecific: {
tlnbtc: {
purpose: 'nodeAuth',
},
},
};

const watchOnlyAccounts = {
master_key_birthday_timestamp: 'dummy',
master_key_fingerprint: 'dummy',
accounts: [
{
xpub: 'upub5Eep7H5q39PzQZLVEYLBytDyBNeV74E8mQsyeL6UozFq9Y3MsZ52G7YGuqrJPgoyAqF7TBeJdnkrHrVrB5pkWkPJ9cJGAePMU6F1Gyw6aFH',
purpose: 49,
coin_type: 0,
account: 0,
},
{
xpub: 'vpub5ZU1PHGpQoDSHckYico4nsvwsD3mTh6UjqL5zyGWXZXzBjTYMNKot7t9eRPQY71hJcnNN9r1ss25g3xA9rmoJ5nWPg8jEWavrttnsVa1qw1',
purpose: 84,
coin_type: 0,
account: 0,
},
],
};

const params = {
encryptedSignerMacaroon: 'test encryptedSignerMacaroon',
encryptedSignerAdminMacaroon: 'test encryptedSignerAdminMacaroon',
signerIp: 'test signerIp',
encryptedSignerTlsKey: 'test encryptedSignerTlsKey',
signerTlsCert: 'test signerTlsCert',
watchOnlyAccounts,
};

it('should update wallet', async function () {
const wallet = new Wallet(bitgo, basecoin, walletData).lightningV2();

const userAuthKeyNock = nock(bgUrl)
.get('/api/v2/' + coinName + '/key/def')
.reply(200, userAuthKey);
const nodeAuthKeyNock = nock(bgUrl)
.get('/api/v2/' + coinName + '/key/ghi')
.reply(200, nodeAuthKey);
const wpWalletUpdateNock = nock(bgUrl).put(`/api/v2/tlnbtc/wallet/${walletData.id}`).reply(200);

await assert.doesNotReject(async () => await wallet.updateWalletCoinSpecific(params, 'password123'));
userAuthKeyNock.done();
nodeAuthKeyNock.done();
wpWalletUpdateNock.done();
});
});
});
24 changes: 14 additions & 10 deletions modules/express/src/lightning/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,19 @@ export const GetWalletStateResponse = t.type(

export type GetWalletStateResponse = t.TypeOf<typeof GetWalletStateResponse>;

export const InitLightningWalletRequest = t.strict(
{
walletId: t.string,
passphrase: t.string,
signerIP: IPAddress,
signerTlsCert: t.string,
signerTlsKey: t.string,
expressIP: IPAddress,
},
export const InitLightningWalletRequest = t.intersection(
[
t.strict({
walletId: t.string,
passphrase: t.string,
signerIp: IPAddress,
signerTlsCert: t.string,
expressIp: IPAddress,
}),
t.partial({
signerTlsKey: t.string,
}),
],
'InitLightningWalletRequest'
);

Expand All @@ -51,7 +55,7 @@ export const CreateSignerMacaroonRequest = t.strict(
{
walletId: t.string,
passphrase: t.string,
watchOnlyIP: IPAddress,
watchOnlyIp: IPAddress,
},
'CreateSignerMacaroonRequest'
);
Expand Down
68 changes: 23 additions & 45 deletions modules/express/src/lightning/lightningSignerRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import * as express from 'express';
import {
decodeOrElse,
createMessageSignature,
getUtxolibNetwork,
signerMacaroonPermissions,
createWatchOnly,
addIPCaveatToMacaroon,
getLightningAuthKeychains,
getLightningKeychain,
updateLightningWallet,
LightningWalletCoinSpecific,
isLightningCoinName,
deriveLightningServiceSharedSecret,
} from '@bitgo/sdk-core';
Expand All @@ -27,12 +22,12 @@ import { LndSignerClient } from './lndSignerClient';
type Decrypt = (params: { input: string; password: string }) => string;

async function createSignerMacaroon(
watchOnlyIP: string,
watchOnlyIp: string,
header: { adminMacaroonHex: string },
lndSignerClient: LndSignerClient
) {
const { macaroon } = await lndSignerClient.bakeMacaroon({ permissions: signerMacaroonPermissions }, header);
const macaroonBase64 = addIPCaveatToMacaroon(Buffer.from(macaroon, 'hex').toString('base64'), watchOnlyIP);
const macaroonBase64 = addIPCaveatToMacaroon(Buffer.from(macaroon, 'hex').toString('base64'), watchOnlyIp);
return Buffer.from(macaroonBase64, 'base64').toString('hex');
}

Expand All @@ -58,7 +53,7 @@ function getMacaroonRootKey(passphrase: string, nodeAuthEncryptedPrv: string, de
* Handle the request to initialise remote signer LND for a wallet.
*/
export async function handleInitLightningWallet(req: express.Request): Promise<unknown> {
const { walletId, passphrase, signerTlsKey, signerTlsCert, signerIP, expressIP } = decodeOrElse(
const { walletId, passphrase, signerTlsKey, signerTlsCert, signerIp, expressIp } = decodeOrElse(
InitLightningWalletRequest.name,
InitLightningWalletRequest,
req.body,
Expand All @@ -77,10 +72,10 @@ export async function handleInitLightningWallet(req: express.Request): Promise<u
}
const coin = bitgo.coin(coinName);

const wallet = await coin.wallets().get({ id: walletId });
const lightningWallet = (await coin.wallets().get({ id: walletId })).lightningV2();

const userKey = await getLightningKeychain(wallet);
const { userAuthKey, nodeAuthKey } = await getLightningAuthKeychains(wallet);
const userKey = await lightningWallet.getLightningKeychain();
const { nodeAuthKey } = await lightningWallet.getLightningAuthKeychains();

const network = getUtxolibNetwork(coin.getChain());
const signerRootKey = getSignerRootKey(passphrase, userKey.encryptedPrv, network, bitgo.decrypt);
Expand All @@ -94,38 +89,28 @@ export async function handleInitLightningWallet(req: express.Request): Promise<u

const encryptedSignerAdminMacaroon = bitgo.encrypt({
password: passphrase,
input: addIPCaveatToMacaroon(adminMacaroon, expressIP),
input: addIPCaveatToMacaroon(adminMacaroon, expressIp),
});
const encryptedSignerTlsKey = bitgo.encrypt({ password: passphrase, input: signerTlsKey });
const watchOnly = createWatchOnly(signerRootKey, network);
const watchOnlyAccounts = createWatchOnly(signerRootKey, network);
const encryptedSignerTlsKey = signerTlsKey ? bitgo.encrypt({ password: passphrase, input: signerTlsKey }) : undefined;

const coinSpecific = {
[coin.getChain()]: {
return await lightningWallet.updateWalletCoinSpecific(
{
encryptedSignerAdminMacaroon,
signerIP,
signerIp,
signerTlsCert,
encryptedSignerTlsKey,
watchOnly,
watchOnlyAccounts,
...(encryptedSignerTlsKey && { encryptedSignerTlsKey }),
},
};

if (!LightningWalletCoinSpecific.is(coinSpecific)) {
throw new Error('Invalid lightning wallet coin specific data');
}

const signature = createMessageSignature(
coinSpecific,
bitgo.decrypt({ password: passphrase, input: userAuthKey.encryptedPrv })
passphrase
);

return await updateLightningWallet(wallet, { coinSpecific, signature });
}

/**
* Handle the request to create a signer macaroon from remote signer LND for a wallet.
*/
export async function handleCreateSignerMacaroon(req: express.Request): Promise<unknown> {
const { walletId, passphrase, watchOnlyIP } = decodeOrElse(
const { walletId, passphrase, watchOnlyIp } = decodeOrElse(
CreateSignerMacaroonRequest.name,
CreateSignerMacaroonRequest,
req.body,
Expand All @@ -145,17 +130,18 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise<
const coin = bitgo.coin(coinName);

const wallet = await coin.wallets().get({ id: walletId });
const lightningWallet = wallet.lightningV2();

const encryptedSignerAdminMacaroon = wallet.coinSpecific()?.encryptedSignerAdminMacaroon;
if (!encryptedSignerAdminMacaroon) {
throw new Error('Missing encryptedSignerAdminMacaroon in wallet');
}
const adminMacaroon = bitgo.decrypt({ password: passphrase, input: encryptedSignerAdminMacaroon });

const { userAuthKey } = await getLightningAuthKeychains(wallet);
const { userAuthKey } = await lightningWallet.getLightningAuthKeychains();

const signerMacaroon = await createSignerMacaroon(
watchOnlyIP,
watchOnlyIp,
{ adminMacaroonHex: Buffer.from(adminMacaroon, 'base64').toString('hex') },
lndSignerClient
);
Expand All @@ -166,20 +152,12 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise<
password: deriveLightningServiceSharedSecret(coinName, userAuthXprv).toString('hex'),
input: signerMacaroon,
});

const coinSpecific = {
[coin.getChain()]: {
return await lightningWallet.updateWalletCoinSpecific(
{
encryptedSignerMacaroon,
},
};

if (!LightningWalletCoinSpecific.is(coinSpecific)) {
throw new Error('Invalid lightning wallet coin specific data');
}

const signature = createMessageSignature(coinSpecific, userAuthXprv);

return await updateLightningWallet(wallet, { coinSpecific, signature });
passphrase
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ export const apiData = {
initWalletRequestBody: {
walletId: 'fakeid',
passphrase: 'password123',
expressIP: '127.0.0.1',
signerIP: '127.0.0.1',
expressIp: '127.0.0.1',
signerIp: '127.0.0.1',
signerTlsCert:
'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNQRENDQWVLZ0F3SUJBZ0lSQU02TEFoaGxOMGo4ZlhxV2dLTWdENmN3Q2dZSUtvWkl6ajBFQXdJd09ERWYKTUIwR0ExVUVDaE1XYkc1a0lHRjFkRzluWlc1bGNtRjBaV1FnWTJWeWRERVZNQk1HQTFVRUF4TU1aV1UxTVdZeApOREV4TUdVMk1CNFhEVEkwTURneE9ERXlNVE14TWxvWERUSTFNVEF4TXpFeU1UTXhNbG93T0RFZk1CMEdBMVVFCkNoTVdiRzVrSUdGMWRHOW5aVzVsY21GMFpXUWdZMlZ5ZERFVk1CTUdBMVVFQXhNTVpXVTFNV1l4TkRFeE1HVTIKTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFclA0d2NXWFEwUWFFazhsVFNVTXBCa1d3ditFbQpxNTNyOWVSeVJUOTRkZGdVR0tTMFlRK0liZzFseVBRU3hiN0dXYloyWG9GUFdiK1VOM0lFMVlMQ2thT0J6RENCCnlUQU9CZ05WSFE4QkFmOEVCQU1DQXFRd0V3WURWUjBsQkF3d0NnWUlLd1lCQlFVSEF3RXdEd1lEVlIwVEFRSC8KQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVb3JmUkNVQytmaUNjZlE4cEhEUTFWaE1uMXBBd2NnWURWUjBSQkdzdwphWUlNWldVMU1XWXhOREV4TUdVMmdnbHNiMk5oYkdodmMzU0NDbk5wWjI1bGNtNXZaR1dDQ1d4dlkyRnNhRzl6CmRJSUVkVzVwZUlJS2RXNXBlSEJoWTJ0bGRJSUhZblZtWTI5dWJvY0Vmd0FBQVljUUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFZY0VyQlFBQWpBS0JnZ3Foa2pPUFFRREFnTklBREJGQWlFQXJuQ0xRTlgzeDZ1NjhIM2xCOG9wOUFKaApBd2RrUjhXOXNSaUZnZDJKM2tZQ0lHczFOVGM0T0toRzByNzVHUWpXb2x0SkJyOUtjWWVyR1V3aklCaCtvZ1h0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K',
signerTlsKey:
Expand All @@ -12,7 +12,7 @@ export const apiData = {
signerMacaroonRequestBody: {
walletId: 'fakeid',
passphrase: 'password123',
watchOnlyIP: '127.0.0.1',
watchOnlyIp: '127.0.0.1',
},
unlockWalletRequestBody: {
walletId: 'fakeid',
Expand Down
Loading

0 comments on commit d052b0c

Please sign in to comment.