Skip to content

Commit

Permalink
refactor(signing): support non-index zero input signing, closes #4620
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Dec 7, 2023
1 parent d275d8c commit bf9c35f
Show file tree
Hide file tree
Showing 14 changed files with 614 additions and 100 deletions.
4 changes: 2 additions & 2 deletions src/app/common/hooks/analytics/use-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function useAnalytics() {
const [category, name, properties, options, ...rest] = args;
const prop = { ...defaultProperties, ...properties };
const opts = { ...defaultOptions, ...options };
logger.debug(`Analytics page view: ${name}`, properties);
// logger.debug(`Analytics page view: ${name}`, properties);

if (typeof name === 'string' && isIgnoredPath(name)) return;

Expand All @@ -73,7 +73,7 @@ export function useAnalytics() {
const [eventName, properties, options, ...rest] = args;
const prop = { ...defaultProperties, ...properties };
const opts = { ...defaultOptions, ...options };
logger.debug(`Analytics event: ${eventName}`, properties);
// logger.debug(`Analytics event: ${eventName}`, properties);

return analytics.track(eventName, prop, opts, ...rest).catch(logger.error);
},
Expand Down
2 changes: 2 additions & 0 deletions src/app/features/collectibles/components/bitcoin/ordinals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export function Ordinals({ setIsLoadingMore }: OrdinalsProps) {
rootMargin: '0% 0% 20% 0%',
});

// console.log(query.data)

useEffect(() => {
async function fetchNextPage() {
if (!query.hasNextPage || query.isLoading || query.isFetchingNextPage) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as btc from '@scure/btc-signer';
import { hexToBytes } from '@stacks/common';
import get from 'lodash.get';

import { BitcoinInputSigningConfig } from '@shared/crypto/bitcoin/signer-config';
import { logger } from '@shared/logger';
import { RouteUrls } from '@shared/route-urls';

Expand Down Expand Up @@ -45,7 +46,7 @@ function LedgerSignBitcoinTxContainer() {
const [unsignedTransaction, setUnsignedTransaction] = useState<null | btc.Transaction>(null);
const signLedger = useSignLedgerBitcoinTx();

const inputsToSign = useLocationStateWithCache<number[]>('inputsToSign');
const inputsToSign = useLocationStateWithCache<BitcoinInputSigningConfig[]>('inputsToSign');
const allowUserToGoBack = useLocationState<boolean>('goBack');

useEffect(() => {
Expand All @@ -68,12 +69,12 @@ function LedgerSignBitcoinTxContainer() {

try {
const versionInfo = await getBitcoinAppVersion(bitcoinApp);

ledgerAnalytics.trackDeviceVersionInfo(versionInfo);
setAwaitingDeviceConnection(false);

setLatestDeviceResponse(versionInfo as any);
} catch (e) {}
} catch (e) {
logger.error('Unable to get Ledger app version info', e);
}

ledgerNavigate.toDeviceBusyStep('Verifying public key on Ledger…');

Expand All @@ -84,7 +85,11 @@ function LedgerSignBitcoinTxContainer() {
ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: false });

try {
const btcTx = await signLedger(bitcoinApp, unsignedTransaction.toPSBT(), inputsToSign);
const btcTx = await signLedger(
bitcoinApp,
unsignedTransaction.toPSBT(),
inputsToSign?.map(x => x.index)
);

if (!btcTx || !unsignedTransactionRaw) throw new Error('No tx returned');
ledgerNavigate.toAwaitingDeviceOperation({ hasApprovedOperation: true });
Expand Down
6 changes: 5 additions & 1 deletion src/app/features/ledger/hooks/use-ledger-navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { bytesToHex } from '@stacks/common';
import { ClarityValue, StacksTransaction } from '@stacks/transactions';

import { SupportedBlockchains } from '@shared/constants';
import { BitcoinInputSigningConfig } from '@shared/crypto/bitcoin/signer-config';
import { RouteUrls } from '@shared/route-urls';

import { immediatelyAttemptLedgerConnection } from './use-when-reattempt-ledger-connection';
Expand All @@ -30,7 +31,10 @@ export function useLedgerNavigate() {
});
},

toConnectAndSignBitcoinTransactionStep(psbt: Uint8Array, inputsToSign?: number[]) {
toConnectAndSignBitcoinTransactionStep(
psbt: Uint8Array,
inputsToSign?: BitcoinInputSigningConfig[]
) {
return navigate(RouteUrls.ConnectLedger, {
replace: true,
relative: 'route',
Expand Down
3 changes: 2 additions & 1 deletion src/app/features/psbt-signer/hooks/use-psbt-signer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { hexToBytes } from '@noble/hashes/utils';
import * as btc from '@scure/btc-signer';

import { BitcoinInputSigningConfig } from '@shared/crypto/bitcoin/signer-config';
import { logger } from '@shared/logger';
import { isString } from '@shared/utils';

Expand All @@ -14,7 +15,7 @@ import {
export type RawPsbt = ReturnType<typeof btc.RawPSBTV0.decode>;

interface SignPsbtArgs {
indexesToSign?: number[];
indexesToSign: BitcoinInputSigningConfig[];
tx: btc.Transaction;
}
export function usePsbtSigner() {
Expand Down
10 changes: 8 additions & 2 deletions src/app/pages/psbt-request/use-psbt-request.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { bytesToHex } from '@noble/hashes/utils';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';

import { finalizePsbt } from '@shared/actions/finalize-psbt';
import { RouteUrls } from '@shared/route-urls';

import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { usePsbtRequestSearchParams } from '@app/common/psbt/use-psbt-request-params';
import { usePsbtSigner } from '@app/features/psbt-signer/hooks/use-psbt-signer';
import { useGetAssumedZeroIndexSigningConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks';

export function usePsbtRequest() {
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -17,6 +18,7 @@ export function usePsbtRequest() {
const { appName, origin, payload, requestToken, signAtIndex, tabId } =
usePsbtRequestSearchParams();
const { signPsbt, getRawPsbt, getPsbtAsTransaction } = usePsbtSigner();
const getDefaultSigningConfig = useGetAssumedZeroIndexSigningConfig();

return useMemo(() => {
return {
Expand All @@ -41,7 +43,10 @@ export function usePsbtRequest() {
const tx = getPsbtAsTransaction(payload.hex);

try {
const signedTx = await signPsbt({ indexesToSign: signAtIndex, tx });
const signedTx = await signPsbt({
tx,
indexesToSign: getDefaultSigningConfig(hexToBytes(payload.hex)),
});

const signedPsbt = signedTx.toPSBT();

Expand Down Expand Up @@ -71,6 +76,7 @@ export function usePsbtRequest() {
tabId,
getPsbtAsTransaction,
signPsbt,
getDefaultSigningConfig,
navigate,
]);
}
8 changes: 7 additions & 1 deletion src/app/pages/rpc-sign-psbt/use-rpc-sign-psbt.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useNavigate } from 'react-router-dom';

import { RpcErrorCode } from '@btckit/types';
import { hexToBytes } from '@noble/hashes/utils';
import { bytesToHex } from '@stacks/common';

import { Money } from '@shared/models/money.model';
Expand All @@ -20,6 +21,7 @@ import {
useCalculateBitcoinFiatValue,
useCryptoCurrencyMarketData,
} from '@app/query/common/market-data/market-data.hooks';
import { useGetAssumedZeroIndexSigningConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks';

interface BroadcastSignedPsbtTxArgs {
addressNativeSegwitTotal: Money;
Expand All @@ -36,6 +38,7 @@ export function useRpcSignPsbt() {
const { refetch } = useCurrentNativeSegwitUtxos();
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const calculateBitcoinFiatValue = useCalculateBitcoinFiatValue();
const getDefaultSigningConfig = useGetAssumedZeroIndexSigningConfig();

if (!requestId || !psbtHex || !origin) throw new Error('Invalid params in useRpcSignPsbt');

Expand Down Expand Up @@ -90,7 +93,10 @@ export function useRpcSignPsbt() {
const tx = getPsbtAsTransaction(psbtHex);

try {
const signedTx = await signPsbt({ tx, indexesToSign: signAtIndex });
const signedTx = await signPsbt({
tx,
indexesToSign: getDefaultSigningConfig(hexToBytes(psbtHex), signAtIndex),
});

const psbt = signedTx.toPSBT();

Expand Down
68 changes: 46 additions & 22 deletions src/app/store/accounts/blockchain/bitcoin/bitcoin.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ import { Psbt } from 'bitcoinjs-lib';
import AppClient from 'ledger-bitcoin';

import { getBitcoinJsLibNetworkConfigByMode } from '@shared/crypto/bitcoin/bitcoin.network';
import { getTaprootAddress } from '@shared/crypto/bitcoin/bitcoin.utils';
import {
extractAddressIndexFromPath,
getTaprootAddress,
} from '@shared/crypto/bitcoin/bitcoin.utils';
import { getInputPaymentType } from '@shared/crypto/bitcoin/bitcoin.utils';
import { getTaprootAccountDerivationPath } from '@shared/crypto/bitcoin/p2tr-address-gen';
import { getNativeSegwitAccountDerivationPath } from '@shared/crypto/bitcoin/p2wpkh-address-gen';
import {
BitcoinInputSigningConfig,
getAssumedZeroIndexSigningConfig,
} from '@shared/crypto/bitcoin/signer-config';
import { logger } from '@shared/logger';
import { allSighashTypes } from '@shared/rpc/methods/sign-psbt';
import { makeNumberRange } from '@shared/utils';
import { isNumber, makeNumberRange } from '@shared/utils';

import { useWalletType } from '@app/common/use-wallet-type';
import { listenForBitcoinTxLedgerSigning } from '@app/features/ledger/flows/bitcoin-tx-signing/bitcoin-tx-signing-event-listeners';
Expand Down Expand Up @@ -62,23 +69,22 @@ export function useZeroIndexTaprootAddress(accIndex?: number) {
return address;
}

// This implementation assumes address re-use of the 0 index. Funds spread
// across multiple address indexes does not work here.
function useSignBitcoinSoftwareTx() {
const createNativeSegwitSigner = useCurrentAccountNativeSegwitSigner();
const createTaprootSigner = useCurrentAccountTaprootSigner();
const network = useCurrentNetwork();
return async (psbt: Uint8Array, inputsToSign?: number[]) => {
const nativeSegwitSigner = createNativeSegwitSigner?.(0);
const taprootSigner = createTaprootSigner?.(0);

if (!nativeSegwitSigner || !taprootSigner) throw new Error('Signers not available');

return async (psbt: Uint8Array, inputSigningConfig: BitcoinInputSigningConfig[]) => {
const tx = btc.Transaction.fromPSBT(psbt);

const inputIndexes = inputsToSign ?? makeNumberRange(tx.inputsLength);
inputSigningConfig.forEach(({ index, derivationPath }) => {
const nativeSegwitSigner = createNativeSegwitSigner?.(
extractAddressIndexFromPath(derivationPath)
);
const taprootSigner = createTaprootSigner?.(extractAddressIndexFromPath(derivationPath));

if (!nativeSegwitSigner || !taprootSigner) throw new Error('Signers not available');

inputIndexes.forEach(index => {
const input = tx.getInput(index);
const addressType = getInputPaymentType(index, input, network.chain.bitcoin.bitcoinNetwork);

Expand Down Expand Up @@ -199,11 +205,10 @@ export function useSignLedgerBitcoinTx() {

export function useAddTapInternalKeysIfMissing() {
const createTaprootSigner = useCurrentAccountTaprootSigner();
return (tx: btc.Transaction, inputIndexes?: number[]) => {
const taprootSigner = createTaprootSigner?.(0);
if (!taprootSigner) throw new Error('Taproot signer not found');

(inputIndexes ?? makeNumberRange(tx.inputsLength)).forEach(index => {
return (tx: btc.Transaction, inputIndexes: BitcoinInputSigningConfig[]) => {
inputIndexes.forEach(({ index, derivationPath }) => {
const taprootSigner = createTaprootSigner?.(extractAddressIndexFromPath(derivationPath));
if (!taprootSigner) throw new Error('Taproot signer not found');
const input = tx.getInput(index);
const witnessOutputScript =
input.witnessUtxo?.script && btc.OutScript.decode(input.witnessUtxo.script);
Expand All @@ -214,28 +219,47 @@ export function useAddTapInternalKeysIfMissing() {
};
}

export function useGetAssumedZeroIndexSigningConfig() {
const network = useCurrentNetwork();
const accountIndex = useCurrentAccountIndex();

return (psbt: Uint8Array, indexesToSign?: number[]) =>
getAssumedZeroIndexSigningConfig({
psbt,
network: network.chain.bitcoin.bitcoinNetwork,
indexesToSign,
}).forAccountIndex(accountIndex);
}

export function useSignBitcoinTx() {
const { whenWallet } = useWalletType();
const ledgerNavigate = useLedgerNavigate();
const signSoftwareTx = useSignBitcoinSoftwareTx();
const getDefaultSigningConfig = useGetAssumedZeroIndexSigningConfig();

/**
* Don't forget to finalize the tx once it's returned. You can broadcast with
* the hex value from `tx.hex` TODO: add support for signing specific inputs
* the hex value from `tx.hex`.
*/
return (psbt: Uint8Array, inputsToSign?: number[]) =>
whenWallet({
return (psbt: Uint8Array, inputsToSign?: BitcoinInputSigningConfig[] | number[]) => {
function getSigningConfig(inputsToSign?: BitcoinInputSigningConfig[] | number[]) {
if (!inputsToSign) return getDefaultSigningConfig(psbt);
if (inputsToSign.every(isNumber)) return getDefaultSigningConfig(psbt, inputsToSign);
return inputsToSign;
}

return whenWallet({
async ledger() {
// Because Ledger signing is a multi-step process that takes place over
// many routes, in order to achieve a consistent API between
// Ledger/software, we subscribe to the event that occurs when the
// unsigned tx is signed
ledgerNavigate.toConnectAndSignBitcoinTransactionStep(psbt, inputsToSign);
// ledgerNavigate.toConnectAndSignBitcoinTransactionStep(psbt, inputsToSign);
ledgerNavigate.toConnectAndSignBitcoinTransactionStep(psbt, getSigningConfig(inputsToSign));
return listenForBitcoinTxLedgerSigning(bytesToHex(psbt));
},
async software() {
return signSoftwareTx(psbt, inputsToSign);
return signSoftwareTx(psbt, getSigningConfig(inputsToSign));
},
})();
};
}
10 changes: 2 additions & 8 deletions src/shared/crypto/bitcoin/bitcoin.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,20 +249,14 @@ export function getTaprootAddress({ index, keychain, network }: GetTaprootAddres
export function getPsbtTxInputs(psbtTx: btc.Transaction) {
const inputsLength = psbtTx.inputsLength;
const inputs: btc.TransactionInput[] = [];
if (inputsLength === 0) return inputs;
for (let i = 0; i < inputsLength; i++) {
inputs.push(psbtTx.getInput(i));
}
for (let i = 0; i < inputsLength; i++) inputs.push(psbtTx.getInput(i));
return inputs;
}

export function getPsbtTxOutputs(psbtTx: btc.Transaction) {
const outputsLength = psbtTx.outputsLength;
const outputs: btc.TransactionOutput[] = [];
if (outputsLength === 0) return outputs;
for (let i = 0; i < outputsLength; i++) {
outputs.push(psbtTx.getOutput(i));
}
for (let i = 0; i < outputsLength; i++) outputs.push(psbtTx.getOutput(i));
return outputs;
}

Expand Down
Loading

0 comments on commit bf9c35f

Please sign in to comment.