Skip to content

Commit

Permalink
refactor: bitcoin methods
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Jun 7, 2024
1 parent 974aa25 commit 26ef673
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 63 deletions.
2 changes: 1 addition & 1 deletion packages/bitcoin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@stacks/common": "6.13.0",
"@stacks/transactions": "6.15.0",
"bip32": "4.0.0",
"bitcoinjs-lib": "6.1.3",
"bitcoinjs-lib": "6.1.5",
"ecpair": "2.1.0",
"varuint-bitcoin": "1.1.2"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/bitcoin/src/bip322/bip322-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import ecc from '@bitcoinerlab/secp256k1';
import { sha256 } from '@noble/hashes/sha256';
import { hexToBytes, utf8ToBytes } from '@stacks/common';
import * as bitcoin from 'bitcoinjs-lib';
import { ECPairFactory } from 'ecpair';
import ECPairFactory from 'ecpair';
import { encode } from 'varuint-bitcoin';

import type { PaymentTypes } from '@leather-wallet/rpc';
import { PaymentTypes } from '@leather-wallet/rpc';
import { isString } from '@leather-wallet/utils';

import { toXOnly } from '../bitcoin.utils';
Expand Down
15 changes: 9 additions & 6 deletions packages/bitcoin/src/bip322/sign-message-bip322-bitcoinjs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { base64 } from '@scure/base';
import * as btc from '@scure/btc-signer';
import * as bitcoin from 'bitcoinjs-lib';

import { BitcoinNetworkModes } from '@leather-wallet/models';
Expand Down Expand Up @@ -44,7 +45,7 @@ function createToSignTx(toSpendTxHex: Buffer, script: Buffer, network: BitcoinNe
virtualToSign.setVersion(0);
const prevTxHash = toSpendTxHex;
const prevOutIndex = 0;
const toSignScriptSig = bitcoin.script.compile([106]);
const toSignScriptSig = bitcoin.script.compile([bitcoin.script.OPS.OP_RETURN]);

virtualToSign.addInput({
hash: prevTxHash,
Expand All @@ -61,23 +62,25 @@ interface SignBip322MessageSimple {
address: string;
message: string;
network: BitcoinNetworkModes;
signPsbt(psbt: bitcoin.Psbt): void;
signPsbt(psbt: bitcoin.Psbt): Promise<btc.Transaction>;
}
export function signBip322MessageSimple(args: SignBip322MessageSimple) {
export async function signBip322MessageSimple(args: SignBip322MessageSimple) {
const { address, message, network, signPsbt } = args;

const { virtualToSpend, script } = createToSpendTx(address, message, network);

const virtualToSign = createToSignTx(virtualToSpend.getHash(), script, network);

signPsbt(virtualToSign);
const signedTx = await signPsbt(virtualToSign);

virtualToSign.finalizeInput(0);
const asBitcoinJsTransaction = bitcoin.Psbt.fromBuffer(Buffer.from(signedTx.toPSBT()));

asBitcoinJsTransaction.finalizeInput(0);

// sign the tx
// section 5.1
// github.com/LegReq/bip0322-signatures/blob/master/BIP0322_signing.ipynb
const toSignTx = virtualToSign.extractTransaction();
const toSignTx = asBitcoinJsTransaction.extractTransaction();

const result = encodeMessageWitnessData(toSignTx.ins[0].witness);

Expand Down
39 changes: 21 additions & 18 deletions packages/bitcoin/src/bip322/sign-message-bip322.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ describe(signBip322MessageSimple.name, () => {
const nativeSegwitAddress = btc.getAddress('wpkh', testVectorKey);
const payment = btc.p2wpkh(secp.getPublicKey(testVectorKey, true));

function signPsbt(psbt: bitcoin.Psbt) {
async function signPsbt(psbt: bitcoin.Psbt) {
psbt.signAllInputs(createNativeSegwitBitcoinJsSigner(Buffer.from(testVectorKey)));
return btc.Transaction.fromPSBT(psbt.toBuffer());
}

if (!nativeSegwitAddress) throw new Error('nativeSegwitAddress is undefined');
Expand All @@ -47,16 +48,16 @@ describe(signBip322MessageSimple.name, () => {
expect(payment.address).toEqual('bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l');
});

test('Signature: "" (empty string)', () => {
test('Signature: "" (empty string)', async () => {
const {
virtualToSpend: emptyStringToSpend,
virtualToSign: emptyStringToSign,
signature: emptyStringSig,
} = signBip322MessageSimple({
} = await signBip322MessageSimple({
address: nativeSegwitAddress,
message: '',
signPsbt,
network: 'mainnet',
signPsbt,
});
expect(emptyStringToSpend.getId()).toEqual(
'c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7'
Expand All @@ -74,13 +75,14 @@ describe(signBip322MessageSimple.name, () => {
});

const helloWorld = 'Hello World';
test(`Signature: "${helloWorld}"`, () => {
const { virtualToSpend, virtualToSign, unencodedSig, signature } = signBip322MessageSimple({
address: nativeSegwitAddress,
message: helloWorld,
signPsbt,
network: 'mainnet',
});
test(`Signature: "${helloWorld}"`, async () => {
const { virtualToSpend, virtualToSign, unencodedSig, signature } =
await signBip322MessageSimple({
address: nativeSegwitAddress,
message: helloWorld,
network: 'mainnet',
signPsbt,
});

// section 3
expect(virtualToSpend.getId()).toEqual(
Expand Down Expand Up @@ -119,11 +121,12 @@ describe(signBip322MessageSimple.name, () => {
ecdsaPublicKeyToSchnorr(secp.getPublicKey(Buffer.from(testVectorKey), true))
);

function signPsbt(psbt: bitcoin.Psbt) {
async function signPsbt(psbt: bitcoin.Psbt) {
psbt.data.inputs.forEach(
input => (input.tapInternalKey = Buffer.from(payment.tapInternalKey))
);
psbt.signAllInputs(createTaprootBitcoinJsSigner(Buffer.from(testVectorKey)));
return btc.Transaction.fromPSBT(psbt.toBuffer());
}

test('Addresses against taproot test vectors', () => {
Expand All @@ -136,8 +139,8 @@ describe(signBip322MessageSimple.name, () => {
});

// Taproot signatures verified with verifymessage request to node
test('Signature: "" (empty string)', () => {
const { signature } = signBip322MessageSimple({
test('Signature: "" (empty string)', async () => {
const { signature } = await signBip322MessageSimple({
address: taprootAddress,
message: '',
network: 'mainnet',
Expand All @@ -149,16 +152,16 @@ describe(signBip322MessageSimple.name, () => {
);
});

test('Signature: "HiroWalletIsTheBest"', () => {
const { signature } = signBip322MessageSimple({
test('Signature: "WearLeather"', async () => {
const { signature } = await signBip322MessageSimple({
address: taprootAddress,
message: 'HiroWalletIsTheBest',
message: 'WearLeather',
network: 'mainnet',
signPsbt,
});

expect(signature).toEqual(
'AUAJNdp5SEAFCDYrIR8TQRkgbNo6P+dUeL97eadGjPWC8iPrhngZSBZvcImVk/HEb3zq3xuyGqyP0dqR7CH2HCa7'
'AUDjK8SJX34boek3m3EKXI94AMBZynJUmdqgO7i4z6JKG6gkUgp+brkWl0ylzWb+8enM4s4B4TWel0iCmcQrKNWS'
);
});
});
Expand Down
4 changes: 1 addition & 3 deletions packages/bitcoin/src/bip322/sign-message-bip322.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import { hashBip322Message } from './bip322-utils';
// Ran into difficiulties with btc-signer vs bitcoinjs-lib
// Using that library to unblock for now, but we should go
// back and replace it when possible.
// Ref PR: https://github.com/leather-wallet/extension/pull/3679
// Prefixing it with underscore for now until we find a use for this function.
// ts-unused-exports:disable-next-line
export function _signBip322MessageSimple(script: Uint8Array, message: string) {
export function _UNSAFE_signBip322MessageSimple(script: Uint8Array, message: string) {
// nVersion = 0
// nLockTime = 0
// vin[0].prevout.hash = 0000...000
Expand Down
116 changes: 89 additions & 27 deletions packages/bitcoin/src/bitcoin.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { hexToBytes } from '@noble/hashes/utils';
import { HDKey, Versions } from '@scure/bip32';
import { mnemonicToSeedSync } from '@scure/bip39';
import * as btc from '@scure/btc-signer';
import { TransactionInput, TransactionOutput } from '@scure/btc-signer/psbt';

import { DerivationPathDepth, extractAccountIndexFromPath } from '@leather-wallet/crypto';
import { BitcoinNetworkModes, NetworkModes } from '@leather-wallet/models';
import type { PaymentTypes } from '@leather-wallet/rpc';
import { defaultWalletKeyId, whenNetwork } from '@leather-wallet/utils';
import { defaultWalletKeyId, isDefined, whenNetwork } from '@leather-wallet/utils';

import { BtcSignerNetwork } from './bitcoin.network';
import { BtcSignerNetwork, getBtcSignerLibNetworkConfigByMode } from './bitcoin.network';
import { getTaprootPayment } from './p2tr-address-gen';

export interface BitcoinAccount {
Expand All @@ -18,8 +19,24 @@ export interface BitcoinAccount {
accountIndex: number;
network: BitcoinNetworkModes;
}
export function initBitcoinAccount(derivationPath: string, policy: string): BitcoinAccount {
const xpub = extractExtendedPublicKeyFromPolicy(policy);
const network = inferNetworkFromPath(derivationPath);
return {
keychain: HDKey.fromExtendedKey(xpub, getHdKeyVersionsFromNetwork(network)),
network,
derivationPath,
type: inferPaymentTypeFromPath(derivationPath),
accountIndex: extractAccountIndexFromPath(derivationPath),
};
}

const bitcoinNetworkToCoreNetworkMap: Record<BitcoinNetworkModes, NetworkModes> = {
/**
* Represents a map of `BitcoinNetworkModes` to `NetworkModes`. While Bitcoin
* has a number of networks, its often only necessary to consider the higher
* level concepts of mainnet and testnet
*/
export const bitcoinNetworkToCoreNetworkMap: Record<BitcoinNetworkModes, NetworkModes> = {
mainnet: 'mainnet',
testnet: 'testnet',
regtest: 'testnet',
Expand All @@ -29,7 +46,13 @@ export function bitcoinNetworkModeToCoreNetworkMode(mode: BitcoinNetworkModes) {
return bitcoinNetworkToCoreNetworkMap[mode];
}

const coinTypeMap: Record<NetworkModes, 0 | 1> = {
/**
* Map representing the "Coin Type" section of a derivation path.
* Consider example below, Coin type is one, thus testnet
* @example
* `m/86'/1'/0'/0/0`
*/
export const coinTypeMap: Record<NetworkModes, 0 | 1> = {
mainnet: 0,
testnet: 1,
};
Expand All @@ -49,7 +72,7 @@ export function deriveAddressIndexZeroFromAccount(keychain: HDKey) {
return deriveAddressIndexKeychainFromAccount(keychain)(0);
}

const ecdsaPublicKeyLength = 33;
export const ecdsaPublicKeyLength = 33;

export function ecdsaPublicKeyToSchnorr(pubKey: Uint8Array) {
if (pubKey.byteLength !== ecdsaPublicKeyLength) throw new Error('Invalid public key length');
Expand Down Expand Up @@ -94,48 +117,61 @@ export function getAddressFromOutScript(script: Uint8Array, bitcoinNetwork: BtcS
});
}

type BtcSignerLibPaymentTypeIdentifers = 'wpkh' | 'wsh' | 'tr' | 'pkh' | 'sh';
/**
* Payment type identifiers, as described by `@scure/btc-signer` library
*/
export type BtcSignerLibPaymentTypeIdentifers = 'wpkh' | 'wsh' | 'tr' | 'pkh' | 'sh';

const paymentTypeMap: Record<BtcSignerLibPaymentTypeIdentifers, PaymentTypes> = {
export const paymentTypeMap: Record<BtcSignerLibPaymentTypeIdentifers, PaymentTypes> = {
wpkh: 'p2wpkh',
wsh: 'p2wpkh-p2sh',
tr: 'p2tr',
pkh: 'p2pkh',
sh: 'p2sh',
};

function btcSignerLibPaymentTypeToPaymentTypeMap(payment: BtcSignerLibPaymentTypeIdentifers) {
export function btcSignerLibPaymentTypeToPaymentTypeMap(
payment: BtcSignerLibPaymentTypeIdentifers
) {
return paymentTypeMap[payment];
}

function isBtcSignerLibPaymentType(payment: string): payment is BtcSignerLibPaymentTypeIdentifers {
export function isBtcSignerLibPaymentType(
payment: string
): payment is BtcSignerLibPaymentTypeIdentifers {
return payment in paymentTypeMap;
}

function parseKnownPaymentType(payment: BtcSignerLibPaymentTypeIdentifers | PaymentTypes) {
export function parseKnownPaymentType(payment: BtcSignerLibPaymentTypeIdentifers | PaymentTypes) {
return isBtcSignerLibPaymentType(payment)
? btcSignerLibPaymentTypeToPaymentTypeMap(payment)
: payment;
}

type PaymentTypeMap<T> = Record<PaymentTypes, T>;
export type PaymentTypeMap<T> = Record<PaymentTypes, T>;
export function whenPaymentType(mode: PaymentTypes | BtcSignerLibPaymentTypeIdentifers) {
return <T extends unknown>(paymentMap: PaymentTypeMap<T>): T =>
paymentMap[parseKnownPaymentType(mode)];
}

function inferPaymentTypeFromPath(path: string): PaymentTypes {
/**
* Infers the Bitcoin payment type from the derivation path.
* Below we see path has 86 in it, per convention, this refers to taproot payments
* @example
* `m/86'/1'/0'/0/0`
*/
export function inferPaymentTypeFromPath(path: string): PaymentTypes {
if (path.startsWith('m/84')) return 'p2wpkh';
if (path.startsWith('m/86')) return 'p2tr';
if (path.startsWith('m/44')) return 'p2pkh';
throw new Error(`Unable to infer payment type from path=${path}`);
}

function inferNetworkFromPath(path: string): NetworkModes {
export function inferNetworkFromPath(path: string): NetworkModes {
return path.split('/')[2].startsWith('0') ? 'mainnet' : 'testnet';
}

function extractExtendedPublicKeyFromPolicy(policy: string) {
export function extractExtendedPublicKeyFromPolicy(policy: string) {
return policy.split(']')[1];
}

Expand All @@ -155,6 +191,30 @@ export function getHdKeyVersionsFromNetwork(network: NetworkModes) {
});
}

export function getBitcoinInputAddress(input: TransactionInput, bitcoinNetwork: BtcSignerNetwork) {
if (isDefined(input.witnessUtxo))
return getAddressFromOutScript(input.witnessUtxo.script, bitcoinNetwork);
if (isDefined(input.nonWitnessUtxo) && isDefined(input.index))
return getAddressFromOutScript(
input.nonWitnessUtxo.outputs[input.index]?.script,
bitcoinNetwork
);
return '';
}

export function getInputPaymentType(
input: TransactionInput,
network: BitcoinNetworkModes
): PaymentTypes {
const address = getBitcoinInputAddress(input, getBtcSignerLibNetworkConfigByMode(network));
if (address === '') throw new Error('Input address cannot be empty');
if (address.startsWith('bc1p') || address.startsWith('tb1p') || address.startsWith('bcrt1p'))
return 'p2tr';
if (address.startsWith('bc1q') || address.startsWith('tb1q') || address.startsWith('bcrt1q'))
return 'p2wpkh';
throw new Error('Unable to infer payment type from input address');
}

// Ledger wallets are keyed by their derivation path. To reuse the look up logic
// between payment types, this factory fn accepts a fn that generates the path
export function lookUpLedgerKeysByPath(
Expand All @@ -173,19 +233,7 @@ export function lookUpLedgerKeysByPath(
};
}

function initBitcoinAccount(derivationPath: string, policy: string): BitcoinAccount {
const xpub = extractExtendedPublicKeyFromPolicy(policy);
const network = inferNetworkFromPath(derivationPath);
return {
keychain: HDKey.fromExtendedKey(xpub, getHdKeyVersionsFromNetwork(network)),
network,
derivationPath,
type: inferPaymentTypeFromPath(derivationPath),
accountIndex: extractAccountIndexFromPath(derivationPath),
};
}

interface GetTaprootAddressArgs {
export interface GetTaprootAddressArgs {
index: number;
keychain?: HDKey;
network: BitcoinNetworkModes;
Expand Down Expand Up @@ -213,3 +261,17 @@ export function mnemonicToRootNode(secretKey: string) {
const seed = mnemonicToSeedSync(secretKey);
return HDKey.fromMasterSeed(seed);
}

export function getPsbtTxInputs(psbtTx: btc.Transaction): TransactionInput[] {
const inputsLength = psbtTx.inputsLength;
const inputs: TransactionInput[] = [];
for (let i = 0; i < inputsLength; i++) inputs.push(psbtTx.getInput(i));
return inputs;
}

export function getPsbtTxOutputs(psbtTx: btc.Transaction): TransactionOutput[] {
const outputsLength = psbtTx.outputsLength;
const outputs: TransactionOutput[] = [];
for (let i = 0; i < outputsLength; i++) outputs.push(psbtTx.getOutput(i));
return outputs;
}
1 change: 0 additions & 1 deletion packages/bitcoin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from './bip322/bip322-utils';
export * from './bip322/sign-message-bip322-bitcoinjs';
export * from './bip322/sign-message-bip322';
export * from './bitcoin-signer';
export * from './bitcoin.network';
export * from './bitcoin.utils';
Expand Down
Loading

0 comments on commit 26ef673

Please sign in to comment.