Skip to content

Commit

Permalink
Merge pull request #812 from eordano/feature/safeSerialize
Browse files Browse the repository at this point in the history
Modify Transaction Interface
  • Loading branch information
maraoz committed Dec 19, 2014
2 parents 6171e67 + 99db72b commit 85169a3
Show file tree
Hide file tree
Showing 12 changed files with 469 additions and 100 deletions.
22 changes: 22 additions & 0 deletions lib/errors/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ module.exports = [{
}, {
name: 'InvalidArgumentType',
message: format('Invalid Argument for {2}, expected {1} but got ') + '+ typeof arguments[0]',
}, {
name: 'Transaction',
message: format('Internal Error on Transaction {0}'),
errors: [
{
name: 'Input',
message: format('Internal Error on Input {0}'),
errors: [{
name: 'MissingScript',
message: format('Need a script to create an input')
}]
}, {
name: 'NeedMoreInfo',
message: format('{0}')
}, {
name: 'FeeError',
message: format('Fees are not correctly set {0}'),
}, {
name: 'ChangeAddressMissing',
message: format('Change address is missing')
}
]
}, {
name: 'Script',
message: format('Internal Error on Script {0}'),
Expand Down
24 changes: 19 additions & 5 deletions lib/transaction/input/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var BufferUtil = require('../../util/buffer');
var JSUtil = require('../../util/js');
var Script = require('../../script');
var Sighash = require('../sighash');
var Output = require('../output');

function Input(params) {
if (!(this instanceof Input)) {
Expand All @@ -33,12 +34,15 @@ Input.prototype._fromObject = function(params) {
if (_.isString(params.prevTxId) && JSUtil.isHexa(params.prevTxId)) {
params.prevTxId = new buffer.Buffer(params.prevTxId, 'hex');
}
this.output = params.output;
this.output = params.output ?
(params.output instanceof Output ? params.output : new Output(params.output)) : undefined;
this.prevTxId = params.prevTxId;
this.outputIndex = params.outputIndex;
this.sequenceNumber = params.sequenceNumber;
if (params.script || params.scriptBuffer) {
this.setScript(params.script || params.scriptBuffer);
if (!_.isUndefined(params.script) || !_.isUndefined(params.scriptBuffer)) {
this.setScript(_.isUndefined(params.script) ? params.scriptBuffer : params.script);
} else {
throw new errors.Transaction.Input.MissingScript();
}
return this;
};
Expand All @@ -48,7 +52,8 @@ Input.prototype.toObject = function toObject() {
prevTxId: this.prevTxId.toString('hex'),
outputIndex: this.outputIndex,
sequenceNumber: this.sequenceNumber,
script: this._script.toString()
script: this.script.toString(),
output: this.output ? this.output.toObject() : undefined
};
};

Expand All @@ -61,6 +66,7 @@ Input.fromJSON = function(json) {
json = JSON.parse(json);
}
return new Input({
output: json.output ? new Output(json.output) : undefined,
prevTxId: json.prevTxId || json.txidbuf,
outputIndex: _.isUndefined(json.outputIndex) ? json.txoutnum : json.outputIndex,
sequenceNumber: json.sequenceNumber || json.seqnum,
Expand Down Expand Up @@ -99,11 +105,13 @@ Input.prototype.setScript = function(script) {
if (script instanceof Script) {
this._script = script;
this._scriptBuffer = script.toBuffer();
} else if (_.isString(script)) {
this._script = new Script(script);
this._scriptBuffer = this._script.toBuffer();
} else if (BufferUtil.isBuffer(script)) {
this._script = null;
this._scriptBuffer = new buffer.Buffer(script);
} else {
console.log(script);
throw new TypeError('Invalid Argument');
}
return this;
Expand Down Expand Up @@ -156,4 +164,10 @@ Input.prototype.isNull = function() {
this.outputIndex === 0xffffffff;
};

Input.prototype._estimateSize = function() {
var bufferWriter = new BufferWriter();
this.toBufferWriter(bufferWriter);
return bufferWriter.toBuffer().length;
};

module.exports = Input;
10 changes: 6 additions & 4 deletions lib/transaction/input/multisigscripthash.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

var _ = require('lodash');
var JSUtil = require('../../util/js');
var inherits = require('inherits');
var Input = require('./input');
var Output = require('../output');
Expand Down Expand Up @@ -123,11 +122,14 @@ MultiSigScriptHashInput.prototype.isValidSignature = function(transaction, signa
);
};

MultiSigScriptHashInput.OPCODES_SIZE = 10;
MultiSigScriptHashInput.SIGNATURE_SIZE = 36;
MultiSigScriptHashInput.OPCODES_SIZE = 7; // serialized size (<=3) + 0 .. N .. M OP_CHECKMULTISIG
MultiSigScriptHashInput.SIGNATURE_SIZE = 74; // size (1) + DER (<=72) + sighash (1)
MultiSigScriptHashInput.PUBKEY_SIZE = 34; // size (1) + DER (<=33)

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;
6 changes: 3 additions & 3 deletions lib/transaction/input/publickeyhash.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ PublicKeyHashInput.prototype.isFullySigned = function() {
return this.script.isPublicKeyHashIn();
};

PublicKeyHashInput.FIXED_SIZE = 32 + 4 + 2;
PublicKeyHashInput.SCRIPT_MAX_SIZE = 34 + 20;
PublicKeyHashInput.SCRIPT_MAX_SIZE = 73 + 34; // sigsize (1 + 72) + pubkey (1 + 33)

PublicKeyHashInput.prototype._estimateSize = function() {
return PublicKeyHashInput.FIXED_SIZE + PublicKeyHashInput.SCRIPT_MAX_SIZE;
return PublicKeyHashInput.SCRIPT_MAX_SIZE;
};

module.exports = PublicKeyHashInput;
6 changes: 5 additions & 1 deletion lib/transaction/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Output.prototype._fromObject = function(param) {
Output.prototype.toObject = function toObject() {
return {
satoshis: this.satoshis,
script: this._script.toString()
script: this.script.toString()
};
};

Expand All @@ -76,10 +76,14 @@ Output.prototype.setScript = function(script) {
if (script instanceof Script) {
this._scriptBuffer = script.toBuffer();
this._script = script;
} else if (_.isString(script)) {
this._script = new Script(script);
this._scriptBuffer = this._script.toBuffer();
} else if (bufferUtil.isBuffer(script)) {
this._scriptBuffer = script;
this._script = null;
} else {
console.log(script);
throw new TypeError('Unrecognized Argument');
}
return this;
Expand Down
131 changes: 118 additions & 13 deletions lib/transaction/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
var _ = require('lodash');
var $ = require('../util/preconditions');
var buffer = require('buffer');
var assert = require('assert');

var errors = require('../errors');
var util = require('../util/js');
var bufferUtil = require('../util/buffer');
var JSUtil = require('../util/js');
Expand Down Expand Up @@ -53,6 +53,8 @@ function Transaction(serialized) {
this.fromBuffer(serialized);
} else if (_.isObject(serialized)) {
this.fromObject(serialized);
} else {
throw new errors.InvalidArgument('Must provide an object or string to deserialize a transaction');
}
} else {
this._newTransaction();
Expand Down Expand Up @@ -98,12 +100,48 @@ Transaction.prototype._getHash = function() {
* Retrieve a hexa string that can be used with bitcoind's CLI interface
* (decoderawtransaction, sendrawtransaction)
*
* @param {boolean=} unsafe if true, skip testing for fees that are too high
* @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');
};

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.FEE_SECURITY_MARGIN = 15;

Transaction.prototype._validateFees = function() {
if (this._getUnspentValue() > 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() {
return '<Transaction: ' + this.toString() + '>';
};
Expand Down Expand Up @@ -186,6 +224,18 @@ Transaction.prototype.toObject = function toObject() {
};
};

Transaction.prototype.fromObject = function(transaction) {
var self = this;
_.each(transaction.inputs, function(input) {
self.addInput(new Input(input));
});
_.each(transaction.outputs, function(output) {
self.addOutput(new Output(output));
});
this.nLockTime = transaction.nLockTime;
this.version = transaction.version;
};

Transaction.prototype.toJSON = function toJSON() {
return JSON.stringify(this.toObject());
};
Expand Down Expand Up @@ -329,10 +379,9 @@ Transaction.prototype._fromMultisigOldUtxo = function(utxo, pubkeys, threshold)
};

Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) {
this._changeSetup = false;
utxo.address = utxo.address && new Address(utxo.address);
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({
script: utxo.script,
satoshis: utxo.satoshis
Expand All @@ -342,7 +391,49 @@ Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold)
sequenceNumber: DEFAULT_SEQNUMBER,
script: Script.empty()
}, 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 errors.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
});
}
return this.uncheckedAddInput(input);
};

/**
* Add an input to this transaction, without checking that the input has information about
* the output that it's spending.
*
* @param {Input} input
* @return Transaction this, for chaining
*/
Transaction.prototype.uncheckedAddInput = function(input) {
$.checkArgumentType(input, Input, 'input');
this._changeSetup = false;
this.inputs.push(input);
if (input.output) {
this._inputAmount += input.output.satoshis;
}
return this;
};

/**
Expand Down Expand Up @@ -398,7 +489,7 @@ Transaction.prototype.change = function(address) {
* @return {Transaction} this, for chaining
*/
Transaction.prototype.to = function(address, amount) {
this._addOutput(new Output({
this.addOutput(new Output({
script: Script(new Address(address)),
satoshis: amount
}));
Expand All @@ -416,14 +507,15 @@ Transaction.prototype.to = function(address, amount) {
* @return {Transaction} this, for chaining
*/
Transaction.prototype.addData = function(value) {
this._addOutput(new Output({
this.addOutput(new Output({
script: Script.buildDataOut(value),
satoshis: 0
}));
return this;
};

Transaction.prototype._addOutput = function(output) {
Transaction.prototype.addOutput = function(output) {
$.checkArgumentType(output, Output, 'output');
this.outputs.push(output);
this._changeSetup = false;
this._outputAmount += output.satoshis;
Expand All @@ -442,12 +534,11 @@ Transaction.prototype._updateChangeOutput = function() {
if (!_.isUndefined(this._changeOutput)) {
this.removeOutput(this._changeOutput);
}
var estimatedSize = this._estimateSize();
var available = this._inputAmount - this._outputAmount;
var fee = this._fee || Transaction._estimateFee(estimatedSize, available);
var available = this._getUnspentValue();
var fee = this.getFee();
if (available - fee > 0) {
this._changeOutput = this.outputs.length;
this._addOutput(new Output({
this.addOutput(new Output({
script: Script.fromAddress(this._change),
satoshis: available - fee
}));
Expand All @@ -457,6 +548,20 @@ Transaction.prototype._updateChangeOutput = function() {
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() {
_.each(this.inputs, function(input) {
input.clearSignatures();
Expand All @@ -472,7 +577,7 @@ Transaction._estimateFee = function(size, amountAvailable) {
// Safe upper bound for change address script
size += Transaction.CHANGE_OUTPUT_MAX_SIZE;
}
return Math.ceil(size / 1000 / Transaction.FEE_PER_KB) * 1000;
return Math.ceil(size / 1000) * Transaction.FEE_PER_KB;
};

Transaction.MAXIMUM_EXTRA_SIZE = 4 + 9 + 9 + 4;
Expand Down
2 changes: 1 addition & 1 deletion test/data/tx_creation.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,6 @@
"change", ["3BazTqvkvEBcWk7J4sbgRnxUw6rjYrogf9"],
"sign", ["L2U9m5My3cdyN5qX1PH4B7XstGDZFWwyukdX8gj8vsJ3fkrqArQo"],
"sign", ["L4jFVcDaqZCkknP5KQWjCBgiLFxKxRxywNGTucm3jC3ozByZcbZv"],
"serialize", "010000000220c24f763536edb05ce8df2a4816d971be4f20b58451d71589db434aca98bfaf00000000fdfe0000483045022100f71c1c4d174c41c9ecd57306f46c9a04ab79f8ee69af54ab47ab17992622e675022047d850988556cf73ac7d0642be67a707ae3ba71018f9ebbf98cf8ca67da1c52f01483045022100b8360e5ad52099cb5a0d624fc73a905871a36ba43b77a93234ea64c47fc2d558022042991aecde368acefcfa8af60b49824a71d76e85331eecff1d82b3e3d7bcdbce014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffffa0644cd1606e081c59eb65fe69d4a83a3a822da423bc392c91712fb77a192edc00000000fdfd000047304402201065d3d9e7009f8c53ac802fe3757009e21a0849138acf96eab860912293b5ee022014552a7c8e2d84bf46468a7927a414eb0cf8cd42601cd7b6d07f44558afbfce901483045022100c7b3d9df174e6b97d3832a9d2cf75b064ae09b258e56448601c6f9e2ea91f0e3022005a723ed84e5b6c8c4ab6637176dd2adb8cb1cd618550cb9fd0e9a0d4db51184014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffff03f04902000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af387007102000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af38763b204000000000017a9146c8d8b04c6a1e664b1ec20ec932760760c97688e8700000000"
"serialize", "010000000220c24f763536edb05ce8df2a4816d971be4f20b58451d71589db434aca98bfaf00000000fdfd000047304402202c34fd898bbea3521c5f88fdeef4bb65f36ce01142633c1121e9b7bef307938902205e1aad62a66bd294898dc71678a4a5448281126baad90bfd9b4139663e852d26014830450221009d9f2b46595dc2578f4c4ac65c779e45e493bcc4649ef6786477e40df40d21fc02206da8a0f80fa2aa2d2b89d56951761c8f0506fdafd53ef6fe7d5c878251bb216b014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffffa0644cd1606e081c59eb65fe69d4a83a3a822da423bc392c91712fb77a192edc00000000fdfd000047304402202e64f052cd5be367f2f9efb31d3a34d0f8339700beff426d31b53c9a776a0368022057c714fda22a5ec301765dd187be3c522bcfe040127e857067163a5fb9ed46c001483045022100bf7a1e5a9e7204361e70312e15746efc95c7be8957a66c76574d2a07db359c550220318dc70b703c84a76fe3a460e96d466462f4c1c54b9fbfb75157b1afe2c51f8e014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffff03f04902000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af387007102000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af3873b8f04000000000017a9146c8d8b04c6a1e664b1ec20ec932760760c97688e8700000000"
]
]
Loading

0 comments on commit 85169a3

Please sign in to comment.