From dc7e337608b77d8611e78d5385d61874287ad43a Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Tue, 30 Jan 2024 00:06:35 +0200 Subject: [PATCH 1/6] add native segwit support for ledger message signing --- ledger/btc.ts | 88 +------------------- ledger/btcMessageSigning.ts | 159 ++++++++++++++++++++++++++++++++++++ ledger/index.ts | 2 + 3 files changed, 162 insertions(+), 87 deletions(-) create mode 100644 ledger/btcMessageSigning.ts diff --git a/ledger/btc.ts b/ledger/btc.ts index caa67fa0..3e0b3a3a 100644 --- a/ledger/btc.ts +++ b/ledger/btc.ts @@ -1,9 +1,7 @@ import { getAddressInfo } from 'bitcoin-address-validation'; -import { Psbt, Transaction } from 'bitcoinjs-lib'; +import { Psbt } from 'bitcoinjs-lib'; import { AppClient, DefaultWalletPolicy } from 'ledger-bitcoin'; -import { encode } from 'varuint-bitcoin'; import EsploraApiProvider from '../api/esplora/esploraAPiProvider'; -import { bip0322Hash } from '../connect/bip322Signature'; import { BTC_SEGWIT_PATH_PURPOSE, BTC_TAPROOT_PATH_PURPOSE } from '../constant'; import { Recipient } from '../transactions/btc'; import { InputToSign } from '../transactions/psbt'; @@ -479,87 +477,3 @@ export async function signIncomingSingleSigPSBT({ return psbt.toBase64(); } - -/** - * This function is used to sign an incoming BIP 322 message with the ledger - * @param transport - the transport object with connected ledger device - * @param networkType - the network type (Mainnet or Testnet) - * @param addressIndex - the index of the account address to sign with - * @param message - the incoming message in string format to sign - * @returns the signature in string (base64) format - * */ -export async function signSimpleBip322Message({ - transport, - networkType, - addressIndex, - message, -}: { - transport: Transport; - networkType: NetworkType; - addressIndex: number; - message: string; -}) { - const app = new AppClient(transport); - const coinType = getCoinType(networkType); - - // Get account details from ledger to not rely on state - const masterFingerPrint = await app.getMasterFingerprint(); - const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`); - const accountPolicy = new DefaultWalletPolicy( - 'tr(@0/**)', - `[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`, - ); - - const { internalPubkey, taprootScript } = getTaprootAccountDataFromXpub(extendedPublicKey, addressIndex, networkType); - - const prevoutHash = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); - const prevoutIndex = 0xffffffff; - const sequence = 0; - const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); - - const txToSpend = new Transaction(); - txToSpend.version = 0; - txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); - txToSpend.addOutput(taprootScript, 0); - - // Need to update input derivation path so the ledger can recognize the inputs to sign - const inputDerivation: TapBip32Derivation = { - path: `${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, - pubkey: internalPubkey, - masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), - leafHashes: [], - }; - - const psbtToSign = new Psbt(); - psbtToSign.setVersion(0); - psbtToSign.addInput({ - hash: txToSpend.getHash(), - index: 0, - sequence: 0, - tapBip32Derivation: [inputDerivation], - tapInternalKey: internalPubkey, - witnessUtxo: { - script: taprootScript, - value: 0, - }, - }); - psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); - - const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); - for (const signature of signatures) { - psbtToSign.updateInput(signature[0], { - tapKeySig: signature[1].signature, - }); - } - - psbtToSign.finalizeAllInputs(); - const txToSign = psbtToSign.extractTransaction(); - - const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]); - - const len = encode(txToSign.ins[0].witness.length); - const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); - - const signature = result.toString('base64'); - return signature; -} diff --git a/ledger/btcMessageSigning.ts b/ledger/btcMessageSigning.ts new file mode 100644 index 00000000..d47d9bf1 --- /dev/null +++ b/ledger/btcMessageSigning.ts @@ -0,0 +1,159 @@ +import AppClient, { DefaultWalletPolicy } from 'ledger-bitcoin'; +import { NetworkType } from '../types'; +import { Bip32Derivation, TapBip32Derivation, Transport } from './types'; +import { getCoinType, getNativeSegwitAccountDataFromXpub, getTaprootAccountDataFromXpub } from './helper'; +import { AddressType, getAddressInfo } from 'bitcoin-address-validation'; +import { BTC_SEGWIT_PATH_PURPOSE, BTC_TAPROOT_PATH_PURPOSE } from '../constant'; +import { encode } from 'varuint-bitcoin'; + +import { bip0322Hash } from '../connect'; +import { Psbt, Transaction } from 'bitcoinjs-lib'; + +/** + * This function is used to sign an incoming BIP 322 message with the ledger + * @param transport - the transport object with connected ledger device + * @param networkType - the network type (Mainnet or Testnet) + * @param addressIndex - the index of the account address to sign with + * @param address - the address used to sign the message + * @param message - the incoming message in string format to sign + * @returns the signature in string (base64) format + * */ +export async function signSimpleBip322Message({ + transport, + networkType, + addressIndex, + address, + message, +}: { + transport: Transport; + networkType: NetworkType; + addressIndex: number; + address: string; + message: string; +}) { + const app = new AppClient(transport); + const coinType = getCoinType(networkType); + const { type } = getAddressInfo(address); + const prevoutHash = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); + const prevoutIndex = 0xffffffff; + const sequence = 0; + const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); + // Get account details from ledger to not rely on state + const masterFingerPrint = await app.getMasterFingerprint(); + if (type === AddressType.p2tr) { + const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`); + const accountPolicy = new DefaultWalletPolicy( + 'tr(@0/**)', + `[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`, + ); + + const { internalPubkey, taprootScript } = getTaprootAccountDataFromXpub( + extendedPublicKey, + addressIndex, + networkType, + ); + + const txToSpend = new Transaction(); + txToSpend.version = 0; + txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); + txToSpend.addOutput(taprootScript, 0); + + // Need to update input derivation path so the ledger can recognize the inputs to sign + const inputDerivation: TapBip32Derivation = { + path: `${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, + pubkey: internalPubkey, + masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), + leafHashes: [], + }; + + const psbtToSign = new Psbt(); + psbtToSign.setVersion(0); + psbtToSign.addInput({ + hash: txToSpend.getHash(), + index: 0, + sequence: 0, + tapBip32Derivation: [inputDerivation], + tapInternalKey: internalPubkey, + witnessUtxo: { + script: taprootScript, + value: 0, + }, + }); + psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); + + const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); + for (const signature of signatures) { + psbtToSign.updateInput(signature[0], { + tapKeySig: signature[1].signature, + }); + } + + psbtToSign.finalizeAllInputs(); + const txToSign = psbtToSign.extractTransaction(); + + const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]); + + const len = encode(txToSign.ins[0].witness.length); + const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); + + const signature = result.toString('base64'); + return signature; + } else if (type === AddressType.p2wpkh) { + const extendedPublicKey = await app.getExtendedPubkey(`${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'`); + const accountPolicy = new DefaultWalletPolicy( + 'wpkh(@0/**)', + `[${masterFingerPrint}/84'/${coinType}'/0']${extendedPublicKey}`, + ); + + const { publicKey, witnessScript } = getNativeSegwitAccountDataFromXpub( + extendedPublicKey, + addressIndex, + networkType, + ); + + const inputDerivation: Bip32Derivation = { + path: `${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, + pubkey: publicKey, + masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), + }; + + const txToSpend = new Transaction(); + txToSpend.version = 0; + txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); + txToSpend.addOutput(witnessScript, 0); + + const psbtToSign = new Psbt(); + psbtToSign.setVersion(0); + psbtToSign.addInput({ + hash: txToSpend.getHash(), + index: 0, + sequence: 0, + bip32Derivation: [inputDerivation], + witnessUtxo: { + script: witnessScript, + value: 0, + }, + }); + psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); + + const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); + for (const signature of signatures) { + psbtToSign.updateInput(signature[0], { + partialSig: [signature[1]], + }); + } + + psbtToSign.finalizeAllInputs(); + const txToSign = psbtToSign.extractTransaction(); + + const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]); + + const len = encode(txToSign.ins[0].witness.length); + const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); + + const signature = result.toString('base64'); + return signature; + } + + throw new Error('Invalid Address Type'); +} diff --git a/ledger/index.ts b/ledger/index.ts index 657221b3..06c17fb9 100644 --- a/ledger/index.ts +++ b/ledger/index.ts @@ -1,6 +1,8 @@ export * from './btc'; export * from './psbt'; export * from './stx'; +export * from './stx'; +export * from './btcMessageSigning'; export * from './types'; export { getMasterFingerPrint } from './helper'; From cf6eac15b25cd131b33e8aba0fbb937c654e17b0 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Tue, 30 Jan 2024 16:09:57 +0200 Subject: [PATCH 2/6] re-arrange signature utils --- ledger/btcMessageSigning.ts | 264 ++++++++++++++++++++---------------- ledger/index.ts | 1 - 2 files changed, 145 insertions(+), 120 deletions(-) diff --git a/ledger/btcMessageSigning.ts b/ledger/btcMessageSigning.ts index d47d9bf1..65deec58 100644 --- a/ledger/btcMessageSigning.ts +++ b/ledger/btcMessageSigning.ts @@ -9,6 +9,145 @@ import { encode } from 'varuint-bitcoin'; import { bip0322Hash } from '../connect'; import { Psbt, Transaction } from 'bitcoinjs-lib'; +const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]); + +const createSegwitBip322Signature = async ({ + message, + app, + addressIndex, + networkType, +}: { + message: string; + app: AppClient; + addressIndex: number; + networkType: NetworkType; +}): Promise => { + const coinType = getCoinType(networkType); + const masterFingerPrint = await app.getMasterFingerprint(); + const prevoutHash = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); + const prevoutIndex = 0xffffffff; + const sequence = 0; + const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); + const extendedPublicKey = await app.getExtendedPubkey(`${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'`); + const accountPolicy = new DefaultWalletPolicy( + 'wpkh(@0/**)', + `[${masterFingerPrint}/84'/${coinType}'/0']${extendedPublicKey}`, + ); + + const { publicKey, witnessScript } = getNativeSegwitAccountDataFromXpub(extendedPublicKey, addressIndex, networkType); + + const inputDerivation: Bip32Derivation = { + path: `${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, + pubkey: publicKey, + masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), + }; + + const txToSpend = new Transaction(); + txToSpend.version = 0; + txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); + txToSpend.addOutput(witnessScript, 0); + + const psbtToSign = new Psbt(); + psbtToSign.setVersion(0); + psbtToSign.addInput({ + hash: txToSpend.getHash(), + index: 0, + sequence: 0, + bip32Derivation: [inputDerivation], + witnessUtxo: { + script: witnessScript, + value: 0, + }, + }); + psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); + + const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); + for (const signature of signatures) { + psbtToSign.updateInput(signature[0], { + partialSig: [signature[1]], + }); + } + + psbtToSign.finalizeAllInputs(); + const txToSign = psbtToSign.extractTransaction(); + + const len = encode(txToSign.ins[0].witness.length); + const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); + + const signature = result.toString('base64'); + return signature; +}; + +const createTaprootBip322Signature = async ({ + message, + app, + addressIndex, + networkType, +}: { + message: string; + app: AppClient; + addressIndex: number; + networkType: NetworkType; +}): Promise => { + const coinType = getCoinType(networkType); + const masterFingerPrint = await app.getMasterFingerprint(); + const prevoutHash = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); + const prevoutIndex = 0xffffffff; + const sequence = 0; + const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); + const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`); + const accountPolicy = new DefaultWalletPolicy( + 'tr(@0/**)', + `[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`, + ); + + const { internalPubkey, taprootScript } = getTaprootAccountDataFromXpub(extendedPublicKey, addressIndex, networkType); + + const txToSpend = new Transaction(); + txToSpend.version = 0; + txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); + txToSpend.addOutput(taprootScript, 0); + + // Need to update input derivation path so the ledger can recognize the inputs to sign + const inputDerivation: TapBip32Derivation = { + path: `${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, + pubkey: internalPubkey, + masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), + leafHashes: [], + }; + + const psbtToSign = new Psbt(); + psbtToSign.setVersion(0); + psbtToSign.addInput({ + hash: txToSpend.getHash(), + index: 0, + sequence: 0, + tapBip32Derivation: [inputDerivation], + tapInternalKey: internalPubkey, + witnessUtxo: { + script: taprootScript, + value: 0, + }, + }); + psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); + + const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); + for (const signature of signatures) { + psbtToSign.updateInput(signature[0], { + tapKeySig: signature[1].signature, + }); + } + + psbtToSign.finalizeAllInputs(); + const txToSign = psbtToSign.extractTransaction(); + + const len = encode(txToSign.ins[0].witness.length); + const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); + + const signature = result.toString('base64'); + return signature; +}; + /** * This function is used to sign an incoming BIP 322 message with the ledger * @param transport - the transport object with connected ledger device @@ -30,129 +169,16 @@ export async function signSimpleBip322Message({ addressIndex: number; address: string; message: string; -}) { +}): Promise { const app = new AppClient(transport); - const coinType = getCoinType(networkType); - const { type } = getAddressInfo(address); - const prevoutHash = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); - const prevoutIndex = 0xffffffff; - const sequence = 0; - const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); - // Get account details from ledger to not rely on state - const masterFingerPrint = await app.getMasterFingerprint(); - if (type === AddressType.p2tr) { - const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`); - const accountPolicy = new DefaultWalletPolicy( - 'tr(@0/**)', - `[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`, - ); - - const { internalPubkey, taprootScript } = getTaprootAccountDataFromXpub( - extendedPublicKey, - addressIndex, - networkType, - ); - - const txToSpend = new Transaction(); - txToSpend.version = 0; - txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); - txToSpend.addOutput(taprootScript, 0); - - // Need to update input derivation path so the ledger can recognize the inputs to sign - const inputDerivation: TapBip32Derivation = { - path: `${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, - pubkey: internalPubkey, - masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), - leafHashes: [], - }; - - const psbtToSign = new Psbt(); - psbtToSign.setVersion(0); - psbtToSign.addInput({ - hash: txToSpend.getHash(), - index: 0, - sequence: 0, - tapBip32Derivation: [inputDerivation], - tapInternalKey: internalPubkey, - witnessUtxo: { - script: taprootScript, - value: 0, - }, - }); - psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); - - const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); - for (const signature of signatures) { - psbtToSign.updateInput(signature[0], { - tapKeySig: signature[1].signature, - }); - } - psbtToSign.finalizeAllInputs(); - const txToSign = psbtToSign.extractTransaction(); - - const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]); - - const len = encode(txToSign.ins[0].witness.length); - const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); + const { type } = getAddressInfo(address); + console.log('🚀 ~ type:', type); - const signature = result.toString('base64'); - return signature; + if (type === AddressType.p2tr) { + return createTaprootBip322Signature({ message, app, addressIndex, networkType }); } else if (type === AddressType.p2wpkh) { - const extendedPublicKey = await app.getExtendedPubkey(`${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'`); - const accountPolicy = new DefaultWalletPolicy( - 'wpkh(@0/**)', - `[${masterFingerPrint}/84'/${coinType}'/0']${extendedPublicKey}`, - ); - - const { publicKey, witnessScript } = getNativeSegwitAccountDataFromXpub( - extendedPublicKey, - addressIndex, - networkType, - ); - - const inputDerivation: Bip32Derivation = { - path: `${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, - pubkey: publicKey, - masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), - }; - - const txToSpend = new Transaction(); - txToSpend.version = 0; - txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); - txToSpend.addOutput(witnessScript, 0); - - const psbtToSign = new Psbt(); - psbtToSign.setVersion(0); - psbtToSign.addInput({ - hash: txToSpend.getHash(), - index: 0, - sequence: 0, - bip32Derivation: [inputDerivation], - witnessUtxo: { - script: witnessScript, - value: 0, - }, - }); - psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); - - const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); - for (const signature of signatures) { - psbtToSign.updateInput(signature[0], { - partialSig: [signature[1]], - }); - } - - psbtToSign.finalizeAllInputs(); - const txToSign = psbtToSign.extractTransaction(); - - const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]); - - const len = encode(txToSign.ins[0].witness.length); - const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); - - const signature = result.toString('base64'); - return signature; + return createSegwitBip322Signature({ message, app, addressIndex, networkType }); } throw new Error('Invalid Address Type'); diff --git a/ledger/index.ts b/ledger/index.ts index 06c17fb9..1596d1f5 100644 --- a/ledger/index.ts +++ b/ledger/index.ts @@ -1,7 +1,6 @@ export * from './btc'; export * from './psbt'; export * from './stx'; -export * from './stx'; export * from './btcMessageSigning'; export * from './types'; From e603a773ce7bfdc600d6a273d67a15348cbc87b6 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Tue, 30 Jan 2024 16:21:36 +0200 Subject: [PATCH 3/6] remove console log --- ledger/btcMessageSigning.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/ledger/btcMessageSigning.ts b/ledger/btcMessageSigning.ts index 65deec58..0c72925f 100644 --- a/ledger/btcMessageSigning.ts +++ b/ledger/btcMessageSigning.ts @@ -11,6 +11,10 @@ import { Psbt, Transaction } from 'bitcoinjs-lib'; const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]); +const DUMMY_INPUT_HASH = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); +const DUMMY_INPUT_INDEX = 0xffffffff; +const DUMMY_INPUT_SEQUENCE = 0; + const createSegwitBip322Signature = async ({ message, app, @@ -24,9 +28,6 @@ const createSegwitBip322Signature = async ({ }): Promise => { const coinType = getCoinType(networkType); const masterFingerPrint = await app.getMasterFingerprint(); - const prevoutHash = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); - const prevoutIndex = 0xffffffff; - const sequence = 0; const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); const extendedPublicKey = await app.getExtendedPubkey(`${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'`); const accountPolicy = new DefaultWalletPolicy( @@ -44,7 +45,7 @@ const createSegwitBip322Signature = async ({ const txToSpend = new Transaction(); txToSpend.version = 0; - txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); + txToSpend.addInput(DUMMY_INPUT_HASH, DUMMY_INPUT_INDEX, DUMMY_INPUT_SEQUENCE, scriptSig); txToSpend.addOutput(witnessScript, 0); const psbtToSign = new Psbt(); @@ -91,9 +92,6 @@ const createTaprootBip322Signature = async ({ }): Promise => { const coinType = getCoinType(networkType); const masterFingerPrint = await app.getMasterFingerprint(); - const prevoutHash = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); - const prevoutIndex = 0xffffffff; - const sequence = 0; const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`); const accountPolicy = new DefaultWalletPolicy( @@ -105,7 +103,7 @@ const createTaprootBip322Signature = async ({ const txToSpend = new Transaction(); txToSpend.version = 0; - txToSpend.addInput(prevoutHash, prevoutIndex, sequence, scriptSig); + txToSpend.addInput(DUMMY_INPUT_HASH, DUMMY_INPUT_INDEX, DUMMY_INPUT_SEQUENCE, scriptSig); txToSpend.addOutput(taprootScript, 0); // Need to update input derivation path so the ledger can recognize the inputs to sign @@ -173,7 +171,6 @@ export async function signSimpleBip322Message({ const app = new AppClient(transport); const { type } = getAddressInfo(address); - console.log('🚀 ~ type:', type); if (type === AddressType.p2tr) { return createTaprootBip322Signature({ message, app, addressIndex, networkType }); From cf0b8944c1444eef97cfc6168c2d234b6e6ddd25 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Wed, 31 Jan 2024 18:29:31 +0200 Subject: [PATCH 4/6] re-arrange methods --- ledger/btcMessageSigning.ts | 165 +++++++++++------------------------- 1 file changed, 48 insertions(+), 117 deletions(-) diff --git a/ledger/btcMessageSigning.ts b/ledger/btcMessageSigning.ts index 0c72925f..4fa1fe82 100644 --- a/ledger/btcMessageSigning.ts +++ b/ledger/btcMessageSigning.ts @@ -1,85 +1,58 @@ import AppClient, { DefaultWalletPolicy } from 'ledger-bitcoin'; +import { encode } from 'varuint-bitcoin'; +import { BTC_SEGWIT_PATH_PURPOSE, BTC_TAPROOT_PATH_PURPOSE } from '../constant'; import { NetworkType } from '../types'; -import { Bip32Derivation, TapBip32Derivation, Transport } from './types'; import { getCoinType, getNativeSegwitAccountDataFromXpub, getTaprootAccountDataFromXpub } from './helper'; -import { AddressType, getAddressInfo } from 'bitcoin-address-validation'; -import { BTC_SEGWIT_PATH_PURPOSE, BTC_TAPROOT_PATH_PURPOSE } from '../constant'; -import { encode } from 'varuint-bitcoin'; - -import { bip0322Hash } from '../connect'; +import { Bip32Derivation, TapBip32Derivation, Transport } from './types'; import { Psbt, Transaction } from 'bitcoinjs-lib'; +import { bip0322Hash } from '../connect'; -const encodeVarString = (b: any) => Buffer.concat([encode(b.byteLength), b]); - +const encodeVarString = (b: Buffer) => Buffer.concat([encode(b.byteLength), b]); const DUMMY_INPUT_HASH = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); const DUMMY_INPUT_INDEX = 0xffffffff; const DUMMY_INPUT_SEQUENCE = 0; - -const createSegwitBip322Signature = async ({ - message, - app, - addressIndex, - networkType, -}: { - message: string; - app: AppClient; - addressIndex: number; - networkType: NetworkType; -}): Promise => { - const coinType = getCoinType(networkType); - const masterFingerPrint = await app.getMasterFingerprint(); +type PsbtInput = Parameters[0]; + +const createMessageSignature = async ( + app: AppClient, + accountPolicy: DefaultWalletPolicy, + message: string, + witnessScript: Buffer, + inputArgs: Pick | Pick, +): Promise => { const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); - const extendedPublicKey = await app.getExtendedPubkey(`${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'`); - const accountPolicy = new DefaultWalletPolicy( - 'wpkh(@0/**)', - `[${masterFingerPrint}/84'/${coinType}'/0']${extendedPublicKey}`, - ); - - const { publicKey, witnessScript } = getNativeSegwitAccountDataFromXpub(extendedPublicKey, addressIndex, networkType); - - const inputDerivation: Bip32Derivation = { - path: `${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, - pubkey: publicKey, - masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), - }; - const txToSpend = new Transaction(); txToSpend.version = 0; txToSpend.addInput(DUMMY_INPUT_HASH, DUMMY_INPUT_INDEX, DUMMY_INPUT_SEQUENCE, scriptSig); txToSpend.addOutput(witnessScript, 0); - const psbtToSign = new Psbt(); psbtToSign.setVersion(0); psbtToSign.addInput({ hash: txToSpend.getHash(), index: 0, sequence: 0, - bip32Derivation: [inputDerivation], witnessUtxo: { script: witnessScript, value: 0, }, + ...inputArgs, }); psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); - const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); for (const signature of signatures) { psbtToSign.updateInput(signature[0], { partialSig: [signature[1]], }); } - psbtToSign.finalizeAllInputs(); const txToSign = psbtToSign.extractTransaction(); - const len = encode(txToSign.ins[0].witness.length); const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); - const signature = result.toString('base64'); return signature; }; -const createTaprootBip322Signature = async ({ +export const createSegwitBip322Signature = async ({ message, app, addressIndex, @@ -92,20 +65,36 @@ const createTaprootBip322Signature = async ({ }): Promise => { const coinType = getCoinType(networkType); const masterFingerPrint = await app.getMasterFingerprint(); - const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); - const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`); + const extendedPublicKey = await app.getExtendedPubkey(`${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'`); + const { publicKey, witnessScript } = getNativeSegwitAccountDataFromXpub(extendedPublicKey, addressIndex, networkType); + const inputDerivation: Bip32Derivation = { + path: `${BTC_SEGWIT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, + pubkey: publicKey, + masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), + }; const accountPolicy = new DefaultWalletPolicy( - 'tr(@0/**)', - `[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`, + 'wpkh(@0/**)', + `[${masterFingerPrint}/84'/${coinType}'/0']${extendedPublicKey}`, ); - + return createMessageSignature(app, accountPolicy, message, witnessScript, { + bip32Derivation: [inputDerivation], + }); +}; +export const createTaprootBip322Signature = async ({ + message, + app, + addressIndex, + networkType, +}: { + message: string; + app: AppClient; + addressIndex: number; + networkType: NetworkType; +}): Promise => { + const coinType = getCoinType(networkType); + const masterFingerPrint = await app.getMasterFingerprint(); + const extendedPublicKey = await app.getExtendedPubkey(`${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'`); const { internalPubkey, taprootScript } = getTaprootAccountDataFromXpub(extendedPublicKey, addressIndex, networkType); - - const txToSpend = new Transaction(); - txToSpend.version = 0; - txToSpend.addInput(DUMMY_INPUT_HASH, DUMMY_INPUT_INDEX, DUMMY_INPUT_SEQUENCE, scriptSig); - txToSpend.addOutput(taprootScript, 0); - // Need to update input derivation path so the ledger can recognize the inputs to sign const inputDerivation: TapBip32Derivation = { path: `${BTC_TAPROOT_PATH_PURPOSE}${coinType}'/0'/0/${addressIndex}`, @@ -113,70 +102,12 @@ const createTaprootBip322Signature = async ({ masterFingerprint: Buffer.from(masterFingerPrint, 'hex'), leafHashes: [], }; - - const psbtToSign = new Psbt(); - psbtToSign.setVersion(0); - psbtToSign.addInput({ - hash: txToSpend.getHash(), - index: 0, - sequence: 0, + const accountPolicy = new DefaultWalletPolicy( + 'tr(@0/**)', + `[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`, + ); + return createMessageSignature(app, accountPolicy, message, taprootScript, { tapBip32Derivation: [inputDerivation], tapInternalKey: internalPubkey, - witnessUtxo: { - script: taprootScript, - value: 0, - }, }); - psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); - - const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); - for (const signature of signatures) { - psbtToSign.updateInput(signature[0], { - tapKeySig: signature[1].signature, - }); - } - - psbtToSign.finalizeAllInputs(); - const txToSign = psbtToSign.extractTransaction(); - - const len = encode(txToSign.ins[0].witness.length); - const result = Buffer.concat([len, ...txToSign.ins[0].witness.map((w) => encodeVarString(w))]); - - const signature = result.toString('base64'); - return signature; }; - -/** - * This function is used to sign an incoming BIP 322 message with the ledger - * @param transport - the transport object with connected ledger device - * @param networkType - the network type (Mainnet or Testnet) - * @param addressIndex - the index of the account address to sign with - * @param address - the address used to sign the message - * @param message - the incoming message in string format to sign - * @returns the signature in string (base64) format - * */ -export async function signSimpleBip322Message({ - transport, - networkType, - addressIndex, - address, - message, -}: { - transport: Transport; - networkType: NetworkType; - addressIndex: number; - address: string; - message: string; -}): Promise { - const app = new AppClient(transport); - - const { type } = getAddressInfo(address); - - if (type === AddressType.p2tr) { - return createTaprootBip322Signature({ message, app, addressIndex, networkType }); - } else if (type === AddressType.p2wpkh) { - return createSegwitBip322Signature({ message, app, addressIndex, networkType }); - } - - throw new Error('Invalid Address Type'); -} From 90d97737840f0ae8b5a5a3b82b5376d48c746545 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Thu, 1 Feb 2024 16:29:31 +0200 Subject: [PATCH 5/6] added generic wrapper util --- ledger/btcMessageSigning.ts | 41 +++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/ledger/btcMessageSigning.ts b/ledger/btcMessageSigning.ts index 4fa1fe82..1f607663 100644 --- a/ledger/btcMessageSigning.ts +++ b/ledger/btcMessageSigning.ts @@ -1,11 +1,12 @@ import AppClient, { DefaultWalletPolicy } from 'ledger-bitcoin'; +import { AddressType, getAddressInfo } from 'bitcoin-address-validation'; +import { Psbt, Transaction } from 'bitcoinjs-lib'; import { encode } from 'varuint-bitcoin'; import { BTC_SEGWIT_PATH_PURPOSE, BTC_TAPROOT_PATH_PURPOSE } from '../constant'; +import { bip0322Hash } from '../connect'; import { NetworkType } from '../types'; import { getCoinType, getNativeSegwitAccountDataFromXpub, getTaprootAccountDataFromXpub } from './helper'; import { Bip32Derivation, TapBip32Derivation, Transport } from './types'; -import { Psbt, Transaction } from 'bitcoinjs-lib'; -import { bip0322Hash } from '../connect'; const encodeVarString = (b: Buffer) => Buffer.concat([encode(b.byteLength), b]); const DUMMY_INPUT_HASH = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'); @@ -52,7 +53,7 @@ const createMessageSignature = async ( return signature; }; -export const createSegwitBip322Signature = async ({ +const createSegwitBip322Signature = async ({ message, app, addressIndex, @@ -80,7 +81,8 @@ export const createSegwitBip322Signature = async ({ bip32Derivation: [inputDerivation], }); }; -export const createTaprootBip322Signature = async ({ + +const createTaprootBip322Signature = async ({ message, app, addressIndex, @@ -111,3 +113,34 @@ export const createTaprootBip322Signature = async ({ tapInternalKey: internalPubkey, }); }; + +/** + * This function is used to sign an incoming BIP 322 message with the ledger + * @param transport - the transport object with connected ledger device + * @param networkType - the network type (Mainnet or Testnet) + * @param addressIndex - the index of the account address to sign with + * @param message - the incoming message in string format to sign + * @returns the signature in string (base64) format + * */ +export async function signSimpleBip322Message({ + transport, + networkType, + addressIndex, + address, + message, +}: { + transport: Transport; + networkType: NetworkType; + addressIndex: number; + address: string; + message: string; +}) { + const app = new AppClient(transport); + const { type } = getAddressInfo(address); + if (type === AddressType.p2tr) { + return createTaprootBip322Signature({ message, app, addressIndex, networkType }); + } else if (type === AddressType.p2wpkh) { + return createSegwitBip322Signature({ message, app, addressIndex, networkType }); + } + throw new Error('Invalid Address Type'); +} From f60736fe5d8f3aea406ce81c67de54ab13a8a704 Mon Sep 17 00:00:00 2001 From: Mahmoud Aboelenein Date: Fri, 2 Feb 2024 13:51:52 +0200 Subject: [PATCH 6/6] update inputs according to address types --- ledger/btcMessageSigning.ts | 41 ++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/ledger/btcMessageSigning.ts b/ledger/btcMessageSigning.ts index 1f607663..f883e2cd 100644 --- a/ledger/btcMessageSigning.ts +++ b/ledger/btcMessageSigning.ts @@ -20,6 +20,7 @@ const createMessageSignature = async ( message: string, witnessScript: Buffer, inputArgs: Pick | Pick, + isSegwit: boolean, ): Promise => { const scriptSig = Buffer.concat([Buffer.from('0020', 'hex'), Buffer.from(bip0322Hash(message), 'hex')]); const txToSpend = new Transaction(); @@ -41,9 +42,15 @@ const createMessageSignature = async ( psbtToSign.addOutput({ script: Buffer.from('6a', 'hex'), value: 0 }); const signatures = await app.signPsbt(psbtToSign.toBase64(), accountPolicy, null); for (const signature of signatures) { - psbtToSign.updateInput(signature[0], { - partialSig: [signature[1]], - }); + if (isSegwit) { + psbtToSign.updateInput(signature[0], { + partialSig: [signature[1]], + }); + } else { + psbtToSign.updateInput(signature[0], { + tapKeySig: signature[1].signature, + }); + } } psbtToSign.finalizeAllInputs(); const txToSign = psbtToSign.extractTransaction(); @@ -77,9 +84,16 @@ const createSegwitBip322Signature = async ({ 'wpkh(@0/**)', `[${masterFingerPrint}/84'/${coinType}'/0']${extendedPublicKey}`, ); - return createMessageSignature(app, accountPolicy, message, witnessScript, { - bip32Derivation: [inputDerivation], - }); + return createMessageSignature( + app, + accountPolicy, + message, + witnessScript, + { + bip32Derivation: [inputDerivation], + }, + true, + ); }; const createTaprootBip322Signature = async ({ @@ -108,10 +122,17 @@ const createTaprootBip322Signature = async ({ 'tr(@0/**)', `[${masterFingerPrint}/86'/${coinType}'/0']${extendedPublicKey}`, ); - return createMessageSignature(app, accountPolicy, message, taprootScript, { - tapBip32Derivation: [inputDerivation], - tapInternalKey: internalPubkey, - }); + return createMessageSignature( + app, + accountPolicy, + message, + taprootScript, + { + tapBip32Derivation: [inputDerivation], + tapInternalKey: internalPubkey, + }, + false, + ); }; /**