Skip to content

Commit 49ae008

Browse files
Merge pull request #5381 from BitGo/BTC-1731.abstract-utxo-offline-vault
feat(abstract-utxo): add descriptor signing support for offline vault
2 parents ad33065 + 7082893 commit 49ae008

File tree

8 files changed

+276
-0
lines changed

8 files changed

+276
-0
lines changed

modules/abstract-utxo/src/names.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ export function getChainFromNetwork(n: utxolib.Network): string {
8282
}
8383
}
8484

85+
/**
86+
* @param coinName - the name of the coin (e.g. 'btc', 'bch', 'ltc'). Also called 'chain' in some contexts.
87+
* @returns the network for a coin. This is the mainnet network for the coin.
88+
*/
89+
export function getNetworkFromChain(coinName: string): utxolib.Network {
90+
for (const network of utxolib.getNetworkList()) {
91+
if (getChainFromNetwork(network) === coinName) {
92+
return network;
93+
}
94+
}
95+
throw new Error(`Unknown chain ${coinName}`);
96+
}
97+
8598
export function getFullNameFromNetwork(n: utxolib.Network): string {
8699
const name = getNetworkName(n);
87100

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { BIP32Interface } from '@bitgo/utxo-lib';
3+
4+
import { getNetworkFromChain } from '../names';
5+
6+
import { OfflineVaultUnsigned } from './OfflineVaultUnsigned';
7+
import { DescriptorTransaction, getHalfSignedPsbt } from './descriptor';
8+
9+
export type OfflineVaultHalfSigned = {
10+
halfSigned: { txHex: string };
11+
};
12+
13+
function createHalfSignedFromPsbt(psbt: utxolib.Psbt): OfflineVaultHalfSigned {
14+
return { halfSigned: { txHex: psbt.toHex() } };
15+
}
16+
17+
export function createHalfSigned(coin: string, prv: string | BIP32Interface, tx: unknown): OfflineVaultHalfSigned {
18+
const network = getNetworkFromChain(coin);
19+
if (typeof prv === 'string') {
20+
prv = utxolib.bip32.fromBase58(prv);
21+
}
22+
if (!OfflineVaultUnsigned.is(tx)) {
23+
throw new Error('unsupported transaction type');
24+
}
25+
if (DescriptorTransaction.is(tx)) {
26+
return createHalfSignedFromPsbt(getHalfSignedPsbt(tx, prv, network));
27+
}
28+
throw new Error('unsupported transaction type');
29+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import { Triple } from '@bitgo/sdk-core';
3+
import * as t from 'io-ts';
4+
5+
export const XPubWithDerivationPath = t.intersection(
6+
[t.type({ xpub: t.string }), t.partial({ derivationPath: t.string })],
7+
'XPubWithDerivationPath'
8+
);
9+
10+
export type XPubWithDerivationPath = t.TypeOf<typeof XPubWithDerivationPath>;
11+
12+
/**
13+
* This is the transaction payload that is sent to the offline vault to sign.
14+
*/
15+
export const OfflineVaultUnsigned = t.type(
16+
{
17+
xpubsWithDerivationPath: t.type({
18+
user: XPubWithDerivationPath,
19+
backup: XPubWithDerivationPath,
20+
bitgo: XPubWithDerivationPath,
21+
}),
22+
coinSpecific: t.type({ txHex: t.string }),
23+
},
24+
'BaseTransaction'
25+
);
26+
27+
export type OfflineVaultUnsigned = t.TypeOf<typeof OfflineVaultUnsigned>;
28+
29+
type WithXpub = { xpub: string };
30+
type NamedKeys = { user: WithXpub; backup: WithXpub; bitgo: WithXpub };
31+
export function toKeyTriple(xpubs: NamedKeys): Triple<utxolib.BIP32Interface> {
32+
return [xpubs.user.xpub, xpubs.backup.xpub, xpubs.bitgo.xpub].map((xpub) =>
33+
utxolib.bip32.fromBase58(xpub)
34+
) as Triple<utxolib.BIP32Interface>;
35+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './transaction';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
import * as t from 'io-ts';
3+
4+
import { NamedDescriptor } from '../../descriptor';
5+
import { OfflineVaultUnsigned, toKeyTriple } from '../OfflineVaultUnsigned';
6+
import {
7+
getValidatorOneOfTemplates,
8+
getValidatorSignedByUserKey,
9+
getValidatorSome,
10+
toDescriptorMapValidate,
11+
} from '../../descriptor/validatePolicy';
12+
import { DescriptorMap } from '../../core/descriptor';
13+
import { signPsbt } from '../../transaction/descriptor';
14+
15+
export const DescriptorTransaction = t.intersection(
16+
[OfflineVaultUnsigned, t.type({ descriptors: t.array(NamedDescriptor) })],
17+
'DescriptorTransaction'
18+
);
19+
20+
export type DescriptorTransaction = t.TypeOf<typeof DescriptorTransaction>;
21+
22+
export function getDescriptorsFromDescriptorTransaction(tx: DescriptorTransaction): DescriptorMap {
23+
const { descriptors, xpubsWithDerivationPath } = tx;
24+
const pubkeys = toKeyTriple(xpubsWithDerivationPath);
25+
const policy = getValidatorSome([
26+
// allow all 2-of-3-ish descriptors where the keys match the wallet keys
27+
getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']),
28+
// allow all descriptors signed by the user key
29+
getValidatorSignedByUserKey(),
30+
]);
31+
return toDescriptorMapValidate(descriptors, pubkeys, policy);
32+
}
33+
34+
export function getHalfSignedPsbt(
35+
tx: DescriptorTransaction,
36+
prv: utxolib.BIP32Interface,
37+
network: utxolib.Network
38+
): utxolib.Psbt {
39+
const psbt = utxolib.bitgo.createPsbtDecode(tx.coinSpecific.txHex, network);
40+
const descriptorMap = getDescriptorsFromDescriptorTransaction(tx);
41+
signPsbt(psbt, descriptorMap, prv, { onUnknownInput: 'throw' });
42+
return psbt;
43+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * as descriptor from './descriptor';
2+
export * from './OfflineVaultHalfSigned';
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"walletKeys": [
3+
"xprv9zXudaVgkhXsjTeXgpJ3K62R6bZDYQ5L5TVYfhGPLDDLNhfLtCYwHm4R4aMnMMeRpHSiM5Krxxbrux7iz99f7rSmZyddtBogiSch4PRVVXZ",
4+
"xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b",
5+
"xprv9s21ZrQH143K3Kh6W9VDkrpUSDrikEYKbbEKyB5Xn9bJeBPRSRSyWqQ5Fzoujj4eFmRKrxFPipYtfVqyu3aNYH4Lojrdhemi4aUdX8CjD8W"
6+
],
7+
"response": {
8+
"txBase64": "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",
9+
"descriptors": [
10+
{
11+
"name": "external",
12+
"value": "wsh(multi(2,xpub6DXG362ab56Awwiznqq3gDy9edPhwroBSgR9U5fztYkKFVzVRjsBqZNtusyuT3U841NogK9ByRFHz6Zhb5jTGWkiJyu3SVBqKiPFzw2GcyV/0/*,xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b/0/*,xpub661MyMwAqRbcFomZcB2E7zmCzFhD9hGAxp9vmZV9LV8HWyiZyxmE4diZ7EREPnU5XdHjx1w9Xp2o7qtP3mGmWM6SWYD1zGcA2VuU6VEjomd/0/*))#gw7wwcku",
13+
"signatures": [],
14+
"lastIndex": 1
15+
},
16+
{
17+
"name": "internal",
18+
"value": "wsh(multi(2,xpub6DXG362ab56Awwiznqq3gDy9edPhwroBSgR9U5fztYkKFVzVRjsBqZNtusyuT3U841NogK9ByRFHz6Zhb5jTGWkiJyu3SVBqKiPFzw2GcyV/1/*,xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b/1/*,xpub661MyMwAqRbcFomZcB2E7zmCzFhD9hGAxp9vmZV9LV8HWyiZyxmE4diZ7EREPnU5XdHjx1w9Xp2o7qtP3mGmWM6SWYD1zGcA2VuU6VEjomd/1/*))#sw3k4yqr",
19+
"signatures": [],
20+
"lastIndex": 0
21+
}
22+
],
23+
"formatVersion": 1,
24+
"coin": "btc",
25+
"pubs": [
26+
"xpub6DXG362ab56Awwiznqq3gDy9edPhwroBSgR9U5fztYkKFVzVRjsBqZNtusyuT3U841NogK9ByRFHz6Zhb5jTGWkiJyu3SVBqKiPFzw2GcyV",
27+
"xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b",
28+
"xpub661MyMwAqRbcFomZcB2E7zmCzFhD9hGAxp9vmZV9LV8HWyiZyxmE4diZ7EREPnU5XdHjx1w9Xp2o7qtP3mGmWM6SWYD1zGcA2VuU6VEjomd"
29+
],
30+
"xpubsWithDerivationPath": {
31+
"user": {
32+
"xpub": "xpub6DXG362ab56Awwiznqq3gDy9edPhwroBSgR9U5fztYkKFVzVRjsBqZNtusyuT3U841NogK9ByRFHz6Zhb5jTGWkiJyu3SVBqKiPFzw2GcyV",
33+
"derivedFromParentWithSeed": "143700591154482"
34+
},
35+
"backup": {
36+
"xpub": "xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b",
37+
"derivedFromParentWithSeed": "210593312354420"
38+
},
39+
"bitgo": {
40+
"xpub": "xpub661MyMwAqRbcFomZcB2E7zmCzFhD9hGAxp9vmZV9LV8HWyiZyxmE4diZ7EREPnU5XdHjx1w9Xp2o7qtP3mGmWM6SWYD1zGcA2VuU6VEjomd"
41+
}
42+
},
43+
"walletId": "6788d3cf8eaf7aa1db27d4575218103c",
44+
"walletLabel": "UtxoWalletClient",
45+
"amount": "199995579",
46+
"address": "bc1q9cp8uk0qpal0h4hzrp2gvjy5hmwune773pnp2qtfwap22ryrnuxsehzwgh",
47+
"pendingApprovalId": "6788d3cf8eaf7aa1db27d4f563e0e411",
48+
"creatorId": "6788d3ce8eaf7aa1db27d18bc5baa93d",
49+
"creatorEmail": "[email protected]",
50+
"createDate": "2025-01-16T09:39:27.667Z",
51+
"enterpriseId": "6788d3ce8eaf7aa1db27d2a664daec54",
52+
"enterpriseName": "Foo Enterprises",
53+
"enterpriseFeatureFlags": [
54+
"enableMMI"
55+
],
56+
"videoId": {
57+
"approver": "",
58+
"date": "",
59+
"link": "",
60+
"exception": "",
61+
"waived": true
62+
},
63+
"coinSpecific": {
64+
"txHex": "70736274ff0100dd020000000201010101010101010101010101010101010101010101010101010101010101010000000000fdffffff01010101010101010101010101010101010101010101010101010101010101010100000000fdffffff0340420f00000000002200202e027e59e00f7efbd6e21854864894beddc9e7de88661501697742a50c839f0d80841e00000000002200202e027e59e00f7efbd6e21854864894beddc9e7de88661501697742a50c839f0dfbe9bd0b0000000022002072cba07a4f95a936089851622eea3b63232de777a4a627b72a3c4958f06f33a1000000000001012b00e1f505000000002200202e027e59e00f7efbd6e21854864894beddc9e7de88661501697742a50c839f0d010569522102f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e2102d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df21023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b53ae2206023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b0c59e62a710000000000000000220602d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df0c1508470b0000000000000000220602f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e0c2a1493e800000000000000000001012b00e1f505000000002200202e027e59e00f7efbd6e21854864894beddc9e7de88661501697742a50c839f0d010569522102f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e2102d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df21023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b53ae2206023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b0c59e62a710000000000000000220602d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df0c1508470b0000000000000000220602f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e0c2a1493e8000000000000000000010169522102f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e2102d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df21023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b53ae2202023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b0c59e62a710000000000000000220202d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df0c1508470b0000000000000000220202f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e0c2a1493e8000000000000000000010169522102f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e2102d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df21023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b53ae2202023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b0c59e62a710000000000000000220202d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df0c1508470b0000000000000000220202f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e0c2a1493e800000000000000000001016952210344a042f622eb71a19f80ef8bfb864f2068ed2d0643d165ba6ce674134d9cb9462103fd3ced6d7adb7b6efb9e07a8e60425f73fb882a6a2eae41bf303aa110c0e85712102b4b094b52276da9686a9a037964d9b637bba5c2b3f2f7b8521fba94851029a1653ae220202b4b094b52276da9686a9a037964d9b637bba5c2b3f2f7b8521fba94851029a160c59e62a71000000000100000022020344a042f622eb71a19f80ef8bfb864f2068ed2d0643d165ba6ce674134d9cb9460c2a1493e80000000001000000220203fd3ced6d7adb7b6efb9e07a8e60425f73fb882a6a2eae41bf303aa110c0e85710c1508470b000000000100000000",
65+
"inputIds": [
66+
"0101010101010101010101010101010101010101010101010101010101010101:0",
67+
"0101010101010101010101010101010101010101010101010101010101010101:1"
68+
]
69+
},
70+
"recipientsInfo": [],
71+
"keyDerivationPath": "143700591154482"
72+
}
73+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as fs from 'fs';
2+
import assert from 'assert';
3+
import crypto from 'crypto';
4+
5+
import * as t from 'io-ts';
6+
import * as utxolib from '@bitgo/utxo-lib';
7+
8+
import { createHalfSigned } from '../../src/offlineVault';
9+
import { DescriptorTransaction } from '../../src/offlineVault/descriptor';
10+
11+
function getFixturesNames(): string[] {
12+
// I'm using sync here because mocha cannot do async setup
13+
// eslint-disable-next-line no-sync
14+
return fs.readdirSync(__dirname + '/fixtures').filter((f) => f.endsWith('.json'));
15+
}
16+
17+
const Fixture = t.type({
18+
walletKeys: t.array(t.string),
19+
response: t.unknown,
20+
});
21+
22+
type Fixture = t.TypeOf<typeof Fixture>;
23+
24+
async function readFixture(name: string): Promise<Fixture> {
25+
const data = JSON.parse(await fs.promises.readFile(__dirname + '/fixtures/' + name, 'utf-8'));
26+
if (!Fixture.is(data)) {
27+
throw new Error(`Invalid fixture ${name}`);
28+
}
29+
return data;
30+
}
31+
32+
function withRotatedXpubs(tx: DescriptorTransaction): DescriptorTransaction {
33+
const { user, backup, bitgo } = tx.xpubsWithDerivationPath;
34+
return {
35+
...tx,
36+
xpubsWithDerivationPath: {
37+
user: bitgo,
38+
backup: user,
39+
bitgo: backup,
40+
},
41+
};
42+
}
43+
44+
function withRandomXpubs(tx: DescriptorTransaction) {
45+
function randomXpub() {
46+
const bytes = crypto.getRandomValues(new Uint8Array(32));
47+
return utxolib.bip32.fromSeed(Buffer.from(bytes)).neutered().toBase58();
48+
}
49+
return {
50+
...tx,
51+
xpubsWithDerivationPath: {
52+
user: randomXpub(),
53+
backup: randomXpub(),
54+
bitgo: randomXpub(),
55+
},
56+
};
57+
}
58+
59+
function withoutDescriptors(tx: DescriptorTransaction): DescriptorTransaction {
60+
return {
61+
...tx,
62+
descriptors: [],
63+
};
64+
}
65+
66+
describe('OfflineVaultHalfSigned', function () {
67+
for (const fixtureName of getFixturesNames()) {
68+
it(`can sign fixture ${fixtureName}`, async function () {
69+
const { walletKeys, response } = await readFixture(fixtureName);
70+
const prv = utxolib.bip32.fromBase58(walletKeys[0]);
71+
createHalfSigned('btc', prv, response);
72+
73+
assert(DescriptorTransaction.is(response));
74+
const mutations = [withRotatedXpubs(response), withRandomXpubs(response), withoutDescriptors(response)];
75+
for (const mutation of mutations) {
76+
assert.throws(() => createHalfSigned('btc', prv, mutation));
77+
}
78+
});
79+
}
80+
});

0 commit comments

Comments
 (0)