From b84d4871124424b88a2665ebd87d1af375fcb37c Mon Sep 17 00:00:00 2001 From: Polybius93 <99192647+Polybius93@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:15:45 +0200 Subject: [PATCH 1/2] feat: taproot funding support (#11) * feat: add taproot support for funding transaction * feat: modify PrivateKeyDLCHandler to support taproot for the funding transaction --- src/dlc-handlers/ledger-dlc-handler.ts | 192 ++++++++++-------- src/dlc-handlers/private-key-dlc-handler.ts | 98 +++++---- .../software-wallet-dlc-handler.ts | 65 +++--- src/functions/bitcoin/bitcoin-functions.ts | 12 +- src/functions/bitcoin/psbt-functions.ts | 37 ++-- src/models/bitcoin-models.ts | 6 +- tests/mocks/constants.ts | 1 + tests/unit/sign-transactions.test.ts | 8 +- 8 files changed, 234 insertions(+), 185 deletions(-) diff --git a/src/dlc-handlers/ledger-dlc-handler.ts b/src/dlc-handlers/ledger-dlc-handler.ts index 3afb97b..14d49d8 100644 --- a/src/dlc-handlers/ledger-dlc-handler.ts +++ b/src/dlc-handlers/ledger-dlc-handler.ts @@ -1,5 +1,5 @@ import { Transaction } from '@scure/btc-signer'; -import { P2Ret, P2TROut, p2wpkh } from '@scure/btc-signer/payment'; +import { P2Ret, P2TROut, p2tr, p2wpkh } from '@scure/btc-signer/payment'; import { Network, Psbt } from 'bitcoinjs-lib'; import { bitcoin, regtest, testnet } from 'bitcoinjs-lib/src/networks.js'; import { AppClient, DefaultWalletPolicy, WalletPolicy } from 'ledger-bitcoin'; @@ -8,6 +8,7 @@ import { createBitcoinInputSigningConfiguration, createTaprootMultisigPayment, deriveUnhardenedPublicKey, + ecdsaPublicKeyToSchnorr, getBalance, getFeeRate, getInputByPaymentTypeArray, @@ -28,18 +29,20 @@ import { RawVault } from '../models/ethereum-models.js'; import { truncateAddress } from '../utilities/index.js'; interface LedgerPolicyInformation { - nativeSegwitWalletPolicy: DefaultWalletPolicy; - taprootMultisigWalletPolicy: WalletPolicy; - taprootMultisigWalletPolicyHMac: Buffer; + fundingWalletPolicy: DefaultWalletPolicy; + multisigWalletPolicy: WalletPolicy; + multisigWalletPolicyHMac: Buffer; } export class LedgerDLCHandler { private ledgerApp: AppClient; private masterFingerprint: string; private walletAccountIndex: number; + private fundingPaymentType: 'wpkh' | 'tr'; private policyInformation: LedgerPolicyInformation | undefined; public payment: ExtendedPaymentInformation | undefined; private bitcoinNetwork: Network; + private bitcoinNetworkIndex: number; private bitcoinBlockchainAPI: string; private bitcoinBlockchainFeeRecommendationAPI: string; @@ -47,6 +50,7 @@ export class LedgerDLCHandler { ledgerApp: AppClient, masterFingerprint: string, walletAccountIndex: number, + fundingPaymentType: 'wpkh' | 'tr', bitcoinNetwork: Network, bitcoinBlockchainAPI?: string, bitcoinBlockchainFeeRecommendationAPI?: string @@ -56,11 +60,13 @@ export class LedgerDLCHandler { this.bitcoinBlockchainAPI = 'https://mempool.space/api'; this.bitcoinBlockchainFeeRecommendationAPI = 'https://mempool.space/api/v1/fees/recommended'; + this.bitcoinNetworkIndex = 0; break; case testnet: this.bitcoinBlockchainAPI = 'https://mempool.space/testnet/api'; this.bitcoinBlockchainFeeRecommendationAPI = 'https://mempool.space/testnet/api/v1/fees/recommended'; + this.bitcoinNetworkIndex = 1; break; case regtest: if ( @@ -73,6 +79,7 @@ export class LedgerDLCHandler { } this.bitcoinBlockchainAPI = bitcoinBlockchainAPI; this.bitcoinBlockchainFeeRecommendationAPI = bitcoinBlockchainFeeRecommendationAPI; + this.bitcoinNetworkIndex = 1; break; default: throw new Error('Invalid Bitcoin Network'); @@ -80,30 +87,31 @@ export class LedgerDLCHandler { this.ledgerApp = ledgerApp; this.masterFingerprint = masterFingerprint; this.walletAccountIndex = walletAccountIndex; + this.fundingPaymentType = fundingPaymentType; this.bitcoinNetwork = bitcoinNetwork; } private setPolicyInformation( - nativeSegwitWalletPolicy: DefaultWalletPolicy, - taprootMultisigWalletPolicy: WalletPolicy, - taprootMultisigWalletPolicyHMac: Buffer + fundingWalletPolicy: DefaultWalletPolicy, + multisigWalletPolicy: WalletPolicy, + multisigWalletPolicyHMac: Buffer ): void { this.policyInformation = { - nativeSegwitWalletPolicy, - taprootMultisigWalletPolicy, - taprootMultisigWalletPolicyHMac, + fundingWalletPolicy, + multisigWalletPolicy, + multisigWalletPolicyHMac, }; } private setPayment( - nativeSegwitPayment: P2Ret, - nativeSegwitDerivedPublicKey: Buffer, - taprootMultisigPayment: P2TROut, + fundingPayment: P2Ret | P2TROut, + fundingDerivedPublicKey: Buffer, + multisigPayment: P2TROut, taprootDerivedPublicKey: Buffer ): void { this.payment = { - nativeSegwitPayment, - nativeSegwitDerivedPublicKey, - taprootMultisigPayment, + fundingPayment, + fundingDerivedPublicKey, + multisigPayment, taprootDerivedPublicKey, }; } @@ -122,7 +130,7 @@ export class LedgerDLCHandler { return this.payment; } - getVaultRelatedAddress(paymentType: 'p2wpkh' | 'p2tr'): string { + getVaultRelatedAddress(paymentType: 'funding' | 'multisig'): string { const payment = this.getPayment(); if (payment === undefined) { @@ -132,17 +140,17 @@ export class LedgerDLCHandler { let address: string; switch (paymentType) { - case 'p2wpkh': - if (!payment.nativeSegwitPayment.address) { - throw new Error('Native Segwit Payment Address is undefined'); + case 'funding': + if (!payment.fundingPayment.address) { + throw new Error('Funding Payment Address is undefined'); } - address = payment.nativeSegwitPayment.address; + address = payment.fundingPayment.address; return address; - case 'p2tr': - if (!payment.taprootMultisigPayment.address) { + case 'multisig': + if (!payment.multisigPayment.address) { throw new Error('Taproot Multisig Payment Address is undefined'); } - address = payment.taprootMultisigPayment.address; + address = payment.multisigPayment.address; return address; default: throw new Error('Invalid Payment Type'); @@ -154,33 +162,40 @@ export class LedgerDLCHandler { attestorGroupPublicKey: string ): Promise { try { - const networkIndex = this.bitcoinNetwork === bitcoin ? 0 : 1; + const fundingPaymentTypeDerivationPath = this.fundingPaymentType === 'wpkh' ? '84' : '86'; - const nativeSegwitExtendedPublicKey = await this.ledgerApp.getExtendedPubkey( - `m/84'/${networkIndex}'/${this.walletAccountIndex}'` + const fundingExtendedPublicKey = await this.ledgerApp.getExtendedPubkey( + `m/${fundingPaymentTypeDerivationPath}'/${this.bitcoinNetworkIndex}'/${this.walletAccountIndex}'` ); - const nativeSegwitKeyinfo = `[${this.masterFingerprint}/84'/${networkIndex}'/${this.walletAccountIndex}']${nativeSegwitExtendedPublicKey}`; + const fundingKeyinfo = `[${this.masterFingerprint}/${fundingPaymentTypeDerivationPath}'/${this.bitcoinNetworkIndex}'/${this.walletAccountIndex}']${fundingExtendedPublicKey}`; - const nativeSegwitWalletPolicy = new DefaultWalletPolicy('wpkh(@0/**)', nativeSegwitKeyinfo); + const fundingWalletPolicy = new DefaultWalletPolicy( + `${this.fundingPaymentType}(@0/**)`, + fundingKeyinfo + ); - const nativeSegwitAddress = await this.ledgerApp.getWalletAddress( - nativeSegwitWalletPolicy, + const fundingAddress = await this.ledgerApp.getWalletAddress( + fundingWalletPolicy, null, 0, 0, false ); - const nativeSegwitDerivedPublicKey = deriveUnhardenedPublicKey( - nativeSegwitExtendedPublicKey, + const fundingDerivedPublicKey = deriveUnhardenedPublicKey( + fundingExtendedPublicKey, this.bitcoinNetwork ); - const nativeSegwitPayment = p2wpkh(nativeSegwitDerivedPublicKey, this.bitcoinNetwork); - if (nativeSegwitPayment.address !== nativeSegwitAddress) { + const fundingPayment = + this.fundingPaymentType === 'wpkh' + ? p2wpkh(fundingDerivedPublicKey, this.bitcoinNetwork) + : p2tr(ecdsaPublicKeyToSchnorr(fundingDerivedPublicKey), undefined, this.bitcoinNetwork); + + if (fundingPayment.address !== fundingAddress) { throw new Error( - `[Ledger] Recreated Native Segwit Address does not match the Ledger Native Segwit Address` + `[Ledger] Recreated Funding Address does not match the Ledger Funding Address` ); } @@ -196,10 +211,10 @@ export class LedgerDLCHandler { ); const taprootExtendedPublicKey = await this.ledgerApp.getExtendedPubkey( - `m/86'/${networkIndex}'/${this.walletAccountIndex}'` + `m/86'/${this.bitcoinNetworkIndex}'/${this.walletAccountIndex}'` ); - const ledgerTaprootKeyInfo = `[${this.masterFingerprint}/86'/${networkIndex}'/${this.walletAccountIndex}']${taprootExtendedPublicKey}`; + const ledgerTaprootKeyInfo = `[${this.masterFingerprint}/86'/${this.bitcoinNetworkIndex}'/${this.walletAccountIndex}']${taprootExtendedPublicKey}`; const taprootDerivedPublicKey = deriveUnhardenedPublicKey( taprootExtendedPublicKey, @@ -229,33 +244,33 @@ export class LedgerDLCHandler { false ); - const taprootMultisigPayment = createTaprootMultisigPayment( + const multisigPayment = createTaprootMultisigPayment( unspendableDerivedPublicKey, attestorDerivedPublicKey, taprootDerivedPublicKey, this.bitcoinNetwork ); - if (taprootMultisigAddress !== taprootMultisigPayment.address) { + if (taprootMultisigAddress !== multisigPayment.address) { throw new Error(`Recreated Multisig Address does not match the Ledger Multisig Address`); } this.setPolicyInformation( - nativeSegwitWalletPolicy, + fundingWalletPolicy, taprootMultisigAccountPolicy, taprootMultisigPolicyHMac ); this.setPayment( - nativeSegwitPayment, - nativeSegwitDerivedPublicKey, - taprootMultisigPayment, + fundingPayment, + fundingDerivedPublicKey, + multisigPayment, taprootDerivedPublicKey ); return { - nativeSegwitPayment, - nativeSegwitDerivedPublicKey, - taprootMultisigPayment, + fundingPayment, + fundingDerivedPublicKey, + multisigPayment, taprootDerivedPublicKey, }; } catch (error: any) { @@ -270,13 +285,12 @@ export class LedgerDLCHandler { customFeeRate?: bigint ): Promise { try { - const { nativeSegwitPayment, nativeSegwitDerivedPublicKey, taprootMultisigPayment } = - await this.createPayment(vault.uuid, attestorGroupPublicKey); + const { fundingPayment, fundingDerivedPublicKey, multisigPayment } = await this.createPayment( + vault.uuid, + attestorGroupPublicKey + ); - if ( - taprootMultisigPayment.address === undefined || - nativeSegwitPayment.address === undefined - ) { + if ([multisigPayment.address, fundingPayment.address].some(x => x === undefined)) { throw new Error('Payment Address is undefined'); } @@ -285,7 +299,7 @@ export class LedgerDLCHandler { BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); const addressBalance = await getBalance( - nativeSegwitPayment.address, + fundingPayment.address as string, this.bitcoinBlockchainAPI ); @@ -296,8 +310,8 @@ export class LedgerDLCHandler { const fundingPSBT = await createFundingTransaction( vault.valueLocked.toBigInt(), this.bitcoinNetwork, - taprootMultisigPayment.address, - nativeSegwitPayment, + multisigPayment.address as string, + fundingPayment, feeRate, vault.btcFeeRecipient, vault.btcMintFeeBasisPoints.toBigInt(), @@ -320,15 +334,26 @@ export class LedgerDLCHandler { this.bitcoinNetwork ); - const nativeSegwitInputsToSign = getNativeSegwitInputsToSign(inputByPaymentTypeArray); + if (this.fundingPaymentType === 'wpkh') { + const nativeSegwitInputsToSign = getNativeSegwitInputsToSign(inputByPaymentTypeArray); - await updateNativeSegwitInputs( - nativeSegwitInputsToSign, - nativeSegwitDerivedPublicKey, - this.masterFingerprint, - formattedFundingPSBT, - this.bitcoinBlockchainAPI - ); + await updateNativeSegwitInputs( + nativeSegwitInputsToSign, + fundingDerivedPublicKey, + this.masterFingerprint, + formattedFundingPSBT, + this.bitcoinBlockchainAPI + ); + } else { + const taprootInputsToSign = getTaprootInputsToSign(inputByPaymentTypeArray); + + await updateTaprootInputs( + taprootInputsToSign, + fundingDerivedPublicKey, + this.masterFingerprint, + formattedFundingPSBT + ); + } return formattedFundingPSBT; } catch (error: any) { @@ -343,10 +368,9 @@ export class LedgerDLCHandler { customFeeRate?: bigint ): Promise { try { - const { nativeSegwitPayment, taprootMultisigPayment, taprootDerivedPublicKey } = - this.getPayment(); + const { fundingPayment, multisigPayment, taprootDerivedPublicKey } = this.getPayment(); - if (nativeSegwitPayment.address === undefined) { + if (fundingPayment.address === undefined) { throw new Error('Could not get Addresses from Payments'); } @@ -358,8 +382,8 @@ export class LedgerDLCHandler { vault.valueLocked.toBigInt(), this.bitcoinNetwork, fundingTransactionID, - taprootMultisigPayment, - nativeSegwitPayment.address, + multisigPayment, + fundingPayment.address, feeRate, vault.btcFeeRecipient, vault.btcRedeemFeeBasisPoints.toBigInt() @@ -398,33 +422,35 @@ export class LedgerDLCHandler { async signPSBT(psbt: Psbt, transactionType: 'funding' | 'closing'): Promise { try { - const { - nativeSegwitWalletPolicy, - taprootMultisigWalletPolicy, - taprootMultisigWalletPolicyHMac, - } = this.getPolicyInformation(); + const { fundingWalletPolicy, multisigWalletPolicy, multisigWalletPolicyHMac } = + this.getPolicyInformation(); let signatures; let transaction: Transaction; switch (transactionType) { case 'funding': - signatures = await this.ledgerApp.signPsbt( - psbt.toBase64(), - nativeSegwitWalletPolicy, - null - ); - addNativeSegwitSignaturesToPSBT(psbt, signatures); + signatures = await this.ledgerApp.signPsbt(psbt.toBase64(), fundingWalletPolicy, null); + switch (this.fundingPaymentType) { + case 'wpkh': + addNativeSegwitSignaturesToPSBT(psbt, signatures); + break; + case 'tr': + addTaprootInputSignaturesToPSBT('funding', psbt, signatures); + break; + default: + throw new Error('Invalid Funding Payment Type'); + } transaction = Transaction.fromPSBT(psbt.toBuffer()); transaction.finalize(); return transaction; case 'closing': signatures = await this.ledgerApp.signPsbt( psbt.toBase64(), - taprootMultisigWalletPolicy, - taprootMultisigWalletPolicyHMac + multisigWalletPolicy, + multisigWalletPolicyHMac ); - addTaprootInputSignaturesToPSBT(psbt, signatures); + addTaprootInputSignaturesToPSBT('closing', psbt, signatures); transaction = Transaction.fromPSBT(psbt.toBuffer()); return transaction; default: diff --git a/src/dlc-handlers/private-key-dlc-handler.ts b/src/dlc-handlers/private-key-dlc-handler.ts index 5d879b4..a07cb82 100644 --- a/src/dlc-handlers/private-key-dlc-handler.ts +++ b/src/dlc-handlers/private-key-dlc-handler.ts @@ -1,5 +1,5 @@ import { Transaction, p2wpkh } from '@scure/btc-signer'; -import { P2Ret, P2TROut } from '@scure/btc-signer/payment'; +import { P2Ret, P2TROut, p2tr } from '@scure/btc-signer/payment'; import { Signer } from '@scure/btc-signer/transaction'; import { BIP32Interface } from 'bip32'; import { Network } from 'bitcoinjs-lib'; @@ -9,6 +9,7 @@ import { createTaprootMultisigPayment, deriveUnhardenedKeyPairFromRootPrivateKey, deriveUnhardenedPublicKey, + ecdsaPublicKeyToSchnorr, getBalance, getFeeRate, getUnspendableKeyCommittedToUUID, @@ -21,13 +22,14 @@ import { PaymentInformation } from '../models/bitcoin-models.js'; import { RawVault } from '../models/ethereum-models.js'; interface RequiredKeyPair { - nativeSegwitDerivedKeyPair: BIP32Interface; + fundingDerivedKeyPair: BIP32Interface; taprootDerivedKeyPair: BIP32Interface; } export class PrivateKeyDLCHandler { private derivedKeyPair: RequiredKeyPair; public payment: PaymentInformation | undefined; + private fundingPaymentType: 'wpkh' | 'tr'; private bitcoinNetwork: Network; private bitcoinBlockchainAPI: string; private bitcoinBlockchainFeeRecommendationAPI: string; @@ -35,6 +37,7 @@ export class PrivateKeyDLCHandler { constructor( bitcoinWalletPrivateKey: string, walletAccountIndex: number, + fundingPaymentType: 'wpkh' | 'tr', bitcoinNetwork: Network, bitcoinBlockchainAPI?: string, bitcoinBlockchainFeeRecommendationAPI?: string @@ -65,11 +68,12 @@ export class PrivateKeyDLCHandler { default: throw new Error('Invalid Bitcoin Network'); } + this.fundingPaymentType = fundingPaymentType; this.bitcoinNetwork = bitcoinNetwork; - const nativeSegwitDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( + const fundingDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( bitcoinWalletPrivateKey, bitcoinNetwork, - 'p2wpkh', + fundingPaymentType === 'wpkh' ? 'p2wpkh' : 'p2tr', walletAccountIndex ); const taprootDerivedKeyPair = deriveUnhardenedKeyPairFromRootPrivateKey( @@ -81,18 +85,18 @@ export class PrivateKeyDLCHandler { this.derivedKeyPair = { taprootDerivedKeyPair, - nativeSegwitDerivedKeyPair, + fundingDerivedKeyPair, }; } - private setPayment(nativeSegwitPayment: P2Ret, taprootMultisigPayment: P2TROut): void { + private setPayment(fundingPayment: P2Ret | P2TROut, multisigPayment: P2TROut): void { this.payment = { - nativeSegwitPayment, - taprootMultisigPayment, + fundingPayment, + multisigPayment, }; } - getVaultRelatedAddress(paymentType: 'p2wpkh' | 'p2tr'): string { + getVaultRelatedAddress(paymentType: 'funding' | 'multisig'): string { const payment = this.payment; if (payment === undefined) { @@ -102,27 +106,27 @@ export class PrivateKeyDLCHandler { let address: string; switch (paymentType) { - case 'p2wpkh': - if (!payment.nativeSegwitPayment.address) { - throw new Error('Native Segwit Payment Address is undefined'); + case 'funding': + if (!payment.fundingPayment.address) { + throw new Error('Funding Address is undefined'); } - address = payment.nativeSegwitPayment.address; + address = payment.fundingPayment.address; return address; - case 'p2tr': - if (!payment.taprootMultisigPayment.address) { + case 'multisig': + if (!payment.multisigPayment.address) { throw new Error('Taproot Multisig Payment Address is undefined'); } - address = payment.taprootMultisigPayment.address; + address = payment.multisigPayment.address; return address; default: throw new Error('Invalid Payment Type'); } } - private getPrivateKey(paymentType: 'p2wpkh' | 'p2tr'): Signer { + private getPrivateKey(paymentType: 'funding' | 'taproot'): Signer { const privateKey = - paymentType === 'p2wpkh' - ? this.derivedKeyPair.nativeSegwitDerivedKeyPair.privateKey + paymentType === 'funding' + ? this.derivedKeyPair.fundingDerivedKeyPair.privateKey : this.derivedKeyPair.taprootDerivedKeyPair.privateKey; if (!privateKey) { @@ -145,23 +149,38 @@ export class PrivateKeyDLCHandler { this.bitcoinNetwork ); - const nativeSegwitPayment = p2wpkh( - this.derivedKeyPair.nativeSegwitDerivedKeyPair.publicKey, - this.bitcoinNetwork - ); + let fundingPayment: P2Ret | P2TROut; + + switch (this.fundingPaymentType) { + case 'wpkh': + fundingPayment = p2wpkh( + this.derivedKeyPair.fundingDerivedKeyPair.publicKey, + this.bitcoinNetwork + ); + break; + case 'tr': + fundingPayment = p2tr( + ecdsaPublicKeyToSchnorr(this.derivedKeyPair.taprootDerivedKeyPair.publicKey), + undefined, + this.bitcoinNetwork + ); + break; + default: + throw new Error('Invalid Funding Payment Type'); + } - const taprootMultisigPayment = createTaprootMultisigPayment( + const multisigPayment = createTaprootMultisigPayment( unspendableDerivedPublicKey, attestorDerivedPublicKey, this.derivedKeyPair.taprootDerivedKeyPair.publicKey, this.bitcoinNetwork ); - this.setPayment(nativeSegwitPayment, taprootMultisigPayment); + this.setPayment(fundingPayment, multisigPayment); return { - nativeSegwitPayment, - taprootMultisigPayment, + fundingPayment, + multisigPayment, }; } catch (error: any) { throw new Error(`Error creating required Payment objects: ${error}`); @@ -174,16 +193,19 @@ export class PrivateKeyDLCHandler { feeRateMultiplier?: number, customFeeRate?: bigint ): Promise { - const { nativeSegwitPayment, taprootMultisigPayment } = this.createPayments( + const { fundingPayment, multisigPayment } = this.createPayments( vault.uuid, attestorGroupPublicKey ); - if (nativeSegwitPayment.address === undefined || taprootMultisigPayment.address === undefined) { - throw new Error('Could not get Addresses from Payments'); + if ([multisigPayment.address, fundingPayment.address].some(x => x === undefined)) { + throw new Error('Payment Address is undefined'); } - const addressBalance = await getBalance(nativeSegwitPayment.address, this.bitcoinBlockchainAPI); + const addressBalance = await getBalance( + fundingPayment.address as string, + this.bitcoinBlockchainAPI + ); if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { throw new Error('Insufficient Funds'); @@ -196,8 +218,8 @@ export class PrivateKeyDLCHandler { const fundingPSBT = await createFundingTransaction( vault.valueLocked.toBigInt(), this.bitcoinNetwork, - taprootMultisigPayment.address, - nativeSegwitPayment, + multisigPayment.address as string, + fundingPayment, feeRate, vault.btcFeeRecipient, vault.btcMintFeeBasisPoints.toBigInt(), @@ -217,9 +239,9 @@ export class PrivateKeyDLCHandler { throw new Error('Payment objects have not been set'); } - const { nativeSegwitPayment, taprootMultisigPayment } = this.payment; + const { fundingPayment, multisigPayment } = this.payment; - if (nativeSegwitPayment.address === undefined) { + if (fundingPayment.address === undefined) { throw new Error('Could not get Addresses from Payments'); } @@ -231,8 +253,8 @@ export class PrivateKeyDLCHandler { vault.valueLocked.toBigInt(), this.bitcoinNetwork, fundingTransactionID, - taprootMultisigPayment, - nativeSegwitPayment.address, + multisigPayment, + fundingPayment.address, feeRate, vault.btcFeeRecipient, vault.btcRedeemFeeBasisPoints.toBigInt() @@ -242,7 +264,7 @@ export class PrivateKeyDLCHandler { } signPSBT(psbt: Transaction, transactionType: 'funding' | 'closing'): Transaction { - psbt.sign(this.getPrivateKey(transactionType === 'funding' ? 'p2wpkh' : 'p2tr')); + psbt.sign(this.getPrivateKey(transactionType === 'funding' ? 'funding' : 'taproot')); if (transactionType === 'funding') psbt.finalize(); return psbt; } diff --git a/src/dlc-handlers/software-wallet-dlc-handler.ts b/src/dlc-handlers/software-wallet-dlc-handler.ts index 7908483..ea41a14 100644 --- a/src/dlc-handlers/software-wallet-dlc-handler.ts +++ b/src/dlc-handlers/software-wallet-dlc-handler.ts @@ -18,7 +18,7 @@ import { PaymentInformation } from '../models/bitcoin-models.js'; import { RawVault } from '../models/ethereum-models.js'; export class SoftwareWalletDLCHandler { - private nativeSegwitDerivedPublicKey: string; + private fundingDerivedPublicKey: string; private taprootDerivedPublicKey: string; public payment: PaymentInformation | undefined; private bitcoinNetwork: Network; @@ -59,14 +59,14 @@ export class SoftwareWalletDLCHandler { throw new Error('Invalid Bitcoin Network'); } this.bitcoinNetwork = bitcoinNetwork; - this.nativeSegwitDerivedPublicKey = nativeSegwitDerivedPublicKey; + this.fundingDerivedPublicKey = nativeSegwitDerivedPublicKey; this.taprootDerivedPublicKey = taprootDerivedPublicKey; } - private setPayment(nativeSegwitPayment: P2Ret, taprootMultisigPayment: P2TROut): void { + private setPayment(fundingPayment: P2Ret, multisigPayment: P2TROut): void { this.payment = { - nativeSegwitPayment, - taprootMultisigPayment, + fundingPayment, + multisigPayment, }; } @@ -77,7 +77,7 @@ export class SoftwareWalletDLCHandler { return this.payment; } - getVaultRelatedAddress(paymentType: 'p2wpkh' | 'p2tr'): string { + getVaultRelatedAddress(paymentType: 'funding' | 'multisig'): string { const payment = this.getPayment(); if (payment === undefined) { @@ -87,17 +87,17 @@ export class SoftwareWalletDLCHandler { let address: string; switch (paymentType) { - case 'p2wpkh': - if (!payment.nativeSegwitPayment.address) { - throw new Error('Native Segwit Payment Address is undefined'); + case 'funding': + if (!payment.fundingPayment.address) { + throw new Error('Funding Payment Address is undefined'); } - address = payment.nativeSegwitPayment.address; + address = payment.fundingPayment.address; return address; - case 'p2tr': - if (!payment.taprootMultisigPayment.address) { + case 'multisig': + if (!payment.multisigPayment.address) { throw new Error('Taproot Multisig Payment Address is undefined'); } - address = payment.taprootMultisigPayment.address; + address = payment.multisigPayment.address; return address; default: throw new Error('Invalid Payment Type'); @@ -109,8 +109,8 @@ export class SoftwareWalletDLCHandler { attestorGroupPublicKey: string ): Promise { try { - const nativeSegwitPayment = p2wpkh( - Buffer.from(this.nativeSegwitDerivedPublicKey, 'hex'), + const fundingPayment = p2wpkh( + Buffer.from(this.fundingDerivedPublicKey, 'hex'), this.bitcoinNetwork ); @@ -125,18 +125,18 @@ export class SoftwareWalletDLCHandler { this.bitcoinNetwork ); - const taprootMultisigPayment = createTaprootMultisigPayment( + const multisigPayment = createTaprootMultisigPayment( unspendableDerivedPublicKey, attestorDerivedPublicKey, Buffer.from(this.taprootDerivedPublicKey, 'hex'), this.bitcoinNetwork ); - this.setPayment(nativeSegwitPayment, taprootMultisigPayment); + this.setPayment(fundingPayment, multisigPayment); return { - nativeSegwitPayment, - taprootMultisigPayment, + fundingPayment, + multisigPayment, }; } catch (error: any) { throw new Error(`Error creating required wallet information: ${error}`); @@ -150,15 +150,12 @@ export class SoftwareWalletDLCHandler { customFeeRate?: bigint ): Promise { try { - const { nativeSegwitPayment, taprootMultisigPayment } = await this.createPayments( + const { fundingPayment, multisigPayment } = await this.createPayments( vault.uuid, attestorGroupPublicKey ); - if ( - taprootMultisigPayment.address === undefined || - nativeSegwitPayment.address === undefined - ) { + if (fundingPayment.address === undefined || multisigPayment.address === undefined) { throw new Error('Payment Address is undefined'); } @@ -166,10 +163,7 @@ export class SoftwareWalletDLCHandler { customFeeRate ?? BigInt(await getFeeRate(this.bitcoinBlockchainFeeRecommendationAPI, feeRateMultiplier)); - const addressBalance = await getBalance( - nativeSegwitPayment.address, - this.bitcoinBlockchainAPI - ); + const addressBalance = await getBalance(fundingPayment.address, this.bitcoinBlockchainAPI); if (BigInt(addressBalance) < vault.valueLocked.toBigInt()) { throw new Error('Insufficient Funds'); @@ -178,8 +172,8 @@ export class SoftwareWalletDLCHandler { const fundingPSBT = await createFundingTransaction( vault.valueLocked.toBigInt(), this.bitcoinNetwork, - taprootMultisigPayment.address, - nativeSegwitPayment, + multisigPayment.address, + fundingPayment, feeRate, vault.btcFeeRecipient, vault.btcMintFeeBasisPoints.toBigInt(), @@ -198,12 +192,9 @@ export class SoftwareWalletDLCHandler { customFeeRate?: bigint ): Promise { try { - const { nativeSegwitPayment, taprootMultisigPayment } = this.getPayment(); + const { fundingPayment, multisigPayment } = this.getPayment(); - if ( - taprootMultisigPayment.address === undefined || - nativeSegwitPayment.address === undefined - ) { + if (multisigPayment.address === undefined || fundingPayment.address === undefined) { throw new Error('Payment Address is undefined'); } @@ -215,8 +206,8 @@ export class SoftwareWalletDLCHandler { vault.valueLocked.toBigInt(), this.bitcoinNetwork, fundingTransactionID, - taprootMultisigPayment, - nativeSegwitPayment.address!, + multisigPayment, + fundingPayment.address!, feeRate, vault.btcFeeRecipient, vault.btcRedeemFeeBasisPoints.toBigInt() diff --git a/src/functions/bitcoin/bitcoin-functions.ts b/src/functions/bitcoin/bitcoin-functions.ts index acfa251..ca55d94 100644 --- a/src/functions/bitcoin/bitcoin-functions.ts +++ b/src/functions/bitcoin/bitcoin-functions.ts @@ -169,14 +169,14 @@ export async function getFeeRate( /** * Gets the UTXOs of the User's Native Segwit Address. * - * @param bitcoinNativeSegwitTransaction - The User's Native Segwit Payment Transaction. + * @param fundingPayment - The User's Funding Payment Transaction. * @returns A Promise that resolves to the UTXOs of the User's Native Segwit Address. */ export async function getUTXOs( - bitcoinNativeSegwitTransaction: P2Ret, + fundingPayment: P2Ret | P2TROut, bitcoinBlockchainAPIURL: string ): Promise { - const utxoEndpoint = `${bitcoinBlockchainAPIURL}/address/${bitcoinNativeSegwitTransaction.address}/utxo`; + const utxoEndpoint = `${bitcoinBlockchainAPIURL}/address/${fundingPayment.address}/utxo`; const utxoResponse = await fetch(utxoEndpoint); if (!utxoResponse.ok) { @@ -188,15 +188,15 @@ export async function getUTXOs( const modifiedUTXOs = await Promise.all( userUTXOs.map(async (utxo: UTXO) => { return { - ...bitcoinNativeSegwitTransaction, + ...fundingPayment, txid: utxo.txid, index: utxo.vout, value: utxo.value, witnessUtxo: { - script: bitcoinNativeSegwitTransaction.script, + script: fundingPayment.script, amount: BigInt(utxo.value), }, - redeemScript: bitcoinNativeSegwitTransaction.redeemScript, + redeemScript: fundingPayment.redeemScript, }; }) ); diff --git a/src/functions/bitcoin/psbt-functions.ts b/src/functions/bitcoin/psbt-functions.ts index 4fb468b..48e8975 100644 --- a/src/functions/bitcoin/psbt-functions.ts +++ b/src/functions/bitcoin/psbt-functions.ts @@ -29,7 +29,7 @@ export async function createFundingTransaction( bitcoinAmount: bigint, bitcoinNetwork: Network, multisigAddress: string, - bitcoinNativeSegwitTransaction: P2Ret, + fundingPayment: P2Ret | P2TROut, feeRate: bigint, feePublicKey: string, feeBasisPoints: bigint, @@ -38,7 +38,7 @@ export async function createFundingTransaction( const feeAddress = getFeeRecipientAddressFromPublicKey(feePublicKey, bitcoinNetwork); const feeAmount = getFeeAmount(Number(bitcoinAmount), Number(feeBasisPoints)); - const userUTXOs = await getUTXOs(bitcoinNativeSegwitTransaction, bitcoinBlockchainAPIURL); + const userUTXOs = await getUTXOs(fundingPayment, bitcoinBlockchainAPIURL); const psbtOutputs = [ { address: multisigAddress, amount: bitcoinAmount }, @@ -49,7 +49,7 @@ export async function createFundingTransaction( ]; const selected = selectUTXO(userUTXOs, psbtOutputs, 'default', { - changeAddress: bitcoinNativeSegwitTransaction.address!, + changeAddress: fundingPayment.address!, feePerByte: feeRate, bip69: false, createTx: true, @@ -302,7 +302,7 @@ async function addNativeSegwitBip32Derivation( export function addNativeSegwitSignaturesToPSBT( psbt: Psbt, signatures: [number, PartialSignature][] -): void { +) { signatures.forEach(([index, signature]) => psbt.updateInput(index, { partialSig: [signature] })); } @@ -313,18 +313,25 @@ export function addNativeSegwitSignaturesToPSBT( * @returns The updated PSBT. */ export function addTaprootInputSignaturesToPSBT( + psbtType: 'funding' | 'closing', psbt: Psbt, signatures: [number, PartialSignature][] ): void { - signatures.forEach(([index, signature]) => - psbt.updateInput(index, { - tapScriptSig: [ - { - signature: signature.signature, - pubkey: signature.pubkey, - leafHash: signature.tapleafHash!, - }, - ], - }) - ); + if (psbtType === 'funding') { + signatures.forEach(([index, signature]) => + psbt.updateInput(index, { tapKeySig: signature.signature }) + ); + } else { + signatures.forEach(([index, signature]) => + psbt.updateInput(index, { + tapScriptSig: [ + { + signature: signature.signature, + pubkey: signature.pubkey, + leafHash: signature.tapleafHash!, + }, + ], + }) + ); + } } diff --git a/src/models/bitcoin-models.ts b/src/models/bitcoin-models.ts index 1f0aa0c..8d1a17e 100644 --- a/src/models/bitcoin-models.ts +++ b/src/models/bitcoin-models.ts @@ -21,12 +21,12 @@ export interface FeeRates { } export interface PaymentInformation { - nativeSegwitPayment: P2Ret; - taprootMultisigPayment: P2TROut; + fundingPayment: P2Ret | P2TROut; + multisigPayment: P2TROut; } export interface ExtendedPaymentInformation extends PaymentInformation { - nativeSegwitDerivedPublicKey: Buffer; + fundingDerivedPublicKey: Buffer; taprootDerivedPublicKey: Buffer; } diff --git a/tests/mocks/constants.ts b/tests/mocks/constants.ts index 02916b8..8ef247d 100644 --- a/tests/mocks/constants.ts +++ b/tests/mocks/constants.ts @@ -13,6 +13,7 @@ export const TEST_BITCOIN_BLOCKCHAIN_API = 'https://devnet.dlc.link/electrs'; export const TEST_BITCOIN_BLOCKCHAIN_FEE_RECOMMENDATION_API = 'https://devnet.dlc.link/electrs/fee-estimates'; export const TEST_BITCOIN_AMOUNT = 0.01; +export const TEST_FUNDING_PAYMENT_TYPE = 'tr'; // Ethereum export const TEST_ETHEREUM_PRIVATE_KEY = ''; diff --git a/tests/unit/sign-transactions.test.ts b/tests/unit/sign-transactions.test.ts index 64f8a5e..66554c9 100644 --- a/tests/unit/sign-transactions.test.ts +++ b/tests/unit/sign-transactions.test.ts @@ -7,6 +7,7 @@ import { TEST_BITCOIN_EXTENDED_PRIVATE_KEY, TEST_BITCOIN_NETWORK, TEST_BITCOIN_WALLET_ACCOUNT_INDEX, + TEST_FUNDING_PAYMENT_TYPE, TEST_REGTEST_ATTESTOR_EXTENDED_GROUP_PUBLIC_KEY, TEST_VAULT, } from '../mocks/constants.js'; @@ -22,6 +23,7 @@ describe('Create and Sign Vault related Transactions', () => { dlcHandler = new PrivateKeyDLCHandler( TEST_BITCOIN_EXTENDED_PRIVATE_KEY, TEST_BITCOIN_WALLET_ACCOUNT_INDEX, + TEST_FUNDING_PAYMENT_TYPE, TEST_BITCOIN_NETWORK, TEST_BITCOIN_BLOCKCHAIN_API, TEST_BITCOIN_BLOCKCHAIN_FEE_RECOMMENDATION_API @@ -41,7 +43,7 @@ describe('Create and Sign Vault related Transactions', () => { Buffer.from(TEST_VAULT.btcFeeRecipient, 'hex'), TEST_BITCOIN_NETWORK ).script; - const multisigScript = dlcHandler.payment?.taprootMultisigPayment.script; + const multisigScript = dlcHandler.payment?.multisigPayment.script; const outputs = Array.from({ length: fundingTransaction.outputsLength }, (_, index) => fundingTransaction.getOutput(index) @@ -78,7 +80,7 @@ describe('Create and Sign Vault related Transactions', () => { Buffer.from(TEST_VAULT.btcFeeRecipient, 'hex'), TEST_BITCOIN_NETWORK ).script; - const userScript = dlcHandler.payment?.nativeSegwitPayment.script; + const userScript = dlcHandler.payment?.fundingPayment.script; const outputs = Array.from({ length: fundingTransaction.outputsLength }, (_, index) => fundingTransaction.getOutput(index) @@ -94,7 +96,7 @@ describe('Create and Sign Vault related Transactions', () => { expect(feeOutput?.amount === feeAmount).toBeTruthy(); expect( closingTransaction.getInput(0).witnessUtxo?.script.toString() == - dlcHandler.payment?.taprootMultisigPayment.script.toString() + dlcHandler.payment?.multisigPayment.script.toString() ).toBeTruthy(); }); From e8654509e89edb617f3482482521392a2efdc42b Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 1 Jul 2024 11:34:32 +0200 Subject: [PATCH 2/2] fix: payment creation in withdrawal flow --- src/dlc-handlers/software-wallet-dlc-handler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dlc-handlers/software-wallet-dlc-handler.ts b/src/dlc-handlers/software-wallet-dlc-handler.ts index ec0c759..becb107 100644 --- a/src/dlc-handlers/software-wallet-dlc-handler.ts +++ b/src/dlc-handlers/software-wallet-dlc-handler.ts @@ -200,7 +200,10 @@ export class SoftwareWalletDLCHandler { customFeeRate?: bigint ): Promise { try { - const { fundingPayment, multisigPayment } = this.getPayment(); + const { fundingPayment, multisigPayment } = await this.createPayments( + vault.uuid, + attestorGroupPublicKey + ); if (multisigPayment.address === undefined || fundingPayment.address === undefined) { throw new Error('Payment Address is undefined');