From ad9a7fdf6327e8ee266fa644d16c14602340147b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Landabaso=20D=C3=ADaz?= Date: Thu, 4 Jul 2024 18:21:24 +0200 Subject: [PATCH 1/2] Add RBF parameter support to PSBT inputs, set RBF default to true - Updated README.md to include `rbf` parameter in the `updatePsbtAsInput` method. - Modified `src/descriptors.ts` to handle `rbf` parameter for PSBT inputs. - Updated `src/psbt.ts` to integrate RBF logic with nSequence. - Set RBF parameter default to true, altering default behavior from previous versions. This update ensures transactions using relative timelocks correctly opt into RBF, providing flexibility for transaction fee adjustments while ensuring compatibility with relative timelocks. --- README.md | 4 ++-- src/descriptors.ts | 31 +++++++++++++++++++++++++------ src/psbt.ts | 14 ++++++++++++-- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ab04c2e..97a40ed 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,10 @@ To call `updatePsbtAsInput()`, use the following syntax: ```javascript import { Psbt } from 'bitcoinjs-lib'; const psbt = new Psbt(); -const inputFinalizer = output.updatePsbtAsInput({ psbt, txHex, vout }); +const inputFinalizer = output.updatePsbtAsInput({ psbt, txHex, vout, rbf }); ``` -Here, `psbt` refers to an instance of the [bitcoinjs-lib Psbt class](https://github.com/bitcoinjs/bitcoinjs-lib). The parameter `txHex` denotes a hex string that serializes the previous transaction containing this output. Meanwhile, `vout` is an integer that marks the position of the output within that transaction. +Here, `psbt` refers to an instance of the [bitcoinjs-lib Psbt class](https://github.com/bitcoinjs/bitcoinjs-lib). The parameter `txHex` denotes a hex string that serializes the previous transaction containing this output. Meanwhile, `vout` is an integer that marks the position of the output within that transaction. Finally, `rbf` is an optional parameter (defaulting to `true`) used to indicate whether the transaction uses Replace-By-Fee (RBF). When RBF is enabled, transactions can be replaced while they are in the mempool with others that have higher fees. Note that RBF is enabled for the entire transaction if at least one input signals it. Also, note that transactions using relative time locks inherently opt into RBF due to the `nSequence` range used. The method returns the `inputFinalizer()` function. This finalizer function completes a PSBT input by adding the unlocking script (`scriptWitness` or `scriptSig`) that satisfies the previous 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 `inputFinalizer()`. Detailed [explanations on the `inputFinalizer` method](#signers-and-finalizers-finalize-psbt-input) can be found in the Signers and Finalizers section. diff --git a/src/descriptors.ts b/src/descriptors.ts index 2a08e72..f8e1e44 100644 --- a/src/descriptors.ts +++ b/src/descriptors.ts @@ -1173,6 +1173,7 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { txId?: string; value?: number; vout: number; + rbf?: boolean; }) { this.updatePsbtAsInput(params); return params.psbt.data.inputs.length - 1; @@ -1195,6 +1196,14 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { * * When unsure, always use `txHex`, and skip `txId` and `value` for safety. * + * Use `rbf` to mark whether this tx can be replaced with another with + * higher fee while being in the mempool. Note that a tx will automatically + * be marked as replacable if a single input requests it. + * Note that any transaction using a relative timelock (nSequence < 0x80000000) + * also falls within the RBF range (nSequence < 0xFFFFFFFE), making it + * inherently replaceable. So don't set `rbf` to false if this is tx uses + * relative time locks. + * * @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: @@ -1207,13 +1216,15 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { txHex, txId, value, - vout //vector output index + vout, //vector output index + rbf = true }: { psbt: Psbt; txHex?: string; txId?: string; value?: number; vout: number; + rbf?: boolean; }) { if (txHex === undefined) { console.warn(`Warning: missing txHex may allow fee attacks`); @@ -1237,7 +1248,8 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { scriptPubKey: this.getScriptPubKey(), isSegwit, witnessScript: this.getWitnessScript(), - redeemScript: this.getRedeemScript() + redeemScript: this.getRedeemScript(), + rbf }); const finalizer = ({ psbt, @@ -1283,16 +1295,23 @@ export function DescriptorsFactory(ecc: TinySecp256k1Interface) { scriptPubKey = out.script; } const locktime = this.getLockTime() || 0; - let sequence = this.getSequence(); - if (sequence === undefined && locktime !== 0) sequence = 0xfffffffe; - if (sequence === undefined && locktime === 0) sequence = 0xffffffff; + const sequence = this.getSequence(); + //We don't know whether the user opted for RBF or not. So check that + //at least one of the 2 sequences matches. + const sequenceNoRBF = + sequence !== undefined + ? sequence + : locktime === 0 + ? 0xffffffff + : 0xfffffffe; + const sequenceRBF = sequence !== undefined ? sequence : 0xfffffffd; const eqBuffers = (buf1: Buffer | undefined, buf2: Buffer | undefined) => buf1 instanceof Buffer && buf2 instanceof Buffer ? Buffer.compare(buf1, buf2) === 0 : buf1 === buf2; if ( Buffer.compare(scriptPubKey, this.getScriptPubKey()) !== 0 || - sequence !== inputSequence || + (sequenceRBF !== inputSequence && sequenceNoRBF !== inputSequence) || locktime !== psbt.locktime || !eqBuffers(this.getWitnessScript(), input.witnessScript) || !eqBuffers(this.getRedeemScript(), input.redeemScript) diff --git a/src/psbt.ts b/src/psbt.ts index 5ddca45..50b63e5 100644 --- a/src/psbt.ts +++ b/src/psbt.ts @@ -140,7 +140,8 @@ export function updatePsbt({ scriptPubKey, isSegwit, witnessScript, - redeemScript + redeemScript, + rbf }: { psbt: Psbt; vout: number; @@ -154,8 +155,11 @@ export function updatePsbt({ isSegwit: boolean; witnessScript: Buffer | undefined; redeemScript: Buffer | undefined; + rbf: boolean; }): number { //Some data-sanity checks: + if (sequence !== undefined && rbf && sequence > 0xfffffffd) + throw new Error(`Error: incompatible sequence and rbf settings`); if (!isSegwit && txHex === undefined) throw new Error(`Error: txHex is mandatory for Non-Segwit inputs`); if ( @@ -209,13 +213,19 @@ export function updatePsbt({ // this input's sequence < 0xffffffff if (sequence === undefined) { //NOTE: if sequence is undefined, bitcoinjs-lib uses 0xffffffff as default - sequence = 0xfffffffe; + sequence = rbf ? 0xfffffffd : 0xfffffffe; } else if (sequence > 0xfffffffe) { throw new Error( `Error: incompatible sequence: ${sequence} and locktime: ${locktime}` ); } + if (sequence === undefined && rbf) sequence = 0xfffffffd; psbt.setLocktime(locktime); + } else { + if (sequence === undefined) { + if (rbf) sequence = 0xfffffffd; + else sequence = 0xffffffff; + } } const input: PsbtInputExtended = { From 9ae22d30f474d0f263be5999c4eed049b1fa431f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Landabaso=20D=C3=ADaz?= Date: Thu, 4 Jul 2024 18:22:00 +0200 Subject: [PATCH 2/2] Update dependencies and bump version to 2.2.0 - Bumped versions of `@bitcoinerlab/miniscript` and `@bitcoinerlab/secp256k1` in package.json and package-lock.json. - Bump to version 2.2.0 since prev commit alters default behavior from previous versions. This update ensures transactions using relative timelocks correctly opt into RBF, providing flexibility for transaction fee adjustments while ensuring compatibility with relative timelocks. --- package-lock.json | 32 ++++++++++++++++---------------- package.json | 6 +++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 677c541..430789c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@bitcoinerlab/descriptors", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bitcoinerlab/descriptors", - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "dependencies": { - "@bitcoinerlab/miniscript": "^1.2.1", - "@bitcoinerlab/secp256k1": "^1.0.5", + "@bitcoinerlab/miniscript": "^1.4.0", + "@bitcoinerlab/secp256k1": "^1.1.1", "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.3", "ecpair": "^2.1.0", @@ -750,17 +750,17 @@ } }, "node_modules/@bitcoinerlab/miniscript": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.2.1.tgz", - "integrity": "sha512-2Pkan4Vvq7afQNeveakFM/Np9D+DhlPtKITO/hTgKjPsPnLWqdOVyja1u/Qhr7Voi2bW9Tor6hLOYTw84QQKQQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.4.0.tgz", + "integrity": "sha512-BsG3dmwQmgKHnRZecDgUsPjwcpnf1wgaZbolcMTByS10k1zYzIx97W51LzG7GvokRJ+wnzTX/GhC8Y3L2X0CQA==", "dependencies": { "bip68": "^1.0.4" } }, "node_modules/@bitcoinerlab/secp256k1": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz", - "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.1.1.tgz", + "integrity": "sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==", "dependencies": { "@noble/hashes": "^1.1.5", "@noble/secp256k1": "^1.7.1" @@ -6952,17 +6952,17 @@ } }, "@bitcoinerlab/miniscript": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.2.1.tgz", - "integrity": "sha512-2Pkan4Vvq7afQNeveakFM/Np9D+DhlPtKITO/hTgKjPsPnLWqdOVyja1u/Qhr7Voi2bW9Tor6hLOYTw84QQKQQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/miniscript/-/miniscript-1.4.0.tgz", + "integrity": "sha512-BsG3dmwQmgKHnRZecDgUsPjwcpnf1wgaZbolcMTByS10k1zYzIx97W51LzG7GvokRJ+wnzTX/GhC8Y3L2X0CQA==", "requires": { "bip68": "^1.0.4" } }, "@bitcoinerlab/secp256k1": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz", - "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.1.1.tgz", + "integrity": "sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==", "requires": { "@noble/hashes": "^1.1.5", "@noble/secp256k1": "^1.7.1" diff --git a/package.json b/package.json index 5b61a84..218350d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@bitcoinerlab/descriptors", "description": "This library parses and creates Bitcoin Miniscript Descriptors and generates Partially Signed Bitcoin Transactions (PSBTs). It provides PSBT finalizers and signers for single-signature, BIP32 and Hardware Wallets.", "homepage": "https://github.com/bitcoinerlab/descriptors", - "version": "2.1.0", + "version": "2.2.0", "author": "Jose-Luis Landabaso", "license": "MIT", "repository": { @@ -67,8 +67,8 @@ "yargs": "^17.7.2" }, "dependencies": { - "@bitcoinerlab/miniscript": "^1.2.1", - "@bitcoinerlab/secp256k1": "^1.0.5", + "@bitcoinerlab/miniscript": "^1.4.0", + "@bitcoinerlab/secp256k1": "^1.1.1", "bip32": "^4.0.0", "bitcoinjs-lib": "^6.1.3", "ecpair": "^2.1.0",