From f1d19b438e08fad6a32f94f52192f522a7cfd324 Mon Sep 17 00:00:00 2001 From: Braydon Fuller Date: Wed, 2 Dec 2015 22:12:04 -0500 Subject: [PATCH] Transaction: Added replace-by-fee (RBF) support - Useful for bidding transactions as described in: https://bitpay.com/chaindb.pdf - Reference: nSequence-based opt-in: https://github.com/bitcoin/bitcoin/pull/6871 --- lib/transaction/input/input.js | 9 ++- lib/transaction/transaction.js | 31 +++++++++- test/transaction/transaction.js | 102 +++++++++++++++++++++++++++++++- 3 files changed, 137 insertions(+), 5 deletions(-) diff --git a/lib/transaction/input/input.js b/lib/transaction/input/input.js index d425768f5..9d86e6656 100644 --- a/lib/transaction/input/input.js +++ b/lib/transaction/input/input.js @@ -11,9 +11,10 @@ var Script = require('../../script'); var Sighash = require('../sighash'); var Output = require('../output'); - -var DEFAULT_SEQNUMBER = 0xFFFFFFFF; -var DEFAULT_LOCKTIME_SEQNUMBER = 0x00000000; +var MAXINT = 0xffffffff; // Math.pow(2, 32) - 1; +var DEFAULT_RBF_SEQNUMBER = MAXINT - 2; +var DEFAULT_SEQNUMBER = MAXINT; +var DEFAULT_LOCKTIME_SEQNUMBER = MAXINT - 1; function Input(params) { if (!(this instanceof Input)) { @@ -24,8 +25,10 @@ function Input(params) { } } +Input.MAXINT = MAXINT; Input.DEFAULT_SEQNUMBER = DEFAULT_SEQNUMBER; Input.DEFAULT_LOCKTIME_SEQNUMBER = DEFAULT_LOCKTIME_SEQNUMBER; +Input.DEFAULT_RBF_SEQNUMBER = DEFAULT_RBF_SEQNUMBER; Object.defineProperty(Input.prototype, 'script', { configurable: false, diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 1acc71eed..66213bb83 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -544,7 +544,7 @@ Transaction.prototype.from = function(utxo, pubkeys, threshold) { return input.prevTxId.toString('hex') === utxo.txId && input.outputIndex === utxo.outputIndex; }); if (exists) { - return; + return this; } if (pubkeys && threshold) { this._fromMultisigUtxo(utxo, pubkeys, threshold); @@ -1195,5 +1195,34 @@ Transaction.prototype.isCoinbase = function() { return (this.inputs.length === 1 && this.inputs[0].isNull()); }; +/** + * Determines if this transaction can be replaced in the mempool with another + * transaction that provides a sufficiently higher fee (RBF). + */ +Transaction.prototype.isRBF = function() { + for (var i = 0; i < this.inputs.length; i++) { + var input = this.inputs[i]; + if (input.sequenceNumber < Input.MAXINT - 1) { + return true; + } + } + return false; +}; + +/** + * Enable this transaction to be replaced in the mempool (RBF) if a transaction + * includes a sufficiently higher fee. It will set the sequenceNumber to + * DEFAULT_RBF_SEQNUMBER for all inputs if the sequence number does not + * already enable RBF. + */ +Transaction.prototype.enableRBF = function() { + for (var i = 0; i < this.inputs.length; i++) { + var input = this.inputs[i]; + if (input.sequenceNumber >= Input.MAXINT - 1) { + input.sequenceNumber = Input.DEFAULT_RBF_SEQNUMBER; + } + } + return this; +}; module.exports = Transaction; diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 9389078bc..7c0d992c3 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -180,7 +180,7 @@ describe('Transaction', function() { var simpleUtxoWith1BTC = { address: fromAddress, txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', - outputIndex: 0, + outputIndex: 1, script: Script.buildPublicKeyHashOut(fromAddress).toString(), satoshis: 1e8 }; @@ -1086,6 +1086,106 @@ describe('Transaction', function() { }); }); + describe('Replace-by-fee', function() { + describe('#enableRBF', function() { + it('only enable inputs not already enabled (0xffffffff)', function() { + var tx = new Transaction() + .from(simpleUtxoWith1BTC) + .from(simpleUtxoWith100000Satoshis) + .to([{address: toAddress, satoshis: 50000}]) + .fee(15000) + .change(changeAddress) + .sign(privateKey); + tx.inputs[0].sequenceNumber = 0x00000000; + tx.enableRBF(); + tx.inputs[0].sequenceNumber.should.equal(0x00000000); + tx.inputs[1].sequenceNumber.should.equal(0xfffffffd); + }); + it('enable for inputs with 0xffffffff and 0xfffffffe', function() { + var tx = new Transaction() + .from(simpleUtxoWith1BTC) + .from(simpleUtxoWith100000Satoshis) + .to([{address: toAddress, satoshis: 50000}]) + .fee(15000) + .change(changeAddress) + .sign(privateKey); + tx.inputs[0].sequenceNumber = 0xffffffff; + tx.inputs[1].sequenceNumber = 0xfffffffe; + tx.enableRBF(); + tx.inputs[0].sequenceNumber.should.equal(0xfffffffd); + tx.inputs[1].sequenceNumber.should.equal(0xfffffffd); + }); + }); + describe('#isRBF', function() { + it('enable and determine opt-in', function() { + var tx = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to([{address: toAddress, satoshis: 50000}]) + .fee(15000) + .change(changeAddress) + .enableRBF() + .sign(privateKey); + tx.isRBF().should.equal(true); + }); + it('determine opt-out with default sequence number', function() { + var tx = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to([{address: toAddress, satoshis: 50000}]) + .fee(15000) + .change(changeAddress) + .sign(privateKey); + tx.isRBF().should.equal(false); + }); + it('determine opt-out with 0xfffffffe', function() { + var tx = new Transaction() + .from(simpleUtxoWith1BTC) + .from(simpleUtxoWith100000Satoshis) + .to([{address: toAddress, satoshis: 50000 + 1e8}]) + .fee(15000) + .change(changeAddress) + .sign(privateKey); + tx.inputs[0].sequenceNumber = 0xfffffffe; + tx.inputs[1].sequenceNumber = 0xfffffffe; + tx.isRBF().should.equal(false); + }); + it('determine opt-out with 0xffffffff', function() { + var tx = new Transaction() + .from(simpleUtxoWith1BTC) + .from(simpleUtxoWith100000Satoshis) + .to([{address: toAddress, satoshis: 50000 + 1e8}]) + .fee(15000) + .change(changeAddress) + .sign(privateKey); + tx.inputs[0].sequenceNumber = 0xffffffff; + tx.inputs[1].sequenceNumber = 0xffffffff; + tx.isRBF().should.equal(false); + }); + it('determine opt-in with 0xfffffffd (first input)', function() { + var tx = new Transaction() + .from(simpleUtxoWith1BTC) + .from(simpleUtxoWith100000Satoshis) + .to([{address: toAddress, satoshis: 50000 + 1e8}]) + .fee(15000) + .change(changeAddress) + .sign(privateKey); + tx.inputs[0].sequenceNumber = 0xfffffffd; + tx.inputs[1].sequenceNumber = 0xffffffff; + tx.isRBF().should.equal(true); + }); + it('determine opt-in with 0xfffffffd (second input)', function() { + var tx = new Transaction() + .from(simpleUtxoWith1BTC) + .from(simpleUtxoWith100000Satoshis) + .to([{address: toAddress, satoshis: 50000 + 1e8}]) + .fee(15000) + .change(changeAddress) + .sign(privateKey); + tx.inputs[0].sequenceNumber = 0xffffffff; + tx.inputs[1].sequenceNumber = 0xfffffffd; + tx.isRBF().should.equal(true); + }); + }); + }); });