diff --git a/modules/abstract-utxo/src/names.ts b/modules/abstract-utxo/src/names.ts index d3a412dce5..3e35a6e08e 100644 --- a/modules/abstract-utxo/src/names.ts +++ b/modules/abstract-utxo/src/names.ts @@ -82,6 +82,19 @@ export function getChainFromNetwork(n: utxolib.Network): string { } } +/** + * @param coinName - the name of the coin (e.g. 'btc', 'bch', 'ltc'). Also called 'chain' in some contexts. + * @returns the network for a coin. This is the mainnet network for the coin. + */ +export function getNetworkFromChain(coinName: string): utxolib.Network { + for (const network of utxolib.getNetworkList()) { + if (getChainFromNetwork(network) === coinName) { + return network; + } + } + throw new Error(`Unknown chain ${coinName}`); +} + export function getFullNameFromNetwork(n: utxolib.Network): string { const name = getNetworkName(n); diff --git a/modules/abstract-utxo/src/offlineVault/OfflineVaultHalfSigned.ts b/modules/abstract-utxo/src/offlineVault/OfflineVaultHalfSigned.ts new file mode 100644 index 0000000000..23f6c61146 --- /dev/null +++ b/modules/abstract-utxo/src/offlineVault/OfflineVaultHalfSigned.ts @@ -0,0 +1,29 @@ +import * as utxolib from '@bitgo/utxo-lib'; +import { BIP32Interface } from '@bitgo/utxo-lib'; + +import { getNetworkFromChain } from '../names'; + +import { OfflineVaultUnsigned } from './OfflineVaultUnsigned'; +import { DescriptorTransaction, getHalfSignedPsbt } from './descriptor'; + +export type OfflineVaultHalfSigned = { + halfSigned: { txHex: string }; +}; + +function createHalfSignedFromPsbt(psbt: utxolib.Psbt): OfflineVaultHalfSigned { + return { halfSigned: { txHex: psbt.toHex() } }; +} + +export function createHalfSigned(coin: string, prv: string | BIP32Interface, tx: unknown): OfflineVaultHalfSigned { + const network = getNetworkFromChain(coin); + if (typeof prv === 'string') { + prv = utxolib.bip32.fromBase58(prv); + } + if (!OfflineVaultUnsigned.is(tx)) { + throw new Error('unsupported transaction type'); + } + if (DescriptorTransaction.is(tx)) { + return createHalfSignedFromPsbt(getHalfSignedPsbt(tx, prv, network)); + } + throw new Error('unsupported transaction type'); +} diff --git a/modules/abstract-utxo/src/offlineVault/OfflineVaultUnsigned.ts b/modules/abstract-utxo/src/offlineVault/OfflineVaultUnsigned.ts new file mode 100644 index 0000000000..48dd14c7b4 --- /dev/null +++ b/modules/abstract-utxo/src/offlineVault/OfflineVaultUnsigned.ts @@ -0,0 +1,35 @@ +import * as utxolib from '@bitgo/utxo-lib'; +import { Triple } from '@bitgo/sdk-core'; +import * as t from 'io-ts'; + +export const XPubWithDerivationPath = t.intersection( + [t.type({ xpub: t.string }), t.partial({ derivationPath: t.string })], + 'XPubWithDerivationPath' +); + +export type XPubWithDerivationPath = t.TypeOf; + +/** + * This is the transaction payload that is sent to the offline vault to sign. + */ +export const OfflineVaultUnsigned = t.type( + { + xpubsWithDerivationPath: t.type({ + user: XPubWithDerivationPath, + backup: XPubWithDerivationPath, + bitgo: XPubWithDerivationPath, + }), + coinSpecific: t.type({ txHex: t.string }), + }, + 'BaseTransaction' +); + +export type OfflineVaultUnsigned = t.TypeOf; + +type WithXpub = { xpub: string }; +type NamedKeys = { user: WithXpub; backup: WithXpub; bitgo: WithXpub }; +export function toKeyTriple(xpubs: NamedKeys): Triple { + return [xpubs.user.xpub, xpubs.backup.xpub, xpubs.bitgo.xpub].map((xpub) => + utxolib.bip32.fromBase58(xpub) + ) as Triple; +} diff --git a/modules/abstract-utxo/src/offlineVault/descriptor/index.ts b/modules/abstract-utxo/src/offlineVault/descriptor/index.ts new file mode 100644 index 0000000000..e8f45da2db --- /dev/null +++ b/modules/abstract-utxo/src/offlineVault/descriptor/index.ts @@ -0,0 +1 @@ +export * from './transaction'; diff --git a/modules/abstract-utxo/src/offlineVault/descriptor/transaction.ts b/modules/abstract-utxo/src/offlineVault/descriptor/transaction.ts new file mode 100644 index 0000000000..eeeb58a9d3 --- /dev/null +++ b/modules/abstract-utxo/src/offlineVault/descriptor/transaction.ts @@ -0,0 +1,43 @@ +import * as utxolib from '@bitgo/utxo-lib'; +import * as t from 'io-ts'; + +import { NamedDescriptor } from '../../descriptor'; +import { OfflineVaultUnsigned, toKeyTriple } from '../OfflineVaultUnsigned'; +import { + getValidatorOneOfTemplates, + getValidatorSignedByUserKey, + getValidatorSome, + toDescriptorMapValidate, +} from '../../descriptor/validatePolicy'; +import { DescriptorMap } from '../../core/descriptor'; +import { signPsbt } from '../../transaction/descriptor'; + +export const DescriptorTransaction = t.intersection( + [OfflineVaultUnsigned, t.type({ descriptors: t.array(NamedDescriptor) })], + 'DescriptorTransaction' +); + +export type DescriptorTransaction = t.TypeOf; + +export function getDescriptorsFromDescriptorTransaction(tx: DescriptorTransaction): DescriptorMap { + const { descriptors, xpubsWithDerivationPath } = tx; + const pubkeys = toKeyTriple(xpubsWithDerivationPath); + const policy = getValidatorSome([ + // allow all 2-of-3-ish descriptors where the keys match the wallet keys + getValidatorOneOfTemplates(['Wsh2Of3', 'Wsh2Of3CltvDrop', 'ShWsh2Of3CltvDrop']), + // allow all descriptors signed by the user key + getValidatorSignedByUserKey(), + ]); + return toDescriptorMapValidate(descriptors, pubkeys, policy); +} + +export function getHalfSignedPsbt( + tx: DescriptorTransaction, + prv: utxolib.BIP32Interface, + network: utxolib.Network +): utxolib.Psbt { + const psbt = utxolib.bitgo.createPsbtDecode(tx.coinSpecific.txHex, network); + const descriptorMap = getDescriptorsFromDescriptorTransaction(tx); + signPsbt(psbt, descriptorMap, prv, { onUnknownInput: 'throw' }); + return psbt; +} diff --git a/modules/abstract-utxo/src/offlineVault/index.ts b/modules/abstract-utxo/src/offlineVault/index.ts new file mode 100644 index 0000000000..e318aaa052 --- /dev/null +++ b/modules/abstract-utxo/src/offlineVault/index.ts @@ -0,0 +1,2 @@ +export * as descriptor from './descriptor'; +export * from './OfflineVaultHalfSigned'; diff --git a/modules/abstract-utxo/test/offlineVault/fixtures/Wsh2Of3.buildAdmin.custodial.json b/modules/abstract-utxo/test/offlineVault/fixtures/Wsh2Of3.buildAdmin.custodial.json new file mode 100644 index 0000000000..be1e098311 --- /dev/null +++ b/modules/abstract-utxo/test/offlineVault/fixtures/Wsh2Of3.buildAdmin.custodial.json @@ -0,0 +1,73 @@ +{ + "walletKeys": [ + "xprv9zXudaVgkhXsjTeXgpJ3K62R6bZDYQ5L5TVYfhGPLDDLNhfLtCYwHm4R4aMnMMeRpHSiM5Krxxbrux7iz99f7rSmZyddtBogiSch4PRVVXZ", + "xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b", + "xprv9s21ZrQH143K3Kh6W9VDkrpUSDrikEYKbbEKyB5Xn9bJeBPRSRSyWqQ5Fzoujj4eFmRKrxFPipYtfVqyu3aNYH4Lojrdhemi4aUdX8CjD8W" + ], + "response": { + "txBase64": "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", + "descriptors": [ + { + "name": "external", + "value": "wsh(multi(2,xpub6DXG362ab56Awwiznqq3gDy9edPhwroBSgR9U5fztYkKFVzVRjsBqZNtusyuT3U841NogK9ByRFHz6Zhb5jTGWkiJyu3SVBqKiPFzw2GcyV/0/*,xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b/0/*,xpub661MyMwAqRbcFomZcB2E7zmCzFhD9hGAxp9vmZV9LV8HWyiZyxmE4diZ7EREPnU5XdHjx1w9Xp2o7qtP3mGmWM6SWYD1zGcA2VuU6VEjomd/0/*))#gw7wwcku", + "signatures": [], + "lastIndex": 1 + }, + { + "name": "internal", + "value": "wsh(multi(2,xpub6DXG362ab56Awwiznqq3gDy9edPhwroBSgR9U5fztYkKFVzVRjsBqZNtusyuT3U841NogK9ByRFHz6Zhb5jTGWkiJyu3SVBqKiPFzw2GcyV/1/*,xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b/1/*,xpub661MyMwAqRbcFomZcB2E7zmCzFhD9hGAxp9vmZV9LV8HWyiZyxmE4diZ7EREPnU5XdHjx1w9Xp2o7qtP3mGmWM6SWYD1zGcA2VuU6VEjomd/1/*))#sw3k4yqr", + "signatures": [], + "lastIndex": 0 + } + ], + "formatVersion": 1, + "coin": "btc", + "pubs": [ + "xpub6DXG362ab56Awwiznqq3gDy9edPhwroBSgR9U5fztYkKFVzVRjsBqZNtusyuT3U841NogK9ByRFHz6Zhb5jTGWkiJyu3SVBqKiPFzw2GcyV", + "xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b", + "xpub661MyMwAqRbcFomZcB2E7zmCzFhD9hGAxp9vmZV9LV8HWyiZyxmE4diZ7EREPnU5XdHjx1w9Xp2o7qtP3mGmWM6SWYD1zGcA2VuU6VEjomd" + ], + "xpubsWithDerivationPath": { + "user": { + "xpub": "xpub6DXG362ab56Awwiznqq3gDy9edPhwroBSgR9U5fztYkKFVzVRjsBqZNtusyuT3U841NogK9ByRFHz6Zhb5jTGWkiJyu3SVBqKiPFzw2GcyV", + "derivedFromParentWithSeed": "143700591154482" + }, + "backup": { + "xpub": "xpub6CYwP1gQmPLwzD67x2nBoxiKquYs2CQExaqyQ3KbcntynZCoUGKErxXNg6Mft3x7r5c1xtHTp827ZRQY7dwUa8XYjQ5RCPUwtTPAe6bSv8b", + "derivedFromParentWithSeed": "210593312354420" + }, + "bitgo": { + "xpub": "xpub661MyMwAqRbcFomZcB2E7zmCzFhD9hGAxp9vmZV9LV8HWyiZyxmE4diZ7EREPnU5XdHjx1w9Xp2o7qtP3mGmWM6SWYD1zGcA2VuU6VEjomd" + } + }, + "walletId": "6788d3cf8eaf7aa1db27d4575218103c", + "walletLabel": "UtxoWalletClient", + "amount": "199995579", + "address": "bc1q9cp8uk0qpal0h4hzrp2gvjy5hmwune773pnp2qtfwap22ryrnuxsehzwgh", + "pendingApprovalId": "6788d3cf8eaf7aa1db27d4f563e0e411", + "creatorId": "6788d3ce8eaf7aa1db27d18bc5baa93d", + "creatorEmail": "testuser-1737020360683@example.com", + "createDate": "2025-01-16T09:39:27.667Z", + "enterpriseId": "6788d3ce8eaf7aa1db27d2a664daec54", + "enterpriseName": "Foo Enterprises", + "enterpriseFeatureFlags": [ + "enableMMI" + ], + "videoId": { + "approver": "", + "date": "", + "link": "", + "exception": "", + "waived": true + }, + "coinSpecific": { + "txHex": "70736274ff0100dd020000000201010101010101010101010101010101010101010101010101010101010101010000000000fdffffff01010101010101010101010101010101010101010101010101010101010101010100000000fdffffff0340420f00000000002200202e027e59e00f7efbd6e21854864894beddc9e7de88661501697742a50c839f0d80841e00000000002200202e027e59e00f7efbd6e21854864894beddc9e7de88661501697742a50c839f0dfbe9bd0b0000000022002072cba07a4f95a936089851622eea3b63232de777a4a627b72a3c4958f06f33a1000000000001012b00e1f505000000002200202e027e59e00f7efbd6e21854864894beddc9e7de88661501697742a50c839f0d010569522102f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e2102d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df21023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b53ae2206023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b0c59e62a710000000000000000220602d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df0c1508470b0000000000000000220602f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e0c2a1493e800000000000000000001012b00e1f505000000002200202e027e59e00f7efbd6e21854864894beddc9e7de88661501697742a50c839f0d010569522102f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e2102d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df21023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b53ae2206023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b0c59e62a710000000000000000220602d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df0c1508470b0000000000000000220602f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e0c2a1493e8000000000000000000010169522102f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e2102d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df21023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b53ae2202023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b0c59e62a710000000000000000220202d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df0c1508470b0000000000000000220202f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e0c2a1493e8000000000000000000010169522102f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e2102d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df21023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b53ae2202023bdb693c4bf735870b5cbe279bf3e86b11dfbbdf2fd542945bbcef8d96dd177b0c59e62a710000000000000000220202d212e44dac0b4586cab689c566963ecc994d300508939180cbb707e13a8c32df0c1508470b0000000000000000220202f70923b37875101b1d49b0849befef4b7bb9f90f3f571f7a3187f98eb45a061e0c2a1493e800000000000000000001016952210344a042f622eb71a19f80ef8bfb864f2068ed2d0643d165ba6ce674134d9cb9462103fd3ced6d7adb7b6efb9e07a8e60425f73fb882a6a2eae41bf303aa110c0e85712102b4b094b52276da9686a9a037964d9b637bba5c2b3f2f7b8521fba94851029a1653ae220202b4b094b52276da9686a9a037964d9b637bba5c2b3f2f7b8521fba94851029a160c59e62a71000000000100000022020344a042f622eb71a19f80ef8bfb864f2068ed2d0643d165ba6ce674134d9cb9460c2a1493e80000000001000000220203fd3ced6d7adb7b6efb9e07a8e60425f73fb882a6a2eae41bf303aa110c0e85710c1508470b000000000100000000", + "inputIds": [ + "0101010101010101010101010101010101010101010101010101010101010101:0", + "0101010101010101010101010101010101010101010101010101010101010101:1" + ] + }, + "recipientsInfo": [], + "keyDerivationPath": "143700591154482" + } +} diff --git a/modules/abstract-utxo/test/offlineVault/halfSigned.ts b/modules/abstract-utxo/test/offlineVault/halfSigned.ts new file mode 100644 index 0000000000..11644493d6 --- /dev/null +++ b/modules/abstract-utxo/test/offlineVault/halfSigned.ts @@ -0,0 +1,80 @@ +import * as fs from 'fs'; +import assert from 'assert'; +import crypto from 'crypto'; + +import * as t from 'io-ts'; +import * as utxolib from '@bitgo/utxo-lib'; + +import { createHalfSigned } from '../../src/offlineVault'; +import { DescriptorTransaction } from '../../src/offlineVault/descriptor'; + +function getFixturesNames(): string[] { + // I'm using sync here because mocha cannot do async setup + // eslint-disable-next-line no-sync + return fs.readdirSync(__dirname + '/fixtures').filter((f) => f.endsWith('.json')); +} + +const Fixture = t.type({ + walletKeys: t.array(t.string), + response: t.unknown, +}); + +type Fixture = t.TypeOf; + +async function readFixture(name: string): Promise { + const data = JSON.parse(await fs.promises.readFile(__dirname + '/fixtures/' + name, 'utf-8')); + if (!Fixture.is(data)) { + throw new Error(`Invalid fixture ${name}`); + } + return data; +} + +function withRotatedXpubs(tx: DescriptorTransaction): DescriptorTransaction { + const { user, backup, bitgo } = tx.xpubsWithDerivationPath; + return { + ...tx, + xpubsWithDerivationPath: { + user: bitgo, + backup: user, + bitgo: backup, + }, + }; +} + +function withRandomXpubs(tx: DescriptorTransaction) { + function randomXpub() { + const bytes = crypto.getRandomValues(new Uint8Array(32)); + return utxolib.bip32.fromSeed(Buffer.from(bytes)).neutered().toBase58(); + } + return { + ...tx, + xpubsWithDerivationPath: { + user: randomXpub(), + backup: randomXpub(), + bitgo: randomXpub(), + }, + }; +} + +function withoutDescriptors(tx: DescriptorTransaction): DescriptorTransaction { + return { + ...tx, + descriptors: [], + }; +} + +describe('OfflineVaultHalfSigned', function () { + for (const fixtureName of getFixturesNames()) { + it(`can sign fixture ${fixtureName}`, async function () { + const { walletKeys, response } = await readFixture(fixtureName); + const prv = utxolib.bip32.fromBase58(walletKeys[0]); + createHalfSigned('btc', prv, response); + + assert(DescriptorTransaction.is(response)); + const mutations = [withRotatedXpubs(response), withRandomXpubs(response), withoutDescriptors(response)]; + for (const mutation of mutations) { + assert.throws(() => createHalfSigned('btc', prv, mutation)); + } + }); + } +});