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 = {