Skip to content

Commit

Permalink
Ledger: udpate ledger methods so that they won't require passing outp…
Browse files Browse the repository at this point in the history
…uts. New approach for finalizers: now uses a returned function when setting an input. Better docs
  • Loading branch information
landabaso committed Oct 18, 2023
1 parent 645d290 commit 5f8b9c1
Show file tree
Hide file tree
Showing 14 changed files with 964 additions and 405 deletions.
31 changes: 10 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ The library can be split into four main parts:
The `Output` class is dynamically created by providing a cryptographic secp256k1 engine as shown below:

```javascript
import * as secp256k1 from '@bitcoinerlab/secp256k1';
import * as ecc from '@bitcoinerlab/secp256k1';
import * as descriptors from '@bitcoinerlab/descriptors';
const { Output } = descriptors.DescriptorsFactory(secp256k1);
const { Output } = descriptors.DescriptorsFactory(ecc);
```

Once set up, you can obtain an instance for an output, described by a descriptor such as a `wpkh`, as follows:
Expand Down Expand Up @@ -129,7 +129,7 @@ const recipientOutput =
recipientOutput.updatePsbtAsOutput({ psbt, value: 10000 });
```

The `finalizePsbtInput()` method completes a PSBT input by adding the unlocking script (either `scriptWitness` or `scriptSig`) that satisfies the input's spending conditions to the PSBT. Bear in mind that both `scriptSig` and `scriptWitness` incorporate signatures. As such, you should complete all necessary signing operations before calling this method. Detailed [explanations on the `finalizePsbtInput` method](#signers-and-finalizers-finalize-psbt-input) can be found in the Signers and Finalizers section.
The `finalizePsbtInput()` method completes a PSBT input by adding the unlocking script (`scriptWitness` or `scriptSig`) that satisfies the output's spending conditions. Bear in mind that both `scriptSig` and `scriptWitness` incorporate signatures. As such, you should complete all necessary signing operations before calling this method. Detailed [explanations on the `finalizePsbtInput` method](#signers-and-finalizers-finalize-psbt-input) can be found in the Signers and Finalizers section.

For further information on using the `Output` class, refer to the [comprehensive guides](https://bitcoinerlab.com/guides) that offer explanations and playgrounds to help learn the module. Additionally, a [Stack Exchange answer](https://bitcoin.stackexchange.com/a/118036/89665) provides a focused explanation on the constructor, specifically the `signersPubKeys` parameter, and the usage of `updatePsbtAsInput`, `finalizePsbtInput`, `getAddress`, and `getScriptPubKey`.

Expand All @@ -138,7 +138,7 @@ For further information on using the `Output` class, refer to the [comprehensive
`DescriptorsFactory` provides a convenient `expand()` function that allows you to parse a descriptor without the need to instantiate the `Output` class. This function can be used as follows:

```javascript
const { expand } = descriptors.DescriptorsFactory(secp256k1);
const { expand } = descriptors.DescriptorsFactory(ecc);
const result = expand({
descriptor: 'sh(wsh(andor(pk(0252972572d465d016d4c501887b8df303eee3ed602c056b1eb09260dfa0da0ab2),older(8640),pk([d34db33f/49'/0'/0']tpubDCdxmvzJ5QBjTN8oCjjyT2V58AyZvA1fkmCeZRC75QMoaHcVP2m45Bv3hmnR7ttAwkb2UNYyoXdHVt4gwBqRrJqLUU2JrM43HippxiWpHra/1/2/3/4/*))))',
network: networks.testnet, // One of bitcoinjs-lib `networks`
Expand Down Expand Up @@ -215,7 +215,7 @@ pkhBIP32(params: {
})
```
For functions suffixed with *Ledger* (designed to generate descriptors for Ledger Hardware devices), replace `masterNode` with both `ledgerClient` and `ledgerState`. Detailed information on Ledger integration will be provided in subsequent sections.
For functions suffixed with *Ledger* (designed to generate descriptors for Ledger Hardware devices), replace `masterNode` with `ledgerManager`. Detailed information on Ledger integration will be provided in subsequent sections.
The `keyExpressions` category includes functions that generate string representations of key expressions for public keys.
Expand All @@ -241,7 +241,7 @@ function keyExpressionBIP32({
});
```
For the `keyExpressionLedger` function, you'd use `ledgerClient` and `ledgerState` instead of `masterNode`. Detailed information on Ledger in subsequent sections.
For the `keyExpressionLedger` function, you'd use `ledgerManager` instead of `masterNode`. Detailed information on Ledger in subsequent sections.
Both functions will generate strings that fully define BIP32 keys. For example: `[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*`. Read [Bitcoin Core descriptors documentation](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md) to learn more about Key Expressions.
Expand All @@ -259,16 +259,7 @@ For signing operations, utilize the methods provided by the `signers`:
```javascript
// For Ledger
await signers.signLedger({
ledgerClient,
ledgerState,
psbt,
outputs
});
// Note: The `outputs` array consists of `Output` instances. It should encompass
// all unspent outputs associated with Ledger-controlled keys, as these are
// essential for signing their linked inputs within the `psbt`. Nevertheless,
// the array may also contain other unrelated unspent outputs.
await signers.signLedger({ psbt, ledgerManager });
// For BIP32 - https://github.com/bitcoinjs/bip32
signers.signBIP32({ psbt, masterNode });
Expand Down Expand Up @@ -318,6 +309,7 @@ await ledger.assertLedgerApp({
});
const ledgerClient = new AppClient(transport);
const ledgerManager = { ledgerClient, ledgerState: {}, ecc, network };
```
Here, `transport` is an instance of a Transport object that allows communication with Ledger devices. You can use any of the transports [provided by Ledger](https://github.com/LedgerHQ/ledger-live#libs---libraries).
Expand All @@ -326,16 +318,15 @@ To register the policies of non-standard descriptors on the Ledger device, use t
```javascript
await ledger.registerLedgerWallet({
ledgerClient,
ledgerState,
ledgerManager,
descriptor: wshDescriptor,
policyName: 'BitcoinerLab'
});
```
This code will auto-skip the policy registration process if it already exists. Please refer to [Ledger documentation](https://github.com/LedgerHQ/app-bitcoin-new/blob/develop/doc/wallet.md) to learn more about their Wallet Policies registration procedures.
Finally, `ledgerState` is an object used to store information related to Ledger devices. Although Ledger devices themselves are stateless, this object can be used to store information such as xpubs, master fingerprints, and wallet policies. You can pass an initially empty object that will be updated with more information as it is used. The object can be serialized and stored for future use.
Finally, `ledgerManager.ledgerState` is an object used to store information related to Ledger devices. Although Ledger devices themselves are stateless, this object can be used to store information such as xpubs, master fingerprints, and wallet policies. You can pass an initially empty object that will be updated with more information as it is used. The object can be serialized and stored for future use.
<a name="documentation"></a>
Expand All @@ -357,8 +348,6 @@ For more information, refer to the following resources:
The generated documentation will be available in the `docs/` directory. Open the `index.html` file to view the documentation.
Please note that not all the functions have been fully documented yet. However, you can easily understand their usage by reading the source code or by checking the integration tests or playgrounds.
## Authors and Contributors
The project was initially developed and is currently maintained by [Jose-Luis Landabaso](https://github.com/landabaso). Contributions and help from other developers are welcome.
Expand Down
149 changes: 134 additions & 15 deletions src/descriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,11 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
};

/**
* Parses and analyzies a descriptor expression and destructures it into {@link Expansion |its elemental parts}.
* Parses and analyzies a descriptor expression and destructures it into
* {@link Expansion |its elemental parts}.
*
* @throws {Error} Throws an error if the descriptor cannot be parsed or does not conform to the expected format.
* @throws {Error} Throws an error if the descriptor cannot be parsed or does
* not conform to the expected format.
*/
function expand(params: {
/**
Expand Down Expand Up @@ -526,6 +528,12 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
});
}

/**
* The `Output` class is the central component for managing descriptors.
* It facilitates the creation of outputs to receive funds and enables the
* signing and finalization of PSBTs (Partially Signed Bitcoin Transactions)
* for spending UTXOs (Unspent Transaction Outputs).
*/
class Output {
readonly #payment: Payment;
readonly #preimages: Preimage[] = [];
Expand Down Expand Up @@ -588,7 +596,17 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
preimages?: Preimage[];

/**
* An array of the public keys used for signing the transaction when spending the output associated with this descriptor. This parameter is only used if the descriptor object is being used to finalize a transaction. It is necessary to specify the spending path when working with miniscript-based expressions that have multiple spending paths. Set this parameter to an array containing the public keys involved in the desired spending path. Leave it `undefined` if you only need to generate the `scriptPubKey` or `address` for a descriptor, or if all the public keys involved in the descriptor will sign the transaction. In the latter case, the satisfier will automatically choose the most optimal spending path (if more than one is available).
* An array of the public keys used for signing the transaction when
* spending the output associated with this descriptor. This parameter is
* only used if the descriptor object is being used to finalize a
* transaction. It is necessary to specify the spending path when working
* with miniscript-based expressions that have multiple spending paths.
* Set this parameter to an array containing the public keys involved in
* the desired spending path. Leave it `undefined` if you only need to
* generate the `scriptPubKey` or `address` for a descriptor, or if all
* the public keys involved in the descriptor will sign the transaction.
* In the latter case, the satisfier will automatically choose the most
* optimal spending path (if more than one is available).
*/
signersPubKeys?: Buffer[];
}) {
Expand Down Expand Up @@ -695,28 +713,50 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
return { nLockTime, nSequence };
} else return undefined;
}
/**
* Creates and returns an instance of bitcoinjs-lib
* [`Payment`](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/payments/index.ts)'s interface with the `scriptPubKey` of this `Output`.
*/
getPayment(): Payment {
return this.#payment;
}
/**
* Returns the Bitcoin Address
* Returns the Bitcoin Address of this `Output`.
*/
getAddress(): string {
if (!this.#payment.address)
throw new Error(`Error: could extract an address from the payment`);
return this.#payment.address;
}
/**
* Returns this `Output`'s scriptPubKey.
*/
getScriptPubKey(): Buffer {
if (!this.#payment.output)
throw new Error(`Error: could extract output.script from the payment`);
return this.#payment.output;
}
/**
* Returns the compiled script satisfaction
* @param {PartialSig[]} signatures An array of signatures using this format: `interface PartialSig { pubkey: Buffer; signature: Buffer; }`
* @returns {Buffer}
* Returns the compiled Script Satisfaction if this `Output` was created
* using a miniscript-based descriptor.
*
* The Satisfaction is the unlocking script that fulfills
* (satisfies) this `Output` and it is derived using the Safisfier algorithm
* [described here](https://bitcoin.sipa.be/miniscript/).
*
* Important: As mentioned above, note that this function only applies to
* miniscript descriptors.
*/
getScriptSatisfaction(signatures: PartialSig[]): Buffer {
getScriptSatisfaction(
/**
* An array with all the signatures needed to
* build the Satisfaction of this miniscript-based `Output`.
*
* `signatures` must be passed using this format (pairs of `pubKey/signature`):
* `interface PartialSig { pubkey: Buffer; signature: Buffer; }`
*/
signatures: PartialSig[]
): Buffer {
const miniscript = this.#miniscript;
const expandedMiniscript = this.#expandedMiniscript;
const expansionMap = this.#expansionMap;
Expand Down Expand Up @@ -751,21 +791,41 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
throw new Error(`Error: could not produce a valid satisfaction`);
return scriptSatisfaction;
}
/**
* Gets the nSequence required to fulfill this `Output`.
*/
getSequence(): number | undefined {
return this.#getTimeConstraints()?.nSequence;
}
/**
* Gets the nLockTime required to fulfill this `Output`.
*/
getLockTime(): number | undefined {
return this.#getTimeConstraints()?.nLockTime;
}
/**
* Gets the witnessScript required to fulfill this `Output`. Only applies to
* Segwit outputs.
*/
getWitnessScript(): Buffer | undefined {
return this.#witnessScript;
}
/**
* Gets the redeemScript required to fullfill this `Output`. Only applies to
* SH outputs: sh(wpkh), sh(wsh), sh(lockingScript).
*/
getRedeemScript(): Buffer | undefined {
return this.#redeemScript;
}
/**
* Gets the bitcoinjs-lib [`network`](https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts) used to create this `Output`.
*/
getNetwork(): Network {
return this.#network;
}
/**
* Whether this `Output` is Segwit.
*/
isSegwit(): boolean | undefined {
return this.#isSegwit;
}
Expand All @@ -780,7 +840,8 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
value?: number;
vout: number;
}) {
return this.updatePsbtAsInput(params);
this.updatePsbtAsInput(params);
return params.psbt.data.inputs.length - 1;
}

/**
Expand All @@ -800,7 +861,12 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
*
* When unsure, always use `txHex`, and skip `txId` and `value` for safety.
*
* @returns The index of the added input.
* @returns A finalizer function to be used after signing the `psbt`.
* This function ensures that this input is properly finalized.
* The finalizer has this signature:
*
* `( { psbt, validate = true } : { psbt: Psbt; validate: boolean | undefined } ) => void`
*
*/
updatePsbtAsInput({
psbt,
Expand All @@ -814,7 +880,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
txId?: string;
value?: number;
vout: number;
}): number {
}) {
if (txHex === undefined) {
console.warn(`Warning: missing txHex may allow fee attacks`);
}
Expand All @@ -825,7 +891,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
`Error: could not determine whether this is a segwit descriptor`
);
}
return updatePsbt({
const index = updatePsbt({
psbt,
vout,
...(txHex !== undefined ? { txHex } : {}),
Expand All @@ -839,6 +905,15 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
witnessScript: this.getWitnessScript(),
redeemScript: this.getRedeemScript()
});
const finalizer = ({
psbt,
validate = true
}: {
psbt: Psbt;
/** @default true */
validate?: boolean | undefined;
}) => this.finalizePsbtInput({ index, psbt, validate });
return finalizer;
}

/**
Expand Down Expand Up @@ -890,13 +965,45 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
);
}
}

/**
* Finalizes a PSBT input by adding the necessary unlocking script that satisfies this `Output`'s
* spending conditions.
*
* 🔴 IMPORTANT 🔴
* It is STRONGLY RECOMMENDED to use the finalizer function returned by
* {@link _Internal_.Output.updatePsbtAsInput | `updatePsbtAsInput`} instead
* of calling this method directly.
* This approach eliminates the need to manage the `Output` instance and the
* input's index, simplifying the process.
*
* The `finalizePsbtInput` method completes a PSBT input by adding the
* unlocking script (`scriptWitness` or `scriptSig`) that satisfies
* this `Output`'s spending conditions. Bear in mind that both
* `scriptSig` and `scriptWitness` incorporate signatures. As such, you
* should complete all necessary signing operations before calling this
* method.
*
* For each unspent output from a previous transaction that you're
* referencing in a `psbt` as an input to be spent, apply this method as
* follows: `output.finalizePsbtInput({ index, psbt })`.
*
* It's essential to specify the exact position (or `index`) of the input in
* the `psbt` that references this unspent `Output`. This `index` should
* align with the value returned by the `updatePsbtAsInput` method.
* Note:
* The `index` corresponds to the position of the input in the `psbt`.
* To get this index, right after calling `updatePsbtAsInput()`, use:
* `index = psbt.data.inputs.length - 1`.
*/
finalizePsbtInput({
index,
psbt,
validate = true
}: {
index: number;
psbt: Psbt;
/** @default true */
validate?: boolean | undefined;
}): void {
if (
Expand Down Expand Up @@ -928,6 +1035,10 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
);
}
}
/**
* Decomposes the descriptor used to form this `Output` into its elemental
* parts. See {@link ExpansionMap ExpansionMap} for a detailed explanation.
*/
expand() {
return {
...(this.#expandedExpression !== undefined
Expand Down Expand Up @@ -967,14 +1078,22 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) {
}
}

return { Descriptor, Output, parseKeyExpression, expand, ECPair, BIP32 };
return {
// deprecated TAG must also be below so it is exported to descriptors.d.ts
/** @deprecated */ Descriptor,
Output,
parseKeyExpression,
expand,
ECPair,
BIP32
};
}

/** @hidden */
/** @hidden @deprecated */
type DescriptorConstructor = ReturnType<
typeof DescriptorsFactory
>['Descriptor'];
/** @hidden */
/** @hidden @deprecated */
type DescriptorInstance = InstanceType<DescriptorConstructor>;
export { DescriptorInstance, DescriptorConstructor };

Expand Down
Loading

0 comments on commit 5f8b9c1

Please sign in to comment.