Browse Source

Modify Transaction to use Multisig

* Allow `Script#add` to add a Script causing concatenation of opcodes
* Add `Script#equals` to compare scripts
* Add `Script#fromAddress`
* Drop `_payTo` methods
* Add `Script.buildP2SHMultisigIn`

Greatly simplifying the internal transaction object
patch-2
Esteban Ordano 10 years ago
parent
commit
de4d2884c7
  1. 7
      lib/address.js
  2. 1
      lib/crypto/ecdsa.js
  3. 6
      lib/encoding/bufferreader.js
  4. 7
      lib/errors/spec.js
  5. 3
      lib/privatekey.js
  6. 84
      lib/script.js
  7. 10
      lib/transaction/input/index.js
  8. 21
      lib/transaction/input/input.js
  9. 108
      lib/transaction/input/multisigscripthash.js
  10. 3
      lib/transaction/input/publickeyhash.js
  11. 19
      lib/transaction/input/scriptHash.js
  12. 7
      lib/transaction/sighash.js
  13. 130
      lib/transaction/transaction.js
  14. 7
      lib/util/js.js
  15. 139
      test/transaction.js
  16. 79
      test/transaction/creation.js
  17. 70
      test/transaction/transaction.js

7
lib/address.js

@ -59,7 +59,7 @@ function Address(data, network, type) {
} else if (data.constructor && (data.constructor.name && data.constructor.name === 'PublicKey')) {
info = Address._transformPublicKey(data);
} else if (data.constructor && (data.constructor.name && data.constructor.name === 'Script')) {
info = Address._transformScript(data);
info = Address._transformScript(data, network);
} else if (data instanceof Address) {
return data;
} else if (typeof(data) === 'string') {
@ -204,11 +204,12 @@ Address._transformPublicKey = function(pubkey){
* @returns {Object} An object with keys: hashBuffer, type
* @private
*/
Address._transformScript = function(script){
Address._transformScript = function(script, network){
var info = {};
if (!script.constructor || (script.constructor.name && script.constructor.name !== 'Script')) {
throw new TypeError('Address must be an instance of Script.');
}
info.network = network || Networks.defaultNetwork;
info.hashBuffer = Hash.sha256ripemd160(script.toBuffer());
info.type = Address.PayToScriptHash;
return info;
@ -277,7 +278,7 @@ Address.fromScriptHash = function(hash, network) {
* @returns {Address} A new valid and frozen instance of an Address
*/
Address.fromScript = function(script, network) {
var info = Address._transformScript(script);
var info = Address._transformScript(script, network);
return new Address(info.hashBuffer, network, info.type);
};

1
lib/crypto/ecdsa.js

@ -1,6 +1,7 @@
'use strict';
var BN = require('./bn');
var BufferReader = require('../encoding/bufferreader');
var Point = require('./point');
var Signature = require('./signature');
var PublicKey = require('../publickey');

6
lib/encoding/bufferreader.js

@ -38,6 +38,12 @@ BufferReader.prototype.read = function(len) {
return buf;
};
BufferReader.prototype.readAll = function() {
var buf = this.buf.slice(this.pos, this.buf.length);
this.pos = this.buf.length;
return buf;
};
BufferReader.prototype.readUInt8 = function() {
var val = this.buf.readUInt8(this.pos);
this.pos = this.pos + 1;

7
lib/errors/spec.js

@ -34,6 +34,13 @@ module.exports = [{
}, {
name: 'InvalidArgumentType',
message: format('Invalid Argument for {2}, expected {1} but got ') + '+ typeof arguments[0]',
}, {
name: 'Script',
message: format('Internal Error on Script {0}'),
errors: [{
name: 'UnrecognizedAddress',
message: format('Expected argument {0} to be an address')
}]
}, {
name: 'HDPrivateKey',
message: format('Internal Error on HDPrivateKey {0}'),

3
lib/privatekey.js

@ -38,6 +38,9 @@ var PrivateKey = function PrivateKey(data, network, compressed) {
if (!(this instanceof PrivateKey)) {
return new PrivateKey(data, network, compressed);
}
if (data instanceof PrivateKey) {
return data;
}
var info = {
compressed: typeof(compressed) !== 'undefined' ? compressed : true,

84
lib/script.js

@ -11,8 +11,9 @@ var Signature = require('./crypto/signature');
var $ = require('./util/preconditions');
var _ = require('lodash');
var errors = require('./errors');
var buffer = require('buffer');
var bufferUtil = require('./util/buffer');
var BufferUtil = require('./util/buffer');
var jsUtil = require('./util/js');
/**
@ -31,8 +32,10 @@ var Script = function Script(from) {
this.chunks = [];
if (bufferUtil.isBuffer(from)) {
if (BufferUtil.isBuffer(from)) {
return Script.fromBuffer(from);
} else if (from instanceof Address) {
return Script.fromAddress(from);
} else if (from instanceof Script) {
return Script.fromBuffer(from.toBuffer());
} else if (typeof from === 'string') {
@ -236,7 +239,7 @@ Script.prototype.getPublicKeyHash = function() {
*/
Script.prototype.isPublicKeyOut = function() {
return this.chunks.length === 2 &&
bufferUtil.isBuffer(this.chunks[0].buf) &&
BufferUtil.isBuffer(this.chunks[0].buf) &&
PublicKey.isValid(this.chunks[0].buf) &&
this.chunks[1].opcodenum === Opcode.OP_CHECKSIG;
};
@ -246,7 +249,7 @@ Script.prototype.isPublicKeyOut = function() {
*/
Script.prototype.isPublicKeyIn = function() {
return this.chunks.length === 1 &&
bufferUtil.isBuffer(this.chunks[0].buf) &&
BufferUtil.isBuffer(this.chunks[0].buf) &&
this.chunks[0].buf.length === 0x47;
};
@ -290,7 +293,7 @@ Script.prototype.isMultisigOut = function() {
return (this.chunks.length > 3 &&
Opcode.isSmallIntOp(this.chunks[0].opcodenum) &&
this.chunks.slice(1, this.chunks.length - 2).every(function(obj) {
return obj.buf && bufferUtil.isBuffer(obj.buf);
return obj.buf && BufferUtil.isBuffer(obj.buf);
}) &&
Opcode.isSmallIntOp(this.chunks[this.chunks.length - 2].opcodenum) &&
this.chunks[this.chunks.length - 1].opcodenum === Opcode.OP_CHECKMULTISIG);
@ -305,7 +308,7 @@ Script.prototype.isMultisigIn = function() {
this.chunks[0].opcodenum === 0 &&
this.chunks.slice(1, this.chunks.length).every(function(obj) {
return obj.buf &&
bufferUtil.isBuffer(obj.buf) &&
BufferUtil.isBuffer(obj.buf) &&
obj.buf.length === 0x47;
});
};
@ -392,6 +395,30 @@ Script.prototype.prepend = function(obj) {
return this;
};
/**
* Compares a script with another script
*/
Script.prototype.equals = function(script) {
$.checkState(script instanceof Script, 'Must provide another script');
if (this.chunks.length !== script.chunks.length) {
return false;
}
var i;
for (i = 0; i < this.chunks.length; i++) {
if (BufferUtil.isBuffer(this.chunks[i]) && !BufferUtil.isBuffer(script.chunks[i])) {
return false;
} else if (this.chunks[i] instanceof Opcode && !(script.chunks[i] instanceof Opcode)) {
return false;
}
if (BufferUtil.isBuffer(this.chunks[i]) && !BufferUtil.equals(this.chunks[i], script.chunks[i])) {
return false;
} else if (this.chunks[i].num !== script.chunks[i].num) {
return false;
}
}
return true;
};
/**
* Adds a script element to the end of the script.
*
@ -411,10 +438,12 @@ Script.prototype._addByType = function(obj, prepend) {
this._addOpcode(obj, prepend);
} else if (obj.constructor && obj.constructor.name && obj.constructor.name === 'Opcode') {
this._addOpcode(obj, prepend);
} else if (bufferUtil.isBuffer(obj)) {
} else if (BufferUtil.isBuffer(obj)) {
this._addBuffer(obj, prepend);
} else if (typeof obj === 'object') {
this._insertAtPosition(obj, prepend);
} else if (obj instanceof Script) {
this.chunks = this.chunks.concat(obj.chunks);
} else {
throw new Error('Invalid script chunk');
}
@ -493,6 +522,7 @@ Script.buildMultisigOut = function(pubkeys, m, opts) {
opts = opts || {};
var s = new Script();
s.add(Opcode.smallInt(m));
pubkeys = _.map(pubkeys, function(pubkey) { return PublicKey(pubkey); });
var sorted = pubkeys;
if (!opts.noSorting) {
sorted = _.sortBy(pubkeys, function(pubkey) {
@ -508,6 +538,29 @@ Script.buildMultisigOut = function(pubkeys, m, opts) {
return s;
};
/**
* A new P2SH Multisig input script for the given public keys, requiring m of those public keys to spend
*
* @param {PublicKey[]} pubkeys list of all public keys controlling the output
* @param {number} threshold amount of required signatures to spend the output
* @param {Array} signatures signatures to append to the script
* @param {Object=} opts
* @param {boolean=false} opts.noSorting don't sort the given public keys before creating the script
* @param {Script=} opts.cachedMultisig don't recalculate the redeemScript
*
* @returns Script
*/
Script.buildP2SHMultisigIn = function(pubkeys, threshold, signatures, opts) {
opts = opts || {};
var s = new Script();
s.add(Opcode.OP_0);
_.each(signatures, function(signature) {
s.add(signature);
});
s.add((opts.cachedMultisig || Script.buildMultisigOut(pubkeys, threshold, opts)).toBuffer());
return s;
};
/**
* @returns a new pay to public key hash output for the given
* address or public key
@ -575,9 +628,9 @@ Script.buildScriptHashOut = function(script) {
*/
Script.buildPublicKeyHashIn = function(publicKey, signature, sigtype) {
var script = new Script()
.add(bufferUtil.concat([
.add(BufferUtil.concat([
signature,
bufferUtil.integerAsSingleByteBuffer(sigtype || Signature.SIGHASH_ALL)
BufferUtil.integerAsSingleByteBuffer(sigtype || Signature.SIGHASH_ALL)
]))
.add(new PublicKey(publicKey).toBuffer());
return script;
@ -597,4 +650,17 @@ Script.prototype.toScriptHashOut = function() {
return Script.buildScriptHashOut(this);
};
/**
* @return Script a script built from the address
*/
Script.fromAddress = function(address) {
address = Address(address);
if (address.isPayToScriptHash()) {
return Script.buildScriptHashOut(address);
} else if (address.isPayToPublicKeyHash()) {
return Script.buildPublicKeyHashOut(address);
}
throw new errors.Script.UnrecognizedAddress(address);
};
module.exports = Script;

10
lib/transaction/input/index.js

@ -1,8 +1,4 @@
'use strict';
module.exports = require('./input');
var Input = require('./input');
Input.PublicKeyHash = require('./publicKeyHash');
Input.ScriptHash = require('./scriptHash');
module.exports = Input;
module.exports.PublicKeyHash = require('./publickeyhash');
module.exports.MultiSigScriptHash = require('./multisigscripthash.js');

21
lib/transaction/input/input.js

@ -7,6 +7,7 @@ var buffer = require('buffer');
var bufferUtil = require('../../util/buffer');
var JSUtil = require('../../util/js');
var Script = require('../../script');
var Sighash = require('../sighash');
function Input(params) {
if (!(this instanceof Input)) {
@ -123,4 +124,24 @@ Input.prototype.getSignatures = function() {
throw new errors.AbstractMethodInvoked('Input#getSignatures');
};
Input.prototype.isFullySigned = function() {
throw new errors.AbstractMethodInvoked('Input#isFullySigned');
};
Input.prototype.addSignature = function() {
throw new errors.AbstractMethodInvoked('Input#addSignature');
};
Input.prototype.isValidSignature = function(transaction, signature) {
// FIXME: Refactor signature so this is not necessary
signature.signature.nhashtype = signature.sigtype;
return Sighash.verify(
transaction,
signature.signature,
signature.publicKey,
signature.inputIndex,
this.output.script
);
};
module.exports = Input;

108
lib/transaction/input/multisigscripthash.js

@ -0,0 +1,108 @@
'use strict';
var _ = require('lodash');
var JSUtil = require('../../util/js');
var inherits = require('inherits');
var Input = require('./input');
var Output = require('../output');
var $ = require('../../util/preconditions');
var Script = require('../../script');
var Signature = require('../../crypto/signature');
var Sighash = require('../sighash');
var BufferUtil = require('../../util/buffer');
function MultiSigScriptHashInput(input, pubkeys, threshold) {
Input.apply(this, arguments);
var self = this;
this.publicKeys = _.sortBy(pubkeys, function(publicKey) { return publicKey.toString('hex'); });
this.redeemScript = Script.buildMultisigOut(this.publicKeys, threshold);
$.checkState(Script.buildScriptHashOut(this.redeemScript).equals(this.output.script),
'Provided public keys don\'t hash to the provided output');
this.publicKeyIndex = {};
_.each(this.publicKeys, function(publicKey, index) {
self.publicKeyIndex[publicKey.toString()] = index;
});
this.threshold = threshold;
// Empty array of signatures
this.signatures = new Array(this.publicKeys.length);
}
inherits(MultiSigScriptHashInput, Input);
MultiSigScriptHashInput.prototype.getSignatures = function(transaction, privateKey, index, sigtype) {
$.checkState(this.output instanceof Output);
sigtype = sigtype || Signature.SIGHASH_ALL;
var self = this;
var results = [];
_.each(this.publicKeys, function(publicKey) {
if (publicKey.toString() === privateKey.publicKey.toString()) {
results.push({
publicKey: privateKey.publicKey,
prevTxId: self.txId,
outputIndex: self.outputIndex,
inputIndex: index,
signature: Sighash.sign(transaction, privateKey, sigtype, index, self.redeemScript),
sigtype: sigtype
});
}
});
return results;
};
MultiSigScriptHashInput.prototype.addSignature = function(transaction, signature) {
$.checkState(!this.isFullySigned(), 'All needed signatures have already been added');
$.checkArgument(!_.isUndefined(this.publicKeyIndex[signature.publicKey.toString()]),
'Signature has no matching public key');
$.checkState(this.isValidSignature(transaction, signature));
this.signatures[this.publicKeyIndex[signature.publicKey.toString()]] = signature;
this._updateScript();
return this;
};
MultiSigScriptHashInput.prototype._updateScript = function() {
this.setScript(Script.buildP2SHMultisigIn(
this.publicKeys,
this.threshold,
this._createSignatures(),
{ cachedMultisig: this.redeemScript }
));
return this;
};
MultiSigScriptHashInput.prototype._createSignatures = function() {
var reverseOrder = JSUtil.cloneArray(this.signatures).reverse();
return _.map(
_.filter(reverseOrder, function(signature) { return !_.isUndefined(signature); }),
function(signature) {
return BufferUtil.concat([
signature.signature.toDER(),
BufferUtil.integerAsSingleByteBuffer(signature.sigtype)
]);
}
);
};
MultiSigScriptHashInput.prototype.clearSignatures = function() {
this.signatures = new Array(this.publicKeys.length);
this._updateScript();
};
MultiSigScriptHashInput.prototype.isFullySigned = function() {
var count = _.reduce(this.signatures, function(sum, signature) { return sum + (!!signature); }, 0);
return count === this.threshold;
};
MultiSigScriptHashInput.prototype.isValidSignature = function(transaction, signature) {
// FIXME: Refactor signature so this is not necessary
signature.signature.nhashtype = signature.sigtype;
return Sighash.verify(
transaction,
signature.signature,
signature.publicKey,
signature.inputIndex,
this.redeemScript
);
};
module.exports = MultiSigScriptHashInput;

3
lib/transaction/input/publicKeyHash.js → lib/transaction/input/publickeyhash.js

@ -57,7 +57,8 @@ PublicKeyHashInput.prototype.getSignatures = function(transaction, privateKey, i
* @param {number=Signature.SIGHASH_ALL} signature.sigtype
* @return {PublicKeyHashInput} this, for chaining
*/
PublicKeyHashInput.prototype.addSignature = function(signature) {
PublicKeyHashInput.prototype.addSignature = function(transaction, signature) {
$.checkState(this.isValidSignature(transaction, signature), 'Signature is invalid');
this.setScript(Script.buildPublicKeyHashIn(
signature.publicKey,
signature.signature.toDER(),

19
lib/transaction/input/scriptHash.js

@ -1,19 +0,0 @@
'use strict';
var inherits = require('inherits');
var Input = require('./input');
var Hash = require('../../crypto/hash');
var Signature = require('../../crypto/signature');
var Sighash = require('../sighash');
var BufferUtil = require('../../util/buffer');
function ScriptHashInput() {
Input.apply(this, arguments);
}
inherits(ScriptHashInput, Input);
ScriptHashInput.prototype.getSignatures = function(transaction, privateKey, index, sigtype, hashData) {
return [];
};
module.exports = ScriptHashInput;

7
lib/transaction/sighash.js

@ -4,7 +4,6 @@ var buffer = require('buffer');
var Signature = require('../crypto/signature');
var Script = require('../script');
var Input = require('./input');
var Output = require('./output');
var BufferReader = require('../encoding/bufferreader');
var BufferWriter = require('../encoding/bufferwriter');
@ -25,9 +24,11 @@ var BITS_64_ON = 'ffffffffffffffff';
* @param {Script} subscript the script that will be signed
*/
function sighash(transaction, sighashType, inputNumber, subscript) {
var Transaction = require('./transaction');
var Input = require('./input');
var i;
// Copy transaction
var Transaction = require('./transaction');
var txcopy = Transaction.shallowCopy(transaction);
// Copy script
@ -88,12 +89,14 @@ function sighash(transaction, sighashType, inputNumber, subscript) {
function sign(transaction, keypair, nhashtype, nin, subscript) {
var hashbuf = sighash(transaction, nhashtype, nin, subscript);
hashbuf = new BufferReader(hashbuf).readReverse();
var sig = ECDSA.sign(hashbuf, keypair, 'little').set({nhashtype: nhashtype});
return sig;
}
function verify(transaction, sig, pubkey, nin, subscript) {
var hashbuf = sighash(transaction, sig.nhashtype, nin, subscript);
hashbuf = new BufferReader(hashbuf).readReverse();
return ECDSA.verify(hashbuf, sig, pubkey, 'little');
}

130
lib/transaction/transaction.js

@ -10,14 +10,13 @@ var JSUtil = require('../util/js');
var BufferReader = require('../encoding/bufferreader');
var BufferWriter = require('../encoding/bufferwriter');
var Hash = require('../crypto/hash');
var Sighash = require('./sighash');
var Signature = require('../crypto/signature');
var errors = require('../errors');
var Address = require('../address');
var Unit = require('../unit');
var Input = require('./input');
var PublicKeyHashInput = Input.PublicKeyHash;
var MultiSigScriptHashInput = Input.MultiSigScriptHash;
var Output = require('./output');
var Script = require('../script');
var PrivateKey = require('../privatekey');
@ -26,16 +25,20 @@ var CURRENT_VERSION = 1;
var DEFAULT_NLOCKTIME = 0;
var DEFAULT_SEQNUMBER = 0xFFFFFFFF;
/**
* Represents a transaction, a set of inputs and outputs to change
* ownership of tokens
*
* @param {*} serialized
*/
function Transaction(serialized) {
if (!(this instanceof Transaction)) {
return new Transaction(serialized);
}
this.inputs = [];
this.outputs = [];
this._outpoints = [];
this._inputAmount = 0;
this._outputAmount = 0;
this._signatures = {};
if (serialized) {
if (serialized instanceof Transaction) {
@ -54,6 +57,13 @@ function Transaction(serialized) {
/* Constructors and Serialization */
/**
* Create a "shallow" copy of the transaction, by serializing and deserializing
* it dropping any additional information that inputs and outputs may have hold
*
* @param {Transaction} transaction
* @return {Transaction}
*/
Transaction.shallowCopy = function(transaction) {
var copy = new Transaction(transaction.toBuffer());
return copy;
@ -69,10 +79,20 @@ var hashProperty = {
Object.defineProperty(Transaction.prototype, 'hash', hashProperty);
Object.defineProperty(Transaction.prototype, 'id', hashProperty);
/**
* Retrieve the little endian hash of the transaction (used for serialization)
* @return {Buffer}
*/
Transaction.prototype._getHash = function() {
return Hash.sha256sha256(this.toBuffer());
};
/**
* Retrieve a hexa string that can be used with bitcoind's CLI interface
* (decoderawtransaction, sendrawtransaction)
*
* @return {string}
*/
Transaction.prototype.serialize = Transaction.prototype.toString = function() {
return this.toBuffer().toString('hex');
};
@ -113,7 +133,6 @@ Transaction.prototype.fromBufferReader = function(reader) {
for (i = 0; i < sizeTxIns; i++) {
var input = Input.fromBufferReader(reader);
this.inputs.push(input);
this._outpoints.push(Transaction._makeOutpoint(input));
}
sizeTxOuts = reader.readVarintNum();
for (i = 0; i < sizeTxOuts; i++) {
@ -185,7 +204,13 @@ Transaction.prototype.from = function(utxo, pubkeys, threshold) {
};
Transaction.prototype._fromMultiSigP2SH = function(utxo, pubkeys, threshold) {
throw new errors.NotImplemented('Transaction#_fromMultiSigP2SH');
if (Transaction._isNewUtxo(utxo)) {
this._fromMultisigNewUtxo(utxo, pubkeys, threshold);
} else if (Transaction._isOldUtxo(utxo)) {
this._fromMultisigOldUtxo(utxo, pubkeys, threshold);
} else {
throw new Transaction.Errors.UnrecognizedUtxoFormat(utxo);
}
};
Transaction.prototype._fromNonP2SH = function(utxo) {
@ -205,6 +230,11 @@ Transaction.prototype._fromNonP2SH = function(utxo) {
}
};
Transaction._isNewUtxo = function(utxo) {
var isDefined = function(param) { return !_.isUndefined(param); };
return _.all(_.map([utxo.txId, utxo.outputIndex, utxo.satoshis, utxo.script], isDefined));
};
Transaction._isOldUtxo = function(utxo) {
var isDefined = function(param) { return !_.isUndefined(param); };
return _.all(_.map([utxo.txid, utxo.vout, utxo.scriptPubKey, utxo.amount], isDefined));
@ -215,20 +245,15 @@ Transaction.prototype._fromOldUtxo = function(utxo) {
address: utxo.address && new Address(utxo.address),
txId: utxo.txid,
outputIndex: utxo.vout,
script: new buffer.Buffer(utxo.scriptPubKey, 'hex'),
script: util.isHexa(utxo.script) ? new buffer.Buffer(utxo.scriptPubKey, 'hex') : utxo.scriptPubKey,
satoshis: Unit.fromBTC(utxo.amount).satoshis
});
};
Transaction._isNewUtxo = function(utxo) {
var isDefined = function(param) { return !_.isUndefined(param); };
return _.all(_.map([utxo.txId, utxo.outputIndex, utxo.satoshis, utxo.script], isDefined));
};
Transaction.prototype._fromNewUtxo = function(utxo) {
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 Input({
this.inputs.push(new PublicKeyHashInput({
output: new Output({
script: utxo.script,
satoshis: utxo.satoshis
@ -241,15 +266,30 @@ Transaction.prototype._fromNewUtxo = function(utxo) {
this._inputAmount += utxo.satoshis;
};
Transaction._makeOutpoint = function(data) {
if (!_.isUndefined(data.txId) && !_.isUndefined(data.outputIndex)) {
return data.txId + ':' + data.outputIndex;
}
if (!_.isUndefined(data.prevTxId) && !_.isUndefined(data.outputIndex)) {
var prevTxId = _.isString(data.prevTxId) ? data.prevTxId : data.prevTxId.toString('hex');
return prevTxId + ':' + data.outputIndex;
}
throw new Transaction.Errors.InvalidOutpointInfo(data);
Transaction.prototype._fromMultisigOldUtxo = function(utxo, pubkeys, threshold) {
return this._fromMultisigNewUtxo({
address: utxo.address && new Address(utxo.address),
txId: utxo.txid,
outputIndex: utxo.vout,
script: new buffer.Buffer(utxo.scriptPubKey, 'hex'),
satoshis: Unit.fromBTC(utxo.amount).satoshis
}, pubkeys, threshold);
};
Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) {
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({
output: new Output({
script: utxo.script,
satoshis: utxo.satoshis
}),
prevTxId: utxo.txId,
outputIndex: utxo.outputIndex,
sequenceNumber: DEFAULT_SEQNUMBER,
script: Script.empty()
}, pubkeys, threshold));
this._inputAmount += utxo.satoshis;
};
Transaction.prototype.hasAllUtxoInfo = function() {
@ -270,29 +310,12 @@ Transaction.prototype.change = function(address) {
return this;
};
Transaction.prototype.to = function() {
// TODO: Type validation
var argSize = _.size(arguments);
if (argSize === 3) {
Transaction.prototype._payToMultisig.apply(this, arguments);
} else if (argSize === 2) {
Transaction.prototype._payToAddress.apply(this, arguments);
} else {
// TODO: Error
throw new Error('');
}
return this;
};
Transaction.prototype._payToMultisig = function(pubkeys, threshold, amount) {
throw new errors.NotImplemented('Transaction#_payToMultisig');
};
Transaction.prototype._payToAddress = function(address, amount) {
Transaction.prototype.to = function(address, amount) {
this._addOutput(new Output({
script: Script.buildPublicKeyHashOut(address),
script: Script(new Address(address)),
satoshis: amount
}));
return this;
};
Transaction.prototype._addOutput = function(output) {
@ -310,7 +333,7 @@ Transaction.prototype.addData = function(value) {
/* Signature handling */
Transaction.prototype.sign = function(privKey) {
Transaction.prototype.sign = function(privKey, sigtype) {
// TODO: Change for preconditions
assert(this.hasAllUtxoInfo());
var self = this;
@ -320,7 +343,7 @@ Transaction.prototype.sign = function(privKey) {
});
return this;
}
_.each(this.getSignatures(privKey), function(signature) {
_.each(this.getSignatures(privKey, sigtype), function(signature) {
self.applySignature(signature);
});
return this;
@ -341,12 +364,23 @@ Transaction.prototype._getPrivateKeySignatures = function(privKey, sigtype) {
};
Transaction.prototype.applySignature = function(signature) {
this.inputs[signature.inputIndex].addSignature(signature);
this.inputs[signature.inputIndex].addSignature(this, signature);
return this;
};
Transaction.prototype.getSignatures = function(privKey) {
return this._getPrivateKeySignatures(privKey);
Transaction.prototype.getSignatures = function(privKey, sigtype) {
return this._getPrivateKeySignatures(privKey, sigtype);
};
Transaction.prototype.isFullySigned = function() {
return _.all(_.map(this.inputs, function(input) {
return input.isFullySigned();
}));
};
Transaction.prototype.isValidSignature = function(signature) {
var self = this;
return this.inputs[signature.inputIndex].isValidSignature(self, signature);
};
module.exports = Transaction;

7
lib/util/js.js

@ -33,6 +33,13 @@ module.exports = {
isHexa: isHexa,
isHexaString: isHexa,
/**
* Clone an array
*/
cloneArray: function(array) {
return [].concat(array);
},
/**
* Define immutable properties on a target object
*

139
test/transaction.js

@ -1,139 +0,0 @@
'use strict';
/* jshint unused: false */
/* jshint latedef: false */
var should = require('chai').should();
var _ = require('lodash');
var bitcore = require('..');
var Transaction = bitcore.Transaction;
var Script = bitcore.Script;
var valid = require('./data/bitcoind/tx_valid.json');
var invalid = require('./data/bitcoind/tx_invalid.json');
describe('Transaction', function() {
describe('bitcoind compliance', function() {
valid.map(function(datum){
if ( typeof(datum[0]) === 'string' ) {
return;
}
it('should deserialize/serialize '+datum[1].slice(0, 15)+'... transaction', function() {
var serialized = datum[1];
var t = new Transaction(serialized);
t.serialize().should.equal(serialized);
});
});
});
it('should serialize and deserialize correctly a given transaction', function() {
var transaction = new Transaction(tx_1_hex);
transaction.serialize().should.equal(tx_1_hex);
});
it('should display correctly in console', function() {
var transaction = new Transaction(tx_1_hex);
transaction.inspect().should.equal('<Transaction: ' + tx_1_hex + '>');
});
it('standard hash of transaction should be decoded correctly', function() {
var transaction = new Transaction(tx_1_hex);
transaction.id.should.equal(tx_1_id);
});
it('serializes an empty transaction', function() {
var transaction = new Transaction();
transaction.serialize().should.equal(tx_empty_hex);
});
it('serializes and deserializes correctly', function() {
var transaction = new Transaction(tx_1_hex);
transaction.serialize().should.equal(tx_1_hex);
});
it('should input/output json', function() {
var transaction = JSON.parse(Transaction().fromJSON(tx_1_json).toJSON());
transaction.should.deep.equal(JSON.parse(tx_1_json));
});
it('should create a sample transaction from an utxo', function() {
var transaction = new Transaction()
.from(utxo_1a)
.to(address_1, amount_1)
.sign(privkey_1a)
.serialize()
.should.equal(tx_1_hex);
});
it.skip('should create a transaction with two utxos', function() {
var transaction = new Transaction()
.from([utxo_2a, utxo_2b])
.to(address_2, amount_2)
.sign([privkey_2a, privkey_2b])
.serialize()
.should.equal(tx_2_hex);
});
});
var tx_empty_hex = '01000000000000000000';
/* jshint maxlen: 1000 */
var tx_1_hex = '01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4000000006a473044022013fa3089327b50263029265572ae1b022a91d10ac80eb4f32f291c914533670b02200d8a5ed5f62634a7e1a0dc9188a3cc460a986267ae4d58faf50c79105431327501210223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5effffffff0150690f00000000001976a9147821c0a3768aa9d1a37e16cf76002aef5373f1a888ac00000000';
var tx_1_id = '779a3e5b3c2c452c85333d8521f804c1a52800e60f4b7c3bbe36f4bab350b72c';
var tx_2_hex = '';
var tx_1_json = JSON.stringify({
version:1,
inputs:[{
prevTxId:"a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458",
outputIndex:0,
sequenceNumber:4294967295,
script:'71 0x3044022013fa3089327b50263029265572ae1b022a91d10ac80eb4f32f291c914533670b02200d8a5ed5f62634a7e1a0dc9188a3cc460a986267ae4d58faf50c79105431327501 33 0x0223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5e'}],
outputs:[{
satoshis:1010000,
script:'OP_DUP OP_HASH160 20 0x7821c0a3768aa9d1a37e16cf76002aef5373f1a8 OP_EQUALVERIFY OP_CHECKSIG'
}],
nLockTime:0
});
var utxo_1a_address = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1';
var utxo_2a_address = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc';
var utxo_2b_address = 'mrCHmWgn54hJNty2srFF4XLmkey5GnCv5m';
/* A new-format utxo */
var utxo_1a = {
address: utxo_1a_address,
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
outputIndex: 0,
script: Script.buildPublicKeyHashOut(utxo_1a_address).toString(),
satoshis: 1020000
};
/* An old-format utxo */
var utxo_2a = {
address: utxo_2a_address,
txid: '779a3e5b3c2c452c85333d8521f804c1a52800e60f4b7c3bbe36f4bab350b72c',
vout: 0,
scriptPubKey: Script.buildPublicKeyHashOut(utxo_2a_address).toString(),
amount: 0.01010000
};
var utxo_2b = {
address: utxo_2b_address,
txid: 'e0f44096fcac31c1baede0714997c831123ecb5e258b52617fb093ba487c1d04',
vout: 0,
scriptPubKey: Script.buildPublicKeyHashOut(utxo_2b_address).toString(),
amount: 0.00090000
};
var address_1 = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc';
var address_2 = 'mrCHmWgn54hJNty2srFF4XLmkey5GnCv5m';
var amount_1 = 1010000;
var amount_2 = 1090000;
var privkey_1a = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY';
var privkey_2a = 'cVLKm6LT1VTpZJVaSYtkYPLP1UP2Ph6NFxGVNLPAKKuSfv8hHreU';
var privkey_2b = 'cVWHj19aJXVAxcKC5xAWQmiyhWyarmcPcuv4dT7nZy1JR37dbWgT';

79
test/transaction/creation.js

@ -0,0 +1,79 @@
'use strict';
var bitcore = require('../..');
var Script = bitcore.Script;
module.exports = [
[
'from', [{
address: 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1',
txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458',
outputIndex: 0,
script: Script.buildPublicKeyHashOut('mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1').toString(),
satoshis: 1020000
}],
'to', ['mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', 1010000],
'sign', ['cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY'],
'serialize', '01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4000000006b4830450221009972100061da4a17a471ac1906c18bb5445c03da2a0be52c59aca6c58f1e342302205eac5ba43830a397f613f40addea4a2eeaa485a1f9a6efa61344c3560762fe3d01210223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5effffffff0150690f00000000001976a9147821c0a3768aa9d1a37e16cf76002aef5373f1a888ac00000000'
],
[
'from', [{
"txid" : "e42447187db5a29d6db161661e4bc66d61c3e499690fe5ea47f87b79ca573986",
"vout" : 1,
"address" : "mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up",
"scriptPubKey" : "76a914073b7eae2823efa349e3b9155b8a735526463a0f88ac",
"amount" : 0.01080000,
}],
'to', ['mn9new5vPYWuVN5m3gUBujfKh1uPQvR9mf', 500000],
'to', ['mw5ctwgEaNRbxkM4JhXH3rp5AyGvTWDZCD', 570000],
'sign', ['cSQUuwwJBAg6tYQhzqqLWW115D1s5KFZDyhCF2ffrnukZxMK6rNZ'],
'serialize', '0100000001863957ca797bf847eae50f6999e4c3616dc64b1e6661b16d9da2b57d184724e4010000006b483045022100855691c90510edf83ab632f0a0b17f5202d2cf7071050dcf0c2778325ed403cd02207270a2f0b30c13dc3c1dee74b5ccabcc2632b402c4f38adabcd07357df1442270121039dd446bbc85db6917f39c0b4c295b0f8cce76d1926fa76d7b84e3f7ff1c5eec5ffffffff0220a10700000000001976a91448c819246ae5645ceecd41fbe1aa6202a0a9b5ca88ac90b20800000000001976a914aab76ba4877d696590d94ea3e02948b55294815188ac00000000'
],
[
'from', [[{
"txid" : "a9db84566e0fc9351e86337d2828ab281b25ddc06fab798f6d4b5baef48c02b3",
"vout" : 0,
"address" : "mn9new5vPYWuVN5m3gUBujfKh1uPQvR9mf",
"account" : "",
"scriptPubKey" : "76a91448c819246ae5645ceecd41fbe1aa6202a0a9b5ca88ac",
"amount" : 0.00500000,
"confirmations" : 0
}, {
"txid" : "a9db84566e0fc9351e86337d2828ab281b25ddc06fab798f6d4b5baef48c02b3",
"vout" : 1,
"address" : "mw5ctwgEaNRbxkM4JhXH3rp5AyGvTWDZCD",
"account" : "",
"scriptPubKey" : "76a914aab76ba4877d696590d94ea3e02948b55294815188ac",
"amount" : 0.00570000,
"confirmations" : 0
}]],
'to', ['mtymcCX5KixPjT1zxtg59qewBGWptj9etH', 1060000],
'sign', [['cPGbA2C54ZZ1sw4dc2ckBE1WqkdrNSbEV8Tkjhi2p1J15oErdgP2', 'cSpyve5bXAuyHrNeV9MjTdFz3HLw739yUjjUAUSMe3ppf2qzj2hw']],
'serialize', '0100000002b3028cf4ae5b4b6d8f79ab6fc0dd251b28ab28287d33861e35c90f6e5684dba9000000006a4730440220635e95e1981bbb360feaf4c232f626a0af8eb5c043a99749a21b0e37fd0048fd02207889f6974f0cad39ce8c2a6dff05c8ca402da9ff6fc41e06c12d86853c91a9d80121030253c73236acf5ea9085d408220141197f6094de07426bd0d32c7a543614fdd7ffffffffb3028cf4ae5b4b6d8f79ab6fc0dd251b28ab28287d33861e35c90f6e5684dba9010000006a4730440220319a0b5ee9c67ccb7de4222234f31059354be4f239c99ca24bff30adfec8e8ec022056e6e99e50f7ceaa062958b8424cde1d504019f95c1dc0a0f0778848d0fb9f4b012102977a001a0a7bbfd1f8a647c7d46e13e8f6920635b328390b43b3303977101149ffffffff01a02c1000000000001976a91493abf1e9e4a20c125b93f93ee39efc16b6e4bc4688ac00000000'
],
[
'from', [{
"txid": "c8beceb964dec7ae5ec6ef5d019429b50c2e5fd07bd369e9a282d5153f23589c",
"vout": 0,
"address": "mtymcCX5KixPjT1zxtg59qewBGWptj9etH",
"account": "",
"scriptPubKey": "76a91493abf1e9e4a20c125b93f93ee39efc16b6e4bc4688ac",
"amount": 0.01060000,
}],
'to', ['2NEQb8rtiUgxqQ9eif4XVeMUEW2LSZ64s58', 1050000],
'sign', ['cMh7xdJ5EZVg6kvFsBybwK1EYGJw3G1DHhe5sNPAwbDts94ohKyK'],
'serialize', '01000000019c58233f15d582a2e969d37bd05f2e0cb52994015defc65eaec7de64b9cebec8000000006a473044022050442862e892b1d12bcaa03857746f0ed168122e093d799861f4e081756bb8aa0220081d4eaf9281ae8f954efaeb47500d9a02e5a74b3ada51b6a258ac83c1f4f6420121039dbeac2610d53eb7107b14c0fa9be4006a731fa5bcef392d4e1a25ec0e58f0d3ffffffff01900510000000000017a91490edc43da6b052c4a23fc178979ce358a8caad5e8700000000'
],
[
'from', [{
"address": "2N6TY8Dc5JmJ87Fg9DhmN66fvFSwnTrjgip",
"txid": "66e64ef8a3b384164b78453fa8c8194de9a473ba14f89485a0e433699daec140",
"vout": 0,
"scriptPubKey": "a91490edc43da6b052c4a23fc178979ce358a8caad5e87",
"amount": 0.01050000
}, ['03fd45c8cd28c4c6a9a89b4515173edebc66a2418353976eccf01c73a7da9bbb12', '0349e0138b2c2f496121258e0426e1dbd698b2c6038e70fd17e3563aa87b4384f9'], 2],
'to', ['mssMdcEm6PiJEr4XZtjk6kkai84EjBbi91', 1040000],
'sign', [['L3wRFe9XHLnkLquf41F56ac77uRXwJ97HZPQ9tppqyMANBKXpoc5', 'KzkfNSL1gvdyU3CGLaP1Cs3pW167M8r9uE8yMtWQrAzz5vCv59CM']],
'serialize', '010000000140c1ae9d6933e4a08594f814ba73a4e94d19c8a83f45784b1684b3a3f84ee66600000000da004730440220366678972728684a94f35635b855583603b28065d430949c08be89412a4ee45d02201aa62e3129c8819ecf2048230e8c77e244d6a496f296954a5bb4a0d0185f8c0201483045022100d06f348b4ef793f2bf749b288f1df165c0946779391c50ddc050e5b1608b2dda02200fcc8c6874b9a313374020253c5de346fe3517c97b18bfa769cea1089ad97144014752210349e0138b2c2f496121258e0426e1dbd698b2c6038e70fd17e3563aa87b4384f92103fd45c8cd28c4c6a9a89b4515173edebc66a2418353976eccf01c73a7da9bbb1252aeffffffff0180de0f00000000001976a914877d4f3be444448f868b345153bc4fc7a11a7c6388ac00000000'
]
];

70
test/transaction/transaction.js

@ -0,0 +1,70 @@
'use strict';
/* jshint unused: false */
/* jshint latedef: false */
var should = require('chai').should();
var _ = require('lodash');
var bitcore = require('../..');
var Transaction = bitcore.Transaction;
var PrivateKey = bitcore.PrivateKey;
var Script = bitcore.Script;
var Address = bitcore.Address;
var Networks = bitcore.Networks;
var transactionVector = require('./creation');
describe('Transaction', function() {
it('should serialize and deserialize correctly a given transaction', function() {
var transaction = new Transaction(tx_1_hex);
transaction.serialize().should.equal(tx_1_hex);
});
it('should display correctly in console', function() {
var transaction = new Transaction(tx_1_hex);
transaction.inspect().should.equal('<Transaction: ' + tx_1_hex + '>');
});
it('standard hash of transaction should be decoded correctly', function() {
var transaction = new Transaction(tx_1_hex);
transaction.id.should.equal(tx_1_id);
});
it('serializes an empty transaction', function() {
var transaction = new Transaction();
transaction.serialize().should.equal(tx_empty_hex);
});
it('serializes and deserializes correctly', function() {
var transaction = new Transaction(tx_1_hex);
transaction.serialize().should.equal(tx_1_hex);
});
describe('transaction creation test vector', function() {
var index = 0;
transactionVector.forEach(function(vector) {
index++;
it('case ' + index, function() {
var i = 0;
var transaction = new Transaction();
while (i < vector.length) {
var command = vector[i];
var args = vector[i+1];
if (command === 'serialize') {
transaction.serialize().should.equal(args);
} else {
transaction[command].apply(transaction, args);
}
i += 2;
}
});
});
});
});
var tx_empty_hex = '01000000000000000000';
/* jshint maxlen: 1000 */
var tx_1_hex = '01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4000000006a473044022013fa3089327b50263029265572ae1b022a91d10ac80eb4f32f291c914533670b02200d8a5ed5f62634a7e1a0dc9188a3cc460a986267ae4d58faf50c79105431327501210223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5effffffff0150690f00000000001976a9147821c0a3768aa9d1a37e16cf76002aef5373f1a888ac00000000';
var tx_1_id = '779a3e5b3c2c452c85333d8521f804c1a52800e60f4b7c3bbe36f4bab350b72c';
Loading…
Cancel
Save