Modify transaction interface

* Add checks when serializing
* Add default _estimateSize to generic inputs
* Fix multisig size estimation
* Change _addOutput to addOutput
* Add addInput and using that internally
* Split `getFee` out from `_updateChangeOutput`
This commit is contained in:
Esteban Ordano 2014-12-18 12:28:43 -03:00
parent 3f1ddd68f2
commit e5631b1a69
6 changed files with 116 additions and 21 deletions

View File

@ -34,6 +34,18 @@ module.exports = [{
}, { }, {
name: 'InvalidArgumentType', name: 'InvalidArgumentType',
message: format('Invalid Argument for {2}, expected {1} but got ') + '+ typeof arguments[0]', message: format('Invalid Argument for {2}, expected {1} but got ') + '+ typeof arguments[0]',
}, {
name: 'Transaction',
message: format('Internal Error on Transaction {0}'),
errors: [
{
name: 'FeeError',
message: format('Fees are not correctly set {0}'),
}, {
name: 'ChangeAddressMissing',
message: format('Change address is missing')
}
]
}, { }, {
name: 'Script', name: 'Script',
message: format('Internal Error on Script {0}'), message: format('Internal Error on Script {0}'),

View File

@ -156,4 +156,10 @@ Input.prototype.isNull = function() {
this.outputIndex === 0xffffffff; this.outputIndex === 0xffffffff;
}; };
Input.prototype._estimateSize = function() {
var bufferWriter = new BufferWriter();
this.toBufferWriter(bufferWriter);
return bufferWriter.toBuffer().length;
};
module.exports = Input; module.exports = Input;

View File

@ -1,7 +1,6 @@
'use strict'; 'use strict';
var _ = require('lodash'); var _ = require('lodash');
var JSUtil = require('../../util/js');
var inherits = require('inherits'); var inherits = require('inherits');
var Input = require('./input'); var Input = require('./input');
var Output = require('../output'); var Output = require('../output');
@ -123,11 +122,14 @@ MultiSigScriptHashInput.prototype.isValidSignature = function(transaction, signa
); );
}; };
MultiSigScriptHashInput.OPCODES_SIZE = 10; MultiSigScriptHashInput.OPCODES_SIZE = 7; // serialized size (<=3) + 0 .. N .. M OP_CHECKMULTISIG
MultiSigScriptHashInput.SIGNATURE_SIZE = 36; MultiSigScriptHashInput.SIGNATURE_SIZE = 74; // size (1) + DER (<=72) + sighash (1)
MultiSigScriptHashInput.PUBKEY_SIZE = 34; // size (1) + DER (<=33)
MultiSigScriptHashInput.prototype._estimateSize = function() { MultiSigScriptHashInput.prototype._estimateSize = function() {
return MultiSigScriptHashInput.OPCODES_SIZE + this.threshold * MultiSigScriptHashInput.SIGNATURE_SIZE; return MultiSigScriptHashInput.OPCODES_SIZE +
this.threshold * MultiSigScriptHashInput.SIGNATURE_SIZE +
this.publicKeys.length * MultiSigScriptHashInput.PUBKEY_SIZE;
}; };
module.exports = MultiSigScriptHashInput; module.exports = MultiSigScriptHashInput;

View File

@ -85,10 +85,10 @@ PublicKeyHashInput.prototype.isFullySigned = function() {
return this.script.isPublicKeyHashIn(); return this.script.isPublicKeyHashIn();
}; };
PublicKeyHashInput.FIXED_SIZE = 32 + 4 + 2; PublicKeyHashInput.SCRIPT_MAX_SIZE = 73 + 34; // sigsize (1 + 72) + pubkey (1 + 33)
PublicKeyHashInput.SCRIPT_MAX_SIZE = 34 + 20;
PublicKeyHashInput.prototype._estimateSize = function() { PublicKeyHashInput.prototype._estimateSize = function() {
return PublicKeyHashInput.FIXED_SIZE + PublicKeyHashInput.SCRIPT_MAX_SIZE; return PublicKeyHashInput.SCRIPT_MAX_SIZE;
}; };
module.exports = PublicKeyHashInput; module.exports = PublicKeyHashInput;

View File

@ -3,8 +3,8 @@
var _ = require('lodash'); var _ = require('lodash');
var $ = require('../util/preconditions'); var $ = require('../util/preconditions');
var buffer = require('buffer'); var buffer = require('buffer');
var assert = require('assert');
var errors = require('../errors');
var util = require('../util/js'); var util = require('../util/js');
var bufferUtil = require('../util/buffer'); var bufferUtil = require('../util/buffer');
var JSUtil = require('../util/js'); var JSUtil = require('../util/js');
@ -98,12 +98,46 @@ Transaction.prototype._getHash = function() {
* Retrieve a hexa string that can be used with bitcoind's CLI interface * Retrieve a hexa string that can be used with bitcoind's CLI interface
* (decoderawtransaction, sendrawtransaction) * (decoderawtransaction, sendrawtransaction)
* *
* @param {boolean=} unsafe if true, skip testing for fees that are too high
* @return {string} * @return {string}
*/ */
Transaction.prototype.serialize = Transaction.prototype.toString = function() { Transaction.prototype.serialize = function(unsafe) {
if (unsafe) {
return this.uncheckedSerialize();
} else {
return this.checkedSerialize();
}
};
Transaction.prototype.uncheckedSerialize = Transaction.prototype.toString = function() {
return this.toBuffer().toString('hex'); return this.toBuffer().toString('hex');
}; };
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);
}
}
return this.uncheckedSerialize();
};
Transaction.prototype._validateFees = function() {
if (this.getFee() > Transaction.FEE_SECURITY_MARGIN * this._estimateFee()) {
return 'Fee is more than ' + Transaction.FEE_SECURITY_MARGIN + ' times the suggested amount';
}
};
Transaction.prototype._validateChange = function() {
if (!this._change) {
return 'Missing change address';
}
};
Transaction.prototype.inspect = function() { Transaction.prototype.inspect = function() {
return '<Transaction: ' + this.toString() + '>'; return '<Transaction: ' + this.toString() + '>';
}; };
@ -327,10 +361,9 @@ Transaction.prototype._fromMultisigOldUtxo = function(utxo, pubkeys, threshold)
}; };
Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) { Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) {
this._changeSetup = false;
utxo.address = utxo.address && new Address(utxo.address); utxo.address = utxo.address && new Address(utxo.address);
utxo.script = new Script(util.isHexa(utxo.script) ? new buffer.Buffer(utxo.script, 'hex') : utxo.script); utxo.script = new Script(util.isHexa(utxo.script) ? new buffer.Buffer(utxo.script, 'hex') : utxo.script);
this.inputs.push(new MultiSigScriptHashInput({ this.addInput(new MultiSigScriptHashInput({
output: new Output({ output: new Output({
script: utxo.script, script: utxo.script,
satoshis: utxo.satoshis satoshis: utxo.satoshis
@ -340,7 +373,35 @@ Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold)
sequenceNumber: DEFAULT_SEQNUMBER, sequenceNumber: DEFAULT_SEQNUMBER,
script: Script.empty() script: Script.empty()
}, pubkeys, threshold)); }, pubkeys, threshold));
this._inputAmount += utxo.satoshis; };
/**
* Add an input to this transaction. The input must be an instance of the `Input` class.
* It should have information about the Output that it's spending, but if it's not already
* set, two additional parameters, `outputScript` and `satoshis` can be provided.
*
* @param {Input} input
* @param {String|Script} outputScript
* @param {number} satoshis
* @return Transaction this, for chaining
*/
Transaction.prototype.addInput = function(input, outputScript, satoshis) {
$.checkArgumentType(input, Input, 'input');
if (!input.output || !(input.output instanceof Output) && !outputScript && !satoshis) {
throw new Transaction.NeedMoreInfo('Need information about the UTXO script and satoshis');
}
if (!input.output && outputScript && satoshis) {
outputScript = outputScript instanceof Script ? outputScript : new Script(outputScript);
$.checkArgumentType(satoshis, 'number', 'satoshis');
input.output = new Output({
script: outputScript,
satoshis: satoshis
});
}
this._changeSetup = false;
this.inputs.push(input);
this._inputAmount += input.output.satoshis;
return this;
}; };
/** /**
@ -396,7 +457,7 @@ Transaction.prototype.change = function(address) {
* @return {Transaction} this, for chaining * @return {Transaction} this, for chaining
*/ */
Transaction.prototype.to = function(address, amount) { Transaction.prototype.to = function(address, amount) {
this._addOutput(new Output({ this.addOutput(new Output({
script: Script(new Address(address)), script: Script(new Address(address)),
satoshis: amount satoshis: amount
})); }));
@ -414,14 +475,15 @@ Transaction.prototype.to = function(address, amount) {
* @return {Transaction} this, for chaining * @return {Transaction} this, for chaining
*/ */
Transaction.prototype.addData = function(value) { Transaction.prototype.addData = function(value) {
this._addOutput(new Output({ this.addOutput(new Output({
script: Script.buildDataOut(value), script: Script.buildDataOut(value),
satoshis: 0 satoshis: 0
})); }));
return this; return this;
}; };
Transaction.prototype._addOutput = function(output) { Transaction.prototype.addOutput = function(output) {
$.checkArgumentType(output, Output, 'output');
this.outputs.push(output); this.outputs.push(output);
this._changeSetup = false; this._changeSetup = false;
this._outputAmount += output.satoshis; this._outputAmount += output.satoshis;
@ -440,12 +502,11 @@ Transaction.prototype._updateChangeOutput = function() {
if (!_.isUndefined(this._changeOutput)) { if (!_.isUndefined(this._changeOutput)) {
this.removeOutput(this._changeOutput); this.removeOutput(this._changeOutput);
} }
var estimatedSize = this._estimateSize(); var available = this._getUnspentValue();
var available = this._inputAmount - this._outputAmount; var fee = this.getFee();
var fee = this._fee || Transaction._estimateFee(estimatedSize, available);
if (available - fee > 0) { if (available - fee > 0) {
this._changeOutput = this.outputs.length; this._changeOutput = this.outputs.length;
this._addOutput(new Output({ this.addOutput(new Output({
script: Script.fromAddress(this._change), script: Script.fromAddress(this._change),
satoshis: available - fee satoshis: available - fee
})); }));
@ -455,6 +516,20 @@ Transaction.prototype._updateChangeOutput = function() {
this._changeSetup = true; this._changeSetup = true;
}; };
Transaction.prototype.getFee = function() {
return this._fee || this._estimateFee();
};
Transaction.prototype._estimateFee = function() {
var estimatedSize = this._estimateSize();
var available = this._getUnspentValue();
return Transaction._estimateFee(estimatedSize, available);
};
Transaction.prototype._getUnspentValue = function() {
return this._inputAmount - this._outputAmount;
};
Transaction.prototype._clearSignatures = function() { Transaction.prototype._clearSignatures = function() {
_.each(this.inputs, function(input) { _.each(this.inputs, function(input) {
input.clearSignatures(); input.clearSignatures();

View File

@ -208,7 +208,7 @@ describe('Interpreter', function() {
sequenceNumber: 0xffffffff, sequenceNumber: 0xffffffff,
script: Script('OP_0 OP_0') script: Script('OP_0 OP_0')
})); }));
credtx._addOutput(new Transaction.Output({ credtx.addOutput(new Transaction.Output({
script: scriptPubkey, script: scriptPubkey,
satoshis: 0 satoshis: 0
})); }));
@ -221,7 +221,7 @@ describe('Interpreter', function() {
sequenceNumber: 0xffffffff, sequenceNumber: 0xffffffff,
script: scriptSig script: scriptSig
})); }));
spendtx._addOutput(new Transaction.Output({ spendtx.addOutput(new Transaction.Output({
script: Script(), script: Script(),
satoshis: 0 satoshis: 0
})); }));