From 4eb1bfd11f6a5d54fcb9ad592d6eda9fc0f8c811 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Tue, 7 May 2024 17:04:04 +0200 Subject: [PATCH] feat: add fully working funding and closing transaction signing flow --- src/bitcoin-functions.ts | 364 +++++++++++++++++++++++++++++++++------ src/index.ts | 1 - src/ledger_test.ts | 215 +++++++++++++++++++---- 3 files changed, 484 insertions(+), 96 deletions(-) diff --git a/src/bitcoin-functions.ts b/src/bitcoin-functions.ts index 2c7d1a8..c177bc5 100644 --- a/src/bitcoin-functions.ts +++ b/src/bitcoin-functions.ts @@ -2,13 +2,16 @@ import { hexToBytes } from '@noble/hashes/utils'; import { hex } from '@scure/base'; -import { selectUTXO } from '@scure/btc-signer'; -import { P2TROut, p2tr, p2tr_ns, p2wpkh } from '@scure/btc-signer/payment'; +import { Transaction, selectUTXO } from '@scure/btc-signer'; +import { Address, OutScript, P2Ret, P2TROut, p2ms, p2pk, p2tr, p2tr_ns, p2wpkh } from '@scure/btc-signer/payment'; import { taprootTweakPubkey } from '@scure/btc-signer/utils'; import { Network, Psbt } from 'bitcoinjs-lib'; import { getNativeSegwitMultisigScript } from './payment-functions.js'; import { bitcoinToSats } from './utilities.js'; +import { TransactionInput } from '@scure/btc-signer/psbt'; +import { bitcoin, testnet } from 'bitcoinjs-lib/src/networks.js'; +import AppClient, { DefaultWalletPolicy } from 'ledger-bitcoin'; interface TransactionStatus { confirmed: boolean; @@ -24,47 +27,69 @@ interface UTXO { value: number; } +export interface BitcoinInputSigningConfig { + derivationPath: string; + index: number; +} + +export type PaymentTypes = 'p2pkh' | 'p2sh' | 'p2wpkh-p2sh' | 'p2wpkh' | 'p2tr'; + +/** + * This class represents a partial signature produced by the app during signing. + * It always contains the `signature` and the corresponding `pubkey` whose private key + * was used for signing; in the case of taproot script paths, it also contains the + * tapleaf hash. + */ +export declare class PartialSignature { + readonly pubkey: Buffer; + readonly signature: Buffer; + readonly tapleafHash?: Buffer; + constructor(pubkey: Buffer, signature: Buffer, tapleafHash?: Buffer); +} + +const TAPROOT_UNSPENDABLE_KEY_HEX = '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; + /** * Gets the UTXOs of the User's Native Segwit Address. * - * @param bitcoinNativeSegwitAddress - The User's Native Segwit Address. + * @param bitcoinNativeSegwitTransaction - * @param bitcoinNetwork - The Bitcoin Network to use. * @returns A Promise that resolves to the UTXOs of the User's Native Segwit Address. */ -export async function getUTXOs( - bitcoinNativeSegwitAddress: string, - publicKeys: string[], - bitcoinNetwork: Network -): Promise { +export async function getUTXOs(bitcoinNativeSegwitTransaction: P2Ret, bitcoinNetwork: Network): Promise { const bitcoinBlockchainAPIURL = process.env.BITCOIN_BLOCKCHAIN_API_URL; - try { - const response = await fetch(`${bitcoinBlockchainAPIURL}/address/${bitcoinNativeSegwitAddress}/utxo`); - - if (!response.ok) { - throw new Error(`Error getting UTXOs: ${response.statusText}`); - } + const utxoResponse = await fetch(`${bitcoinBlockchainAPIURL}/address/${bitcoinNativeSegwitTransaction.address}/utxo`); - const allUTXOs = await response.json(); + if (!utxoResponse.ok) { + throw new Error(`Error getting UTXOs: ${utxoResponse.statusText}`); + } - const spend = getNativeSegwitMultisigScript(publicKeys, bitcoinNetwork); + const userUTXOs = await utxoResponse.json(); + + const modifiedUTXOs = await Promise.all( + userUTXOs.map(async (utxo: UTXO) => { + return { + ...bitcoinNativeSegwitTransaction, + txid: utxo.txid, + index: utxo.vout, + value: utxo.value, + witnessUtxo: { + script: bitcoinNativeSegwitTransaction.script, + amount: BigInt(utxo.value), + }, + redeemScript: bitcoinNativeSegwitTransaction.redeemScript, + }; + }) + ); + return modifiedUTXOs; +} - const utxos = await Promise.all( - allUTXOs.map(async (utxo: UTXO) => { - const txHex = await (await fetch(`${bitcoinBlockchainAPIURL}/tx/${utxo.txid}/hex`)).text(); - return { - ...spend, - txid: utxo.txid, - index: utxo.vout, - value: utxo.value, - nonWitnessUtxo: hex.decode(txHex), - }; - }) - ); - return utxos; - } catch (error) { - throw new Error(`Error getting UTXOs: ${error}`); - } +function getFeeRecipientAddressFromPublicKey(feePublicKey: string, bitcoinNetwork: Network): string { + const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); + const { address } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); + if (!address) throw new Error('Could not create Fee Address'); + return address; } /** @@ -81,14 +106,20 @@ export function createMultisigTransaction( userPublicKey: Uint8Array, attestorGroupPublicKey: Uint8Array, vaultUUID: string, - bitcoinNetwork: Network + bitcoinNetwork: Network, + unspendablePublicKey?: Uint8Array ): P2TROut { const multisig = p2tr_ns(2, [userPublicKey, attestorGroupPublicKey]); - const TAPROOT_UNSPENDABLE_KEY_STR = '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; - const TAPROOT_UNSPENDABLE_KEY = hexToBytes(TAPROOT_UNSPENDABLE_KEY_STR); + if (unspendablePublicKey) { + const multisigTransaction = p2tr(unspendablePublicKey, multisig, bitcoinNetwork); + multisigTransaction.tapInternalKey = unspendablePublicKey; + return multisigTransaction; + } + + const unspendablePublicKeyBytes = hexToBytes(TAPROOT_UNSPENDABLE_KEY_HEX); - const tweakedUnspendableWithUUID = taprootTweakPubkey(TAPROOT_UNSPENDABLE_KEY, Buffer.from(vaultUUID))[0]; + const tweakedUnspendableWithUUID = taprootTweakPubkey(unspendablePublicKeyBytes, Buffer.from(vaultUUID))[0]; const multisigTransaction = p2tr(tweakedUnspendableWithUUID, multisig, bitcoinNetwork); multisigTransaction.tapInternalKey = tweakedUnspendableWithUUID; @@ -101,8 +132,7 @@ export function createMultisigTransaction( * @param bitcoinAmount - The amount of Bitcoin to fund the Transaction with. * @param bitcoinNetwork - The Bitcoin Network to use. * @param multisigAddress - The Multisig Address. - * @param utxos - The UTXOs to use for the Transaction. - * @param userChangeAddress - The user's Change Address. + * @param bitcoinNativeSegwitTransaction - The user's Native Segwit Transaction. * @param feeRate - The Fee Rate to use for the Transaction. * @param feePublicKey - The Fee Recipient's Public Key. * @param feeBasisPoints - The Fee Basis Points. @@ -112,29 +142,28 @@ export async function createFundingTransaction( bitcoinAmount: number, bitcoinNetwork: Network, multisigAddress: string, - nativeSegwitAddress: string, - nativeSegwitPublicKeys: string[], + bitcoinNativeSegwitTransaction: P2Ret, feeRate: bigint, feePublicKey: string, feeBasisPoints: number ): Promise { - const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); - const { address: feeAddress } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); + const feeAddress = getFeeRecipientAddressFromPublicKey(feePublicKey, bitcoinNetwork); + const feeRecipientOutputValue = BigInt(bitcoinToSats(bitcoinAmount) * feeBasisPoints); - if (!feeAddress) throw new Error('Could not create Fee Address'); + const outputValue = BigInt(bitcoinToSats(bitcoinAmount)); - const utxos = await getUTXOs(nativeSegwitAddress, nativeSegwitPublicKeys, bitcoinNetwork); + const userUTXOs = await getUTXOs(bitcoinNativeSegwitTransaction, bitcoinNetwork); - const outputs = [ - { address: multisigAddress, amount: BigInt(bitcoinToSats(bitcoinAmount)) }, + const psbtOutputs = [ + { address: multisigAddress, amount: outputValue }, { address: feeAddress, - amount: BigInt(bitcoinToSats(bitcoinAmount) * feeBasisPoints), + amount: feeRecipientOutputValue, }, ]; - const selected = selectUTXO(utxos, outputs, 'default', { - changeAddress: nativeSegwitAddress, + const selected = selectUTXO(userUTXOs, psbtOutputs, 'default', { + changeAddress: bitcoinNativeSegwitTransaction.address!, feePerByte: feeRate, bip69: false, createTx: true, @@ -143,20 +172,14 @@ export async function createFundingTransaction( const fundingTX = selected?.tx; + console.log('Funding Transaction:', fundingTX); if (!fundingTX) throw new Error('Could not create Funding Transaction'); - const fundingPSBT = fundingTX.toPSBT(); + const fundingPSBT = fundingTX.toPSBT(0); return fundingPSBT; } -export function getFeeRecipientAddress(feePublicKey: string, bitcoinNetwork: Network): string { - const feePublicKeyBuffer = Buffer.from(feePublicKey, 'hex'); - const { address } = p2wpkh(feePublicKeyBuffer, bitcoinNetwork); - if (!address) throw new Error('Could not create Fee Address'); - return address; -} - /** * Creates a Funding Transaction to fund the Multisig Transaction. * @@ -273,3 +296,230 @@ export async function broadcastTransaction(transaction: string): Promise throw new Error(`Error broadcasting Transaction: ${error}`); } } + +export function getInputPaymentType(index: number, input: TransactionInput, bitcoinNetwork: Network): PaymentTypes { + const bitcoinAddress = getBitcoinInputAddress(index, input, bitcoinNetwork); + + if (bitcoinAddress === '') throw new Error('Bitcoin Address is empty'); + if (bitcoinAddress.startsWith('bc1p') || bitcoinAddress.startsWith('tb1p') || bitcoinAddress.startsWith('bcrt1p')) + return 'p2tr'; + if (bitcoinAddress.startsWith('bc1q') || bitcoinAddress.startsWith('tb1q') || bitcoinAddress.startsWith('bcrt1q')) + return 'p2wpkh'; + throw new Error('Unable to infer payment type from BitcoinAddress'); +} + +export function getBitcoinInputAddress(index: number, input: TransactionInput, bitcoinNetwork: Network) { + if (isDefined(input.witnessUtxo)) return getAddressFromOutScript(input.witnessUtxo.script, bitcoinNetwork); + if (isDefined(input.nonWitnessUtxo)) + return getAddressFromOutScript(input.nonWitnessUtxo.outputs[index]?.script, bitcoinNetwork); + return ''; +} + +export function createRangeFromLength(length: number) { + return [...Array(length).keys()]; +} + +export function isUndefined(value: unknown): value is undefined { + return typeof value === 'undefined'; +} + +export function isDefined(argument: T | undefined): argument is T { + return !isUndefined(argument); +} + +export function getAddressFromOutScript(script: Uint8Array, bitcoinNetwork: Network) { + const outputScript = OutScript.decode(script); + + switch (outputScript.type) { + case 'pkh': + case 'sh': + case 'wpkh': + case 'wsh': + return Address(bitcoinNetwork).encode({ + type: outputScript.type, + hash: outputScript.hash, + }); + case 'tr': + return Address(bitcoinNetwork).encode({ + type: outputScript.type, + pubkey: outputScript.pubkey, + }); + case 'ms': + return p2ms(outputScript.m, outputScript.pubkeys).address ?? ''; + case 'pk': + return p2pk(outputScript.pubkey, bitcoinNetwork).address ?? ''; + case 'tr_ms': + case 'tr_ns': + throw new Error('Unsupported Script Type'); + case 'unknown': + throw new Error('Unknown Script Type'); + default: + throw new Error('Unsupported Script Type'); + } +} + +export function createBitcoinInputSigningConfiguration( + psbt: Uint8Array, + bitcoinNetwork: Network +): BitcoinInputSigningConfig[] { + let nativeSegwitDerivationPath = ''; + let taprootDerivationPath = ''; + + switch (bitcoinNetwork) { + case bitcoin: + nativeSegwitDerivationPath = "m/84'/0'/0'/0/0"; + taprootDerivationPath = "m/86'/0'/0'/0/0"; + break; + case testnet: + nativeSegwitDerivationPath = "m/84'/1'/0'/0/0"; + taprootDerivationPath = "m/86'/1'/0'/0/0"; + break; + default: + throw new Error('Unsupported Bitcoin Network'); + } + + const transaction = Transaction.fromPSBT(psbt); + const indexesToSign = createRangeFromLength(transaction.inputsLength); + return indexesToSign.map((inputIndex) => { + const input = transaction.getInput(inputIndex); + + if (isUndefined(input.index)) throw new Error('Input must have an index for payment type'); + const paymentType = getInputPaymentType(input.index, input, bitcoinNetwork); + + switch (paymentType) { + case 'p2wpkh': + return { + index: inputIndex, + derivationPath: nativeSegwitDerivationPath, + }; + case 'p2tr': + return { + index: inputIndex, + derivationPath: taprootDerivationPath, + }; + default: + throw new Error('Unsupported Payment Type'); + } + }); +} + +export function getInputByPaymentTypeArray( + signingConfiguration: BitcoinInputSigningConfig[], + psbt: Buffer, + bitcoinNetwork: Network +) { + const transaction = Transaction.fromPSBT(psbt); + + return signingConfiguration.map((config) => { + const inputIndex = transaction.getInput(config.index).index; + if (isUndefined(inputIndex)) throw new Error('Input must have an index for payment type'); + return [config, getInputPaymentType(inputIndex, transaction.getInput(config.index), bitcoinNetwork)]; + }) as [BitcoinInputSigningConfig, PaymentTypes][]; +} + +export async function updateNativeSegwitInputs( + inputByPaymentType: [BitcoinInputSigningConfig, PaymentTypes][], + nativeSegwitPublicKey: Buffer, + masterFingerprint: string, + psbt: Psbt +) { + const nativeSegwitInputsToSign = inputByPaymentType + .filter(([_, paymentType]) => paymentType === 'p2wpkh') + .map(([index]) => index); + + if (nativeSegwitInputsToSign.length) { + try { + await addNativeSegwitUTXOLedgerProps(psbt, nativeSegwitInputsToSign); + } catch (e) {} + + await addNativeSegwitBip32Derivation(psbt, masterFingerprint, nativeSegwitPublicKey, nativeSegwitInputsToSign); + + return psbt; + } +} + +export async function updateTaprootInputs( + inputsToUpdate: BitcoinInputSigningConfig[] = [], + taprootPublicKey: Buffer, + masterFingerprint: string, + psbt: Psbt +): Promise { + inputsToUpdate.forEach(({ index, derivationPath }) => { + psbt.updateInput(index, { + tapBip32Derivation: [ + { + masterFingerprint: Buffer.from(masterFingerprint, 'hex'), + pubkey: ecdsaPublicKeyToSchnorr(taprootPublicKey), + path: derivationPath, + leafHashes: [], + }, + ], + }); + }); + + return psbt; +} + +export async function addNativeSegwitUTXOLedgerProps( + psbt: Psbt, + inputSigningConfiguration: BitcoinInputSigningConfig[] +): Promise { + const bitcoinBlockchainAPIURL = process.env.BITCOIN_BLOCKCHAIN_API_URL; + + const inputTransactionHexes = await Promise.all( + psbt.txInputs.map(async (input) => + (await fetch(`${bitcoinBlockchainAPIURL}/tx/${reverseBytes(input.hash).toString('hex')}/hex`)).text() + ) + ); + + inputSigningConfiguration.forEach(({ index }) => { + psbt.updateInput(index, { + nonWitnessUtxo: Buffer.from(inputTransactionHexes[index], 'hex'), + }); + }); + + return psbt; +} + +export async function addNativeSegwitBip32Derivation( + psbt: Psbt, + masterFingerPrint: string, + nativeSegwitPublicKey: Buffer, + inputSigningConfiguration: BitcoinInputSigningConfig[] +): Promise { + inputSigningConfiguration.forEach(({ index, derivationPath }) => { + psbt.updateInput(index, { + bip32Derivation: [ + { + masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), + pubkey: nativeSegwitPublicKey, + path: derivationPath, + }, + ], + }); + }); + + return psbt; +} + +export function addNativeSegwitSignaturesToPSBT(psbt: Psbt, signatures: [number, PartialSignature][]) { + signatures.forEach(([index, signature]) => psbt.updateInput(index, { partialSig: [signature] })); +} + +export function addTaprootInputSignaturesToPSBT(psbt: Psbt, signatures: [number, PartialSignature][]) { + signatures.forEach(([index, signature]) => psbt.updateInput(index, { tapKeySig: signature.signature })); +} + +const ecdsaPublicKeyLength = 33; + +export function ecdsaPublicKeyToSchnorr(publicKey: Buffer) { + if (publicKey.byteLength !== ecdsaPublicKeyLength) throw new Error('Invalid Public Key Length'); + return publicKey.subarray(1); +} + +export function reverseBytes(bytes: Buffer): Buffer; +export function reverseBytes(bytes: Uint8Array): Uint8Array; +export function reverseBytes(bytes: Buffer | Uint8Array) { + if (Buffer.isBuffer(bytes)) return Buffer.from(bytes).reverse(); + return new Uint8Array(bytes.slice().reverse()); +} diff --git a/src/index.ts b/src/index.ts index 768267d..5fad2d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,6 @@ import { createClosingTransaction, createFundingTransaction, createMultisigTransaction, - getFeeRecipientAddress, getUTXOs, } from './bitcoin-functions.js'; import { diff --git a/src/ledger_test.ts b/src/ledger_test.ts index 67b274b..2601cdb 100644 --- a/src/ledger_test.ts +++ b/src/ledger_test.ts @@ -1,12 +1,24 @@ /** @format */ import Transport from '@ledgerhq/hw-transport-node-hid'; -import { testnet } from 'bitcoinjs-lib/src/networks.js'; +import { Network, bitcoin, testnet } from 'bitcoinjs-lib/src/networks.js'; import { AppClient, DefaultWalletPolicy, WalletPolicy } from 'ledger-bitcoin'; import { BIP32Factory } from 'bip32'; import * as ellipticCurveCryptography from 'tiny-secp256k1'; -import { initEccLib } from 'bitcoinjs-lib'; -import { p2tr, p2tr_ns, p2wpkh } from '@scure/btc-signer'; +import { Psbt, initEccLib } from 'bitcoinjs-lib'; +import { p2tr, p2tr_ns, p2wpkh, Transaction } from '@scure/btc-signer'; +import { + addNativeSegwitSignaturesToPSBT, + addTaprootInputSignaturesToPSBT, + createBitcoinInputSigningConfiguration, + createClosingTransaction, + createFundingTransaction, + getInputByPaymentTypeArray, + updateNativeSegwitInputs, + updateTaprootInputs, +} from './bitcoin-functions.js'; +import { TEST_BITCOIN_AMOUNT, TEST_FEE_AMOUNT, TEST_FEE_PUBLIC_KEY, TEST_FEE_RATE } from './constants.js'; +import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; type BitcoinNetworkName = 'Mainnet' | 'Testnet'; @@ -15,7 +27,6 @@ const TEST_EXTENDED_PRIVATE_KEY_1 = const TEST_EXTENDED_PUBLIC_KEY_1 = 'tpubD6NzVbkrYhZ4Wwhizz1jTWe6vYDL1Z5fm7mphbFJNKhXxwRoDvW4yEtGmWJ6n9JE86wpvQsDpzn5t49uenYStgAqwgmKNjDe1D71TdAjy8o'; const TEST_MASTER_FINGERPRINT_1 = '8400dc04'; -const TAPROOT_UNSPENDABLE_KEY_STRING = '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; const TEST_EXTENDED_PRIVATE_KEY_2 = 'tprv8ZgxMBicQKsPfJ6T1H5ErNLa1fZyj2fxCR7vRqVokCLvWg9JypYJoGVdvU6UNkj59o6qDdB97QFk7CQa2XnKZGSzQGhfoc4hCGXrviFuxwP'; @@ -23,14 +34,29 @@ const TEST_EXTENDED_PUBLIC_KEY_2 = 'tpubD6NzVbkrYhZ4Ym8EtvjqFmzgah5utMrrmiihiMY7AU9KMAQ5cDMtym7W6ccSUinTVbDqK1Vno96HNhaqhS1DuVCrjHoFG9bFa3DKUUMErCv'; const TEST_MASTER_FINGERPRINT_2 = 'b2cd3e18'; -const rootTaprootDerivationPath = "86'/1'/0'"; -const rootSegwitDerivationPath = "86'/1'/0'"; +const TAPROOT_UNSPENDABLE_KEY_STRING = '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0'; + +const ROOT_TAPROOT_DERIVATION_PATH = "86'/1'/0'"; +const ROOT_NATIVE_SEGWIT_DERIVATION_PATH = "84'/1'/0'"; initEccLib(ellipticCurveCryptography); const bip32 = BIP32Factory(ellipticCurveCryptography); +function getBitcoinNetwork(): [BitcoinNetworkName, Network] { + const { BITCOIN_NETWORK } = process.env; + switch (BITCOIN_NETWORK) { + case 'Mainnet': + return ['Mainnet', bitcoin]; + case 'Testnet': + return ['Testnet', testnet]; + default: + throw new Error('Invalid Bitcoin Network'); + } +} + export async function main(transport: any) { - const bitcoinNetworkName: BitcoinNetworkName = 'Testnet'; + // ==> Get Bitcoin Network + const [bitcoinNetworkName, bitcoinNetwork] = getBitcoinNetwork(); // ==> Create a new instance of the AppClient const ledgerApp = new AppClient(transport); @@ -39,12 +65,21 @@ export async function main(transport: any) { const fpr = await ledgerApp.getMasterFingerprint(); // ==> Get Ledger First Native Segwit Extended Public Key - const ledgerFirstNativeSegwitExtendedPublicKey = await ledgerApp.getExtendedPubkey(`m/${rootSegwitDerivationPath}`); + const ledgerFirstNativeSegwitExtendedPublicKey = await ledgerApp.getExtendedPubkey( + `m${ROOT_NATIVE_SEGWIT_DERIVATION_PATH}` + ); + console.log( + `[Ledger][${bitcoinNetworkName}] Ledger First Native Segwit Extended Public Key: ${ledgerFirstNativeSegwitExtendedPublicKey}` + ); // ==> Get Ledger First Native Segwit Account Policy const ledgerFirstNativeSegwitAccountPolicy = new DefaultWalletPolicy( 'wpkh(@0/**)', - `[${fpr}/${rootSegwitDerivationPath}]${ledgerFirstNativeSegwitExtendedPublicKey}` + `[${fpr}/${ROOT_NATIVE_SEGWIT_DERIVATION_PATH}]${ledgerFirstNativeSegwitExtendedPublicKey}` + ); + + console.log( + `[Ledger][${bitcoinNetworkName}] Ledger First Native Segwit Account Policy: ${ledgerFirstNativeSegwitAccountPolicy.toString()}` ); // ==> Get Ledger First Native Segwit Address @@ -55,32 +90,38 @@ export async function main(transport: any) { 0, true // show address on the wallet's screen ); - console.log(`Ledger First Native Segwit Account Address: ${ledgerFirstNativeSegwitAccountAddress}`); + console.log( + `[Ledger][${bitcoinNetworkName}] Ledger First Native Segwit Account Address: ${ledgerFirstNativeSegwitAccountAddress}` + ); + + const nativeSegwitDerivedPublicKey = bip32 + .fromBase58(ledgerFirstNativeSegwitExtendedPublicKey, testnet) + .derivePath('0/0').publicKey; // ==> Get derivation path for Ledger Native Segwit Address - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(async (index) => { - const nativeSegwitAddress = p2wpkh( - bip32.fromBase58(ledgerFirstNativeSegwitExtendedPublicKey, testnet).derivePath(`0/${index}`).publicKey, - testnet - ).address; - if (nativeSegwitAddress === ledgerFirstNativeSegwitAccountAddress) { - console.log(`Found Ledger Native Segwit Address at derivation path ${rootSegwitDerivationPath}0/${index}`); - } - }); + const nativeSegwitTransaction = p2wpkh(nativeSegwitDerivedPublicKey, bitcoinNetwork); + + console.log(`[Ledger][${bitcoinNetworkName}] Recreated Native Segwit Address: ${nativeSegwitTransaction.address}`); + + if (nativeSegwitTransaction.address !== ledgerFirstNativeSegwitAccountAddress) { + throw new Error( + `[Ledger][${bitcoinNetworkName}] Recreated Native Segwit Address does not match the Ledger Native Segwit Address` + ); + } // ==> Get Ledger Derived Public Key - const ledgerExtendedPublicKey = await ledgerApp.getExtendedPubkey(`m/${rootTaprootDerivationPath}`); + const ledgerExtendedPublicKey = await ledgerApp.getExtendedPubkey(`m/${ROOT_TAPROOT_DERIVATION_PATH}`); // ==> Get External Derived Public Keys const unspendableExtendedPublicKey = bip32 - .fromBase58(TEST_EXTENDED_PRIVATE_KEY_1, testnet) - .derivePath(`m/${rootTaprootDerivationPath}`) + .fromBase58(TEST_EXTENDED_PRIVATE_KEY_1, bitcoinNetwork) + .derivePath(`m/${ROOT_TAPROOT_DERIVATION_PATH}`) .neutered() .toBase58(); const externalExtendedPublicKey = bip32 - .fromBase58(TEST_EXTENDED_PRIVATE_KEY_2, testnet) - .derivePath(`m/${rootTaprootDerivationPath}`) + .fromBase58(TEST_EXTENDED_PRIVATE_KEY_2, bitcoinNetwork) + .derivePath(`m/${ROOT_TAPROOT_DERIVATION_PATH}`) .neutered() .toBase58(); @@ -89,7 +130,7 @@ export async function main(transport: any) { console.log(`[Ledger][${bitcoinNetworkName}] External Extended Public Key 2: ${externalExtendedPublicKey}`); // ==> Create Key Info - const ledgerKeyInfo = `[${fpr}/${rootTaprootDerivationPath}]${ledgerExtendedPublicKey}`; + const ledgerKeyInfo = `[${fpr}/${ROOT_TAPROOT_DERIVATION_PATH}]${ledgerExtendedPublicKey}`; console.log(`[Ledger][${bitcoinNetworkName}] Ledger Key Info: ${ledgerKeyInfo}`); // We don't need to create the external key info, as we can use the extended public key directly. @@ -97,40 +138,138 @@ export async function main(transport: any) { // const externalKeyInfo2 = `[${TEST_MASTER_FINGERPRINT_2}/${derivationPath}]${externalExtendedPublicKey2}`; // ==> Create Multisig Wallet Policy - const multisigPolicy = new WalletPolicy('Multisig Taproot Wallet', `tr(@0/**,and_v(v:pk(@1/**),pk(@2/**)))`, [ + const ledgerMultisigPolicy = new WalletPolicy('Multisig Taproot Wallet', `tr(@0/**,and_v(v:pk(@1/**),pk(@2/**)))`, [ unspendableExtendedPublicKey, externalExtendedPublicKey, ledgerKeyInfo, ]); // ==> Register Wallet - const [policyId, policyHmac] = await ledgerApp.registerWallet(multisigPolicy); + const [policyId, policyHmac] = await ledgerApp.registerWallet(ledgerMultisigPolicy); console.log(`[Ledger][${bitcoinNetworkName}] Policy HMac: ${policyHmac.toString('hex')}`); // => Assert Policy ID - console.assert(policyId.compare(multisigPolicy.getId()) == 0); // + console.assert(policyId.compare(ledgerMultisigPolicy.getId()) == 0); // // ==> Get Wallet Address from Ledger - const multisigAddressFromLedger = await ledgerApp.getWalletAddress(multisigPolicy, policyHmac, 0, 0, true); - console.log( - `[Ledger][${bitcoinNetworkName}]Taproot Multisig Wallet Address From Ledger: ${multisigAddressFromLedger}` - ); + const ledgerMultisigAddress = await ledgerApp.getWalletAddress(ledgerMultisigPolicy, policyHmac, 0, 0, true); + console.log(`[Ledger][${bitcoinNetworkName}] Ledger Taproot Multisig Wallet Address: ${ledgerMultisigAddress}`); // ==> Recreate Multisig Address to retrieve script const multiLeafWallet = p2tr_ns(2, [ - bip32.fromBase58(externalExtendedPublicKey).derivePath('0/0').publicKey, - bip32.fromBase58(ledgerExtendedPublicKey).derivePath('0/0').publicKey, + bip32.fromBase58(externalExtendedPublicKey, bitcoinNetwork).derivePath('0/0').publicKey.subarray(1), + bip32.fromBase58(ledgerExtendedPublicKey, bitcoinNetwork).derivePath('0/0').publicKey.subarray(1), ]); const multisigTransaction = p2tr( - bip32.fromBase58(unspendableExtendedPublicKey).derivePath('0/0').publicKey, + bip32.fromBase58(unspendableExtendedPublicKey, bitcoinNetwork).derivePath('0/0').publicKey.subarray(1), multiLeafWallet, - testnet + bitcoinNetwork + ); + + if (ledgerMultisigAddress !== multisigTransaction.address) { + throw new Error( + `[Ledger][${bitcoinNetworkName}] Recreated Multisig Address does not match the Ledger Multisig Address` + ); + } + + // ==> Create Funding Transaction + const fundingPSBT = await createFundingTransaction( + TEST_BITCOIN_AMOUNT, + bitcoinNetwork, + multisigTransaction.address, + nativeSegwitTransaction, + TEST_FEE_RATE, + TEST_FEE_PUBLIC_KEY, + TEST_FEE_AMOUNT + ); + + // ==> Update Funding PSBT with Ledger related information + const signingConfiguration = createBitcoinInputSigningConfiguration(fundingPSBT, bitcoinNetwork); + + console.log(`[Ledger][${bitcoinNetworkName}] Signing Configuration: ${signingConfiguration}`); + + const formattedFundingPSBT = Psbt.fromBuffer(Buffer.from(fundingPSBT), { + network: bitcoinNetwork, + }); + + const inputByPaymentTypeArray = getInputByPaymentTypeArray( + signingConfiguration, + formattedFundingPSBT.toBuffer(), + bitcoinNetwork + ); + + await updateNativeSegwitInputs(inputByPaymentTypeArray, nativeSegwitDerivedPublicKey, fpr, formattedFundingPSBT); + + // ==> Sign Funding PSBT with Ledger + const fundingTransactionSignatures = await ledgerApp.signPsbt( + formattedFundingPSBT.toBase64(), + ledgerFirstNativeSegwitAccountPolicy, + null + ); + + console.log('[Ledger][${bitcoinNetworkName}] Funding PSBT Ledger Signatures:', fundingTransactionSignatures); + + addNativeSegwitSignaturesToPSBT(formattedFundingPSBT, fundingTransactionSignatures); + + const fundingTransaction = Transaction.fromPSBT(formattedFundingPSBT.toBuffer()); + + // ==> Finalize Funding Transaction + fundingTransaction.finalize(); + + console.log('[Ledger][${bitcoinNetworkName}] Funding Transaction Signed By Ledger:', fundingTransaction); + + // ==> Create Closing PSBT + const closingPSBT = await createClosingTransaction( + TEST_BITCOIN_AMOUNT, + bitcoinNetwork, + fundingTransaction.id, + multisigTransaction, + nativeSegwitTransaction.address, + TEST_FEE_RATE, + TEST_FEE_PUBLIC_KEY, + TEST_FEE_AMOUNT ); - const multisigAddress = multisigTransaction.address; - console.log(`[Ledger][${bitcoinNetworkName}] Recreated Multisig Address: ${multisigAddress}`); + // ==> Update Closing PSBT with Ledger related information + const closingTransactionSigningConfiguration = createBitcoinInputSigningConfiguration(closingPSBT, bitcoinNetwork); + + const formattedClosingPSBT = Psbt.fromBuffer(Buffer.from(closingPSBT), { + network: bitcoinNetwork, + }); + + const closingInputByPaymentTypeArray = getInputByPaymentTypeArray( + closingTransactionSigningConfiguration, + formattedClosingPSBT.toBuffer(), + bitcoinNetwork + ); + + const taprootInputsToSign = closingInputByPaymentTypeArray + .filter(([_, paymentType]) => paymentType === 'p2tr') + .map(([index]) => index); + + updateTaprootInputs( + taprootInputsToSign, + bip32.fromBase58(ledgerExtendedPublicKey, bitcoinNetwork).derivePath('0/0').publicKey, + fpr, + formattedClosingPSBT + ); + + // ==> Sign Closing PSBT with Ledger + const closingTransactionSignatures = await ledgerApp.signPsbt( + formattedClosingPSBT.toBase64(), + ledgerMultisigPolicy, + policyHmac + ); + + console.log('[Ledger][${bitcoinNetworkName}] Closing PSBT Ledger Signatures:', closingTransactionSignatures); + + addTaprootInputSignaturesToPSBT(formattedClosingPSBT, closingTransactionSignatures); + + const closingTransaction = Transaction.fromPSBT(formattedClosingPSBT.toBuffer()); + + console.log('[Ledger][${bitcoinNetworkName}] Closing Transaction Partially Signed By Ledger:', closingTransaction); } export async function testLedger() {