Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(signing): support non-index zero input signing #4646

Merged
merged 1 commit into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
9 changes: 5 additions & 4 deletions 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[];
signingConfig: BitcoinInputSigningConfig[];
tx: btc.Transaction;
}
export function usePsbtSigner() {
Expand All @@ -23,9 +24,9 @@ export function usePsbtSigner() {

return useMemo(
() => ({
async signPsbt({ indexesToSign, tx }: SignPsbtArgs) {
addMissingTapInteralKeys(tx, indexesToSign);
return signBitcoinTx(tx.toPSBT(), indexesToSign);
async signPsbt({ signingConfig, tx }: SignPsbtArgs) {
addMissingTapInteralKeys(tx, signingConfig);
return signBitcoinTx(tx.toPSBT(), signingConfig);
},
getPsbtAsTransaction(psbt: string | Uint8Array) {
const bytes = isString(psbt) ? hexToBytes(psbt) : psbt;
Expand Down
8 changes: 6 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,8 @@ export function usePsbtRequest() {
const tx = getPsbtAsTransaction(payload.hex);

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

const signedPsbt = signedTx.toPSBT();

Expand Down Expand Up @@ -71,6 +74,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,
signingConfig: getDefaultSigningConfig(hexToBytes(psbtHex), signAtIndex),
});

const psbt = signedTx.toPSBT();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as btc from '@scure/btc-signer';
import { AddressType, getAddressInfo } from 'bitcoin-address-validation';

import { BitcoinInputSigningConfig } from '@shared/crypto/bitcoin/signer-config';
import { logger } from '@shared/logger';
import { OrdinalSendFormValues } from '@shared/models/form.model';

import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { createCounter } from '@app/common/utils/counter';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { TaprootUtxo } from '@app/query/bitcoin/bitcoin-client';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
Expand All @@ -29,6 +31,7 @@ export function useGenerateUnsignedOrdinalTx(taprootInput: TaprootUtxo) {

function formTaprootOrdinalTx(values: OrdinalSendFormValues) {
const inscriptionInput = taprootInput;

const taprootSigner = createTaprootSigner?.(inscriptionInput.addressIndex);
const nativeSegwitSigner = createNativeSegwitSigner?.(0);

Expand All @@ -42,6 +45,10 @@ export function useGenerateUnsignedOrdinalTx(taprootInput: TaprootUtxo) {
feeRate: values.feeRate,
});

const psbtInputCounter = createCounter();

const signingConfig: BitcoinInputSigningConfig[] = [];

if (!result.success) return null;

const { inputs, outputs } = result;
Expand All @@ -60,9 +67,14 @@ export function useGenerateUnsignedOrdinalTx(taprootInput: TaprootUtxo) {
amount: BigInt(taprootInput.value),
},
});
signingConfig.push({
derivationPath: taprootSigner.derivationPath,
index: psbtInputCounter.getValue(),
});
psbtInputCounter.increment();

// Fee-covering Native Segwit inputs
inputs.forEach(input =>
inputs.forEach(input => {
tx.addInput({
txid: input.txid,
index: input.vout,
Expand All @@ -71,15 +83,20 @@ export function useGenerateUnsignedOrdinalTx(taprootInput: TaprootUtxo) {
amount: BigInt(input.value),
script: nativeSegwitSigner.payment.script,
},
})
);
});
signingConfig.push({
derivationPath: nativeSegwitSigner.derivationPath,
index: psbtInputCounter.getValue(),
});
psbtInputCounter.increment();
});

// Recipient and change outputs
outputs.forEach(output => tx.addOutputAddress(output.address, output.value, networkMode));

tx.toPSBT();

return { hex: tx.hex, psbt: tx.toPSBT() };
return { psbt: tx.toPSBT(), signingConfig };
} catch (e) {
logger.error('Unable to sign transaction');
return null;
Expand All @@ -88,6 +105,7 @@ export function useGenerateUnsignedOrdinalTx(taprootInput: TaprootUtxo) {

function formNativeSegwitOrdinalTx(values: OrdinalSendFormValues) {
const nativeSegwitSigner = createNativeSegwitSigner?.(0);

const { feeRate, recipient } = values;
if (!nativeSegwitSigner || !nativeSegwitUtxos || !values.feeRate) return;

Expand Down Expand Up @@ -130,7 +148,7 @@ export function useGenerateUnsignedOrdinalTx(taprootInput: TaprootUtxo) {
tx.addOutputAddress(values.recipient, BigInt(output.value), networkMode);
});

return { hex: tx.hex, psbt: tx.toPSBT() };
return { psbt: tx.toPSBT(), signingConfig: undefined };
} catch (e) {
logger.error('Unable to sign transaction');
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function useSendInscriptionForm() {
return;
}

const signedTx = await sign(resp.psbt);
const signedTx = await sign(resp.psbt, resp.signingConfig);

if (!signedTx) {
logger.error('No signed transaction returned');
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));
},
})();
};
}
Loading
Loading