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
This commit is contained in:
Braydon Fuller 2015-12-02 22:12:04 -05:00
parent 6e225ef70e
commit f1d19b438e
3 changed files with 137 additions and 5 deletions

View File

@ -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,

View File

@ -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;

View File

@ -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);
});
});
});
});