fix bug in change calculation

This commit is contained in:
Manuel Araoz 2015-02-10 18:12:45 -03:00
parent 986264e181
commit aa1158097d
4 changed files with 104 additions and 59 deletions

View File

@ -112,11 +112,11 @@ When outputs' value don't sum up to the same amount that inputs, the difference
For this reason, some methods in the Transaction class are provided:
* `change(address)`: Set up the change address. This will set an internal `_change` property that will store the change address.
* `change(address)`: Set up the change address. This will set an internal `_changeScript` property that will store the change script associated with that address.
* `fee(amount)`: Sets up the exact amount of fee to pay. If no change address is provided, this will raise an exception.
* `getFee()`: returns the estimated fee amount to be paid, based on the size of the transaction, but disregarding the priority of the outputs.
Internally, a `_changeOutput` property stores the index of the change output (so it can get updated when a new input or output is added).
Internally, a `_changeIndex` property stores the index of the change output (so it can get updated when a new input or output is added).
## Multisig Transactions

View File

@ -36,15 +36,14 @@ Input.prototype._fromObject = function(params) {
params.prevTxId = new buffer.Buffer(params.prevTxId, 'hex');
}
this.output = params.output ?
(params.output instanceof Output ? params.output : new Output(params.output)) : undefined;
(params.output instanceof Output ? params.output : new Output(params.output)) : undefined;
this.prevTxId = params.prevTxId;
this.outputIndex = params.outputIndex;
this.sequenceNumber = params.sequenceNumber;
if (!_.isUndefined(params.script) || !_.isUndefined(params.scriptBuffer)) {
this.setScript(_.isUndefined(params.script) ? params.scriptBuffer : params.script);
} else {
if (_.isUndefined(params.script) && _.isUndefined(params.scriptBuffer)) {
throw new errors.Transaction.Input.MissingScript();
}
this.setScript(params.scriptBuffer || params.script);
return this;
};

View File

@ -126,13 +126,12 @@ Transaction.prototype.uncheckedSerialize = function() {
Transaction.prototype.checkedSerialize = Transaction.prototype.toString = function() {
var feeError = this._validateFees();
if (feeError) {
var changeError = this._validateChange();
if (changeError) {
throw new errors.Transaction.ChangeAddressMissing();
} else {
throw new errors.Transaction.FeeError(feeError);
}
var missingChange = this._missingChange();
if (feeError && missingChange) {
throw new errors.Transaction.ChangeAddressMissing();
}
if (feeError && !missingChange) {
throw new errors.Transaction.FeeError(feeError);
}
if (this._hasDustOutputs()) {
throw new errors.Transaction.DustOutputs();
@ -148,10 +147,8 @@ Transaction.prototype._validateFees = function() {
}
};
Transaction.prototype._validateChange = function() {
if (!this._change) {
return 'Missing change address';
}
Transaction.prototype._missingChange = function() {
return !this._changeScript;
};
Transaction.DUST_AMOUNT = 5460;
@ -217,23 +214,7 @@ Transaction.prototype.fromJSON = function(json) {
if (JSUtil.isValidJSON(json)) {
json = JSON.parse(json);
}
var self = this;
this.inputs = [];
var inputs = json.inputs || json.txins;
inputs.forEach(function(input) {
self.inputs.push(Input.fromJSON(input));
});
this.outputs = [];
var outputs = json.outputs || json.txouts;
outputs.forEach(function(output) {
self.outputs.push(Output.fromJSON(output));
});
if (json.change) {
this.change(json.change);
}
this.version = json.version;
this.nLockTime = json.nLockTime;
return this;
return this.fromObject(json);
};
Transaction.prototype.toObject = function toObject() {
@ -246,7 +227,8 @@ Transaction.prototype.toObject = function toObject() {
outputs.push(output.toObject());
});
return {
change: this._change ? this._change.toString() : undefined,
changeScript: this._changeScript ? this._changeScript.toString() : undefined,
changeIndex: !_.isUndefined(this._changeIndex) ? this._changeIndex : undefined,
fee: this._fee ? this._fee : undefined,
version: this.version,
inputs: inputs,
@ -275,21 +257,36 @@ Transaction.prototype.fromObject = function(transaction) {
_.each(transaction.outputs, function(output) {
self.addOutput(new Output(output));
});
if (transaction.change) {
this.change(transaction.change);
if (transaction.changeIndex) {
this._changeIndex = transaction.changeIndex;
}
if (transaction.changeScript) {
this._changeScript = new Script(transaction.changeScript);
}
if (transaction.fee) {
this.fee(transaction.fee);
}
this.nLockTime = transaction.nLockTime;
this.version = transaction.version;
this._checkConsistency();
return this;
};
Transaction.prototype._checkConsistency = function() {
if (!_.isUndefined(this._changeIndex)) {
$.checkState(this._changeScript);
$.checkState(this.outputs[this._changeIndex]);
$.checkState(this.outputs[this._changeIndex].script.toString() ===
this._changeScript.toString());
}
// TODO: add other checks
};
/**
* Sets nLockTime so that transaction is not valid until the desired date(a
* timestamp in seconds since UNIX epoch is also accepted)
*
* @param {Date | Number} time
* @param {Date | Number} time
* @return {Transaction} this
*/
Transaction.prototype.lockUntilDate = function(time) {
@ -337,7 +334,7 @@ Transaction.prototype.getLockTime = function() {
if (this.nLockTime < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) {
return this.nLockTime;
}
return new Date(1000*this.nLockTime);
return new Date(1000 * this.nLockTime);
};
Transaction.prototype.toJSON = function toJSON() {
@ -533,11 +530,22 @@ Transaction.prototype.fee = function(amount) {
* @return {Transaction} this, for chaining
*/
Transaction.prototype.change = function(address) {
this._change = new Address(address);
this._changeScript = Script.fromAddress(address);
this._updateChangeOutput();
return this;
};
/**
* @return {Output} change output, if it exists
*/
Transaction.prototype.getChangeOutput = function() {
if (!_.isUndefined(this._changeIndex)) {
return this.outputs[this._changeIndex];
}
return null;
};
/**
* Add an output to the transaction.
*
@ -586,33 +594,46 @@ Transaction.prototype._addOutput = function(output) {
};
Transaction.prototype._updateChangeOutput = function() {
if (!this._change) {
if (!this._changeScript) {
return;
}
this._clearSignatures();
if (!_.isUndefined(this._changeOutput)) {
this._removeOutput(this._changeOutput);
if (!_.isUndefined(this._changeIndex)) {
this._removeOutput(this._changeIndex);
}
var available = this._getUnspentValue();
var fee = this.getFee();
if (available - fee > 0) {
this._changeOutput = this.outputs.length;
var changeAmount = available - fee;
if (changeAmount > 0) {
this._changeIndex = this.outputs.length;
this._addOutput(new Output({
script: Script.fromAddress(this._change),
satoshis: available - fee
script: this._changeScript,
satoshis: changeAmount
}));
} else {
this._changeOutput = undefined;
this._changeIndex = undefined;
}
};
/**
* Calculates the fees for the transaction.
*
* If there is no change output set, the fee will be the
* output amount minus the input amount.
* If there's a fixed fee set, return that
* If there's no fee set, estimate it based on size
* @return {Number} miner fee for this transaction in satoshis
*/
Transaction.prototype.getFee = function() {
if (!this._change) {
// if no change output is set, fees should equal all the unspent amount
if (!this._changeScript) {
return this._getUnspentValue();
}
return this._fee || this._estimateFee();
};
/**
* Estimates fee from serialized transaction size in bytes.
*/
Transaction.prototype._estimateFee = function() {
var estimatedSize = this._estimateSize();
var available = this._getUnspentValue();
@ -630,12 +651,12 @@ Transaction.prototype._clearSignatures = function() {
};
Transaction.FEE_PER_KB = 10000;
// Safe upper bound for change address script
Transaction.CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4;
Transaction._estimateFee = function(size, amountAvailable) {
var fee = Math.ceil(size / Transaction.FEE_PER_KB);
if (amountAvailable > fee) {
// Safe upper bound for change address script
size += Transaction.CHANGE_OUTPUT_MAX_SIZE;
}
return Math.ceil(size / 1000) * Transaction.FEE_PER_KB;

View File

@ -142,7 +142,16 @@ describe('Transaction', function() {
tx.inputs.length.should.equal(1);
});
describe('not enough information errors', function() {
describe('isFullySigned', function() {
it('works for normal p2pkh', function() {
var transaction = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 50000)
.change(changeAddress)
.sign(privateKey);
transaction.isFullySigned().should.equal(true);
});
it('fails when Inputs are not subclassed and isFullySigned is called', function() {
var tx = new Transaction(tx_1_hex);
expect(function() {
@ -172,6 +181,7 @@ describe('Transaction', function() {
transaction.outputs[1].satoshis.should.equal(40000);
transaction.outputs[1].script.toString()
.should.equal(Script.fromAddress(changeAddress).toString());
transaction.getChangeOutput().script.should.deep.equal(Script.fromAddress(changeAddress));
});
it('accepts a P2SH address for change', function() {
var transaction = new Transaction()
@ -243,7 +253,8 @@ describe('Transaction', function() {
.change(changeAddress)
.toObject();
var deserialized = new Transaction(serialized);
expect(deserialized._change.toString()).to.equal(changeAddress);
expect(deserialized._changeScript.toString()).to.equal(Script.fromAddress(changeAddress).toString());
expect(deserialized.getChangeOutput()).to.equal(null);
});
it('can avoid checked serialize', function() {
var transaction = new Transaction()
@ -309,13 +320,23 @@ describe('Transaction', function() {
describe('to and from JSON', function() {
it('takes a string that is a valid JSON and deserializes from it', function() {
var transaction = new Transaction();
expect(new Transaction(transaction.toJSON()).serialize()).to.equal(transaction.serialize());
var simple = new Transaction();
expect(new Transaction(simple.toJSON()).serialize()).to.equal(simple.serialize());
var complex = new Transaction()
.from(simpleUtxoWith100000Satoshis)
.to(toAddress, 50000)
.change(changeAddress)
.sign(privateKey);
var cj = complex.toJSON();
var ctx = new Transaction(cj);
expect(ctx.serialize()).to.equal(complex.serialize());
});
it('serializes the `change` information', function() {
var transaction = new Transaction();
transaction.change(changeAddress);
expect(JSON.parse(transaction.toJSON()).change).to.equal(changeAddress.toString());
expect(JSON.parse(transaction.toJSON()).changeScript).to.equal(Script.fromAddress(changeAddress).toString());
expect(new Transaction(transaction.toJSON()).serialize()).to.equal(transaction.serialize());
});
it('serializes correctly p2sh multisig signed tx', function() {
var t = new Transaction(tx2hex);
@ -413,11 +434,15 @@ describe('Transaction', function() {
var timestamp = 1423504946;
var blockHeight = 342734;
var date = new Date(timestamp * MILLIS_IN_SECOND);
it('handles a null locktime', function() {
var transaction = new Transaction();
expect(transaction.getLockTime()).to.equal(null);
});
it('handles a simple example', function() {
var future = new Date(2025,10,30); // Sun Nov 30 2025
var future = new Date(2025, 10, 30); // Sun Nov 30 2025
var transaction = new Transaction()
.lockUntilDate(future);
transaction.nLockTime.should.equal(future.getTime()/1000);
transaction.nLockTime.should.equal(future.getTime() / 1000);
transaction.getLockTime().should.deep.equal(future);
});
it('accepts a date instance', function() {
@ -430,7 +455,7 @@ describe('Transaction', function() {
var transaction = new Transaction()
.lockUntilDate(timestamp);
transaction.nLockTime.should.equal(timestamp);
transaction.getLockTime().should.deep.equal(new Date(timestamp*1000));
transaction.getLockTime().should.deep.equal(new Date(timestamp * 1000));
});
it('accepts a block height', function() {
var transaction = new Transaction()