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:
parent
6e225ef70e
commit
f1d19b438e
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue