From 605be801af4fe9ee94f2e860341aae900b160a08 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 11 Feb 2015 11:04:04 -0300 Subject: [PATCH] Add Signature subclass for Transaction Signatures --- lib/crypto/signature.js | 2 +- lib/transaction/index.js | 1 + lib/transaction/input/multisigscripthash.js | 15 +-- lib/transaction/input/publickeyhash.js | 5 +- lib/transaction/signature.js | 111 ++++++++++++++++ test/transaction/signature.js | 132 ++++++++++++++++++++ 6 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 lib/transaction/signature.js create mode 100644 test/transaction/signature.js diff --git a/lib/crypto/signature.js b/lib/crypto/signature.js index 463b48ca1..deaa66012 100644 --- a/lib/crypto/signature.js +++ b/lib/crypto/signature.js @@ -50,7 +50,7 @@ Signature.fromCompact = function(buf) { return sig; }; -Signature.fromDER = function(buf, strict) { +Signature.fromDER = Signature.fromBuffer = function(buf, strict) { var obj = Signature.parseDER(buf, strict); var sig = new Signature(); diff --git a/lib/transaction/index.js b/lib/transaction/index.js index e82663587..c03b0c823 100644 --- a/lib/transaction/index.js +++ b/lib/transaction/index.js @@ -3,3 +3,4 @@ module.exports = require('./transaction'); module.exports.Input = require('./input'); module.exports.Output = require('./output'); module.exports.UnspentOutput = require('./unspentoutput'); +module.exports.Signature = require('./signature'); diff --git a/lib/transaction/input/multisigscripthash.js b/lib/transaction/input/multisigscripthash.js index e8a628657..b6fb67149 100644 --- a/lib/transaction/input/multisigscripthash.js +++ b/lib/transaction/input/multisigscripthash.js @@ -11,6 +11,7 @@ var Signature = require('../../crypto/signature'); var Sighash = require('../sighash'); var PublicKey = require('../../publickey'); var BufferUtil = require('../../util/buffer'); +var TransactionSignature = require('../signature'); /** * @constructor @@ -45,14 +46,14 @@ MultiSigScriptHashInput.prototype._deserializeSignatures = function(signatures) if (!signature) { return undefined; } - return { + return new TransactionSignature({ publicKey: new PublicKey(signature.publicKey), prevTxId: signature.txId, outputIndex: signature.outputIndex, inputIndex: signature.inputIndex, signature: Signature.fromString(signature.signature), sigtype: signature.sigtype - }; + }); }); }; @@ -61,14 +62,14 @@ MultiSigScriptHashInput.prototype._serializeSignatures = function() { if (!signature) { return undefined; } - return { + return new TransactionSignature({ publicKey: signature.publicKey.toString(), prevTxId: signature.txId, outputIndex: signature.outputIndex, inputIndex: signature.inputIndex, signature: signature.signature.toString(), sigtype: signature.sigtype - }; + }); }); }; @@ -80,14 +81,14 @@ MultiSigScriptHashInput.prototype.getSignatures = function(transaction, privateK var results = []; _.each(this.publicKeys, function(publicKey) { if (publicKey.toString() === privateKey.publicKey.toString()) { - results.push({ + results.push(new TransactionSignature({ publicKey: privateKey.publicKey, - prevTxId: self.txId, + prevTxId: self.prevTxId, outputIndex: self.outputIndex, inputIndex: index, signature: Sighash.sign(transaction, privateKey, sigtype, index, self.redeemScript), sigtype: sigtype - }); + })); } }); return results; diff --git a/lib/transaction/input/publickeyhash.js b/lib/transaction/input/publickeyhash.js index c8c2dbe5a..b5338c662 100644 --- a/lib/transaction/input/publickeyhash.js +++ b/lib/transaction/input/publickeyhash.js @@ -11,6 +11,7 @@ var Output = require('../output'); var Sighash = require('../sighash'); var Script = require('../../script'); var Signature = require('../../crypto/signature'); +var TransactionSignature = require('../signature'); /** * Represents a special kind of input of PayToPublicKeyHash kind. @@ -36,14 +37,14 @@ PublicKeyHashInput.prototype.getSignatures = function(transaction, privateKey, i sigtype = sigtype || Signature.SIGHASH_ALL; if (BufferUtil.equals(hashData, this.output.script.getPublicKeyHash())) { - return [{ + return [new TransactionSignature({ publicKey: privateKey.publicKey, prevTxId: this.prevTxId, outputIndex: this.outputIndex, inputIndex: index, signature: Sighash.sign(transaction, privateKey, sigtype, index, this.output.script), sigtype: sigtype - }]; + })]; } return []; }; diff --git a/lib/transaction/signature.js b/lib/transaction/signature.js new file mode 100644 index 000000000..a734268f0 --- /dev/null +++ b/lib/transaction/signature.js @@ -0,0 +1,111 @@ +'use strict'; + +var _ = require('lodash'); +var $ = require('../util/preconditions'); +var inherits = require('inherits'); +var BufferUtil = require('../util/buffer'); +var JSUtil = require('../util/js'); + +var PublicKey = require('../publickey'); +var errors = require('../errors'); +var Signature = require('../crypto/signature'); + +/** + * @desc + * Wrapper around Signature with fields related to signing a transaction specifically + * + * @param {Object|string|TransactionSignature} arg + * @constructor + */ +function TransactionSignature(arg) { + if (!(this instanceof TransactionSignature)) { + return new TransactionSignature(arg); + } + if (arg instanceof TransactionSignature) { + return arg; + } + if (_.isString(arg)) { + if (JSUtil.isValidJSON(arg)) { + return TransactionSignature.fromJSON(arg); + } + } + if (_.isObject(arg)) { + return this._fromObject(arg); + } + throw new errors.InvalidArgument('TransactionSignatures must be instantiated from an object'); +} +inherits(TransactionSignature, Signature); + +TransactionSignature.prototype._fromObject = function(arg) { + this._checkObjectArgs(arg); + this.publicKey = new PublicKey(arg.publicKey); + this.prevTxId = BufferUtil.isBuffer(arg.prevTxId) ? arg.prevTxId : new Buffer(arg.prevTxId, 'hex'); + this.outputIndex = arg.outputIndex; + this.inputIndex = arg.inputIndex; + this.signature = (arg.signature instanceof Signature) ? arg.signature : + BufferUtil.isBuffer(arg.signature) ? Signature.fromBuffer(arg.signature) : + Signature.fromString(arg.signature); + this.sigtype = arg.sigtype; + return this; +}; + +TransactionSignature.prototype._checkObjectArgs = function(arg) { + $.checkArgument(PublicKey(arg.publicKey), 'publicKey'); + $.checkArgument(!_.isUndefined(arg.inputIndex), 'inputIndex'); + $.checkArgument(!_.isUndefined(arg.outputIndex), 'outputIndex'); + $.checkState(_.isNumber(arg.inputIndex), 'inputIndex must be a number'); + $.checkState(_.isNumber(arg.outputIndex), 'outputIndex must be a number'); + $.checkArgument(arg.signature, 'signature'); + $.checkArgument(arg.prevTxId, 'prevTxId'); + $.checkState(arg.signature instanceof Signature || + BufferUtil.isBuffer(arg.signature) || + JSUtil.isHexa(arg.signature), 'signature must be a buffer or hexa value'); + $.checkState(BufferUtil.isBuffer(arg.prevTxId) || + JSUtil.isHexa(arg.prevTxId), 'prevTxId must be a buffer or hexa value'); + $.checkArgument(arg.sigtype, 'sigtype'); + $.checkState(_.isNumber(arg.sigtype), 'sigtype must be a number'); +}; + +/** + * Serializes a transaction to a plain JS object + * @return {Object} + */ +TransactionSignature.prototype.toObject = function() { + return { + publicKey: this.publicKey.toString(), + prevTxId: this.prevTxId.toString('hex'), + outputIndex: this.outputIndex, + inputIndex: this.inputIndex, + signature: this.signature.toString(), + sigtype: this.sigtype + }; +}; + +/** + * Serializes a transaction to a JSON string + * @return {string} + */ +TransactionSignature.prototype.toJSON = function() { + return JSON.stringify(this.toObject()); +}; + +/** + * Builds a TransactionSignature from a JSON string + * @param {string} json + * @return {TransactionSignature} + */ +TransactionSignature.fromJSON = function(json) { + return new TransactionSignature(JSON.parse(json)); +}; + +/** + * Builds a TransactionSignature from an object + * @param {Object} object + * @return {TransactionSignature} + */ +TransactionSignature.fromObject = function(object) { + $.checkArgument(object); + return new TransactionSignature(object); +}; + +module.exports = TransactionSignature; diff --git a/test/transaction/signature.js b/test/transaction/signature.js new file mode 100644 index 000000000..7e6b581c7 --- /dev/null +++ b/test/transaction/signature.js @@ -0,0 +1,132 @@ +'use strict'; + +/* jshint unused: false */ +/* jshint latedef: false */ +var should = require('chai').should(); +var expect = require('chai').expect; +var _ = require('lodash'); + +var bitcore = require('../..'); +var Transaction = bitcore.Transaction; +var TransactionSignature = bitcore.Transaction.Signature; +var Script = bitcore.Script; +var PrivateKey = bitcore.PrivateKey; +var errors = bitcore.errors; + +describe('TransactionSignature', function() { + + var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1'; + var privateKey = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY'; + var simpleUtxoWith100000Satoshis = { + address: fromAddress, + txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', + outputIndex: 0, + script: Script.buildPublicKeyHashOut(fromAddress).toString(), + satoshis: 100000 + }; + + var getSignatureFromTransaction = function() { + var transaction = new Transaction(); + transaction.from(simpleUtxoWith100000Satoshis); + return transaction.getSignatures(privateKey)[0]; + }; + + it('can be created without the `new` keyword', function() { + var signature = getSignatureFromTransaction(); + var serialized = signature.toObject(); + var nonew = TransactionSignature(serialized); + expect(nonew.toObject()).to.deep.equal(serialized); + }); + + it('can be retrieved from Transaction#getSignatures', function() { + var signature = getSignatureFromTransaction(); + expect(signature instanceof TransactionSignature).to.equal(true); + }); + + it('fails when trying to create from invalid arguments', function() { + expect(function() { + return new TransactionSignature(); + }).to.throw(errors.InvalidArgument); + expect(function() { + return new TransactionSignature(1); + }).to.throw(errors.InvalidArgument); + expect(function() { + return new TransactionSignature('hello world'); + }).to.throw(errors.InvalidArgument); + }); + it('returns the same object if called with a TransactionSignature', function() { + var signature = getSignatureFromTransaction(); + expect(new TransactionSignature(signature)).to.equal(signature); + }); + + it('gets returned by a P2SH multisig output', function() { + var private1 = new PrivateKey('6ce7e97e317d2af16c33db0b9270ec047a91bff3eff8558afb5014afb2bb5976'); + var private2 = new PrivateKey('c9b26b0f771a0d2dad88a44de90f05f416b3b385ff1d989343005546a0032890'); + var public1 = private1.publicKey; + var public2 = private2.publicKey; + var utxo = { + txId: '0000000000000000000000000000000000000000000000000000000000000000', // Not relevant + outputIndex: 0, + script: Script.buildMultisigOut([public1, public2], 2).toScriptHashOut(), + satoshis: 100000 + }; + var transaction = new Transaction().from(utxo, [public1, public2], 2); + var signatures = transaction.getSignatures(private1); + expect(signatures[0] instanceof TransactionSignature).to.equal(true); + signatures = transaction.getSignatures(private2); + expect(signatures[0] instanceof TransactionSignature).to.equal(true); + }); + + it('can be aplied to a Transaction with Transaction#addSignature', function() { + var transaction = new Transaction(); + transaction.from(simpleUtxoWith100000Satoshis); + var signature = transaction.getSignatures(privateKey)[0]; + var addSignature = function() { + return transaction.applySignature(signature); + }; + expect(signature instanceof TransactionSignature).to.equal(true); + expect(addSignature).to.not.throw(); + }); + + describe('serialization', function() { + it('serializes to an object and roundtrips correctly', function() { + var signature = getSignatureFromTransaction(); + var serialized = signature.toObject(); + expect(new TransactionSignature(serialized).toObject()).to.deep.equal(serialized); + }); + + it('can be deserialized with fromObject', function() { + var signature = getSignatureFromTransaction(); + var serialized = signature.toObject(); + expect(TransactionSignature.fromObject(serialized).toObject()).to.deep.equal(serialized); + }); + + it('can deserialize when signature is a buffer', function() { + var signature = getSignatureFromTransaction(); + var serialized = signature.toObject(); + serialized.signature = new Buffer(serialized.signature, 'hex'); + expect(TransactionSignature.fromObject(serialized).toObject()).to.deep.equal(signature.toObject()); + }); + + it('can roundtrip to/from json', function() { + var signature = getSignatureFromTransaction(); + var serialized = signature.toObject(); + var json = signature.toJSON(); + expect(TransactionSignature(json).toObject()).to.deep.equal(serialized); + expect(TransactionSignature.fromJSON(json).toObject()).to.deep.equal(serialized); + }); + + it('can parse a previously known json string', function() { + expect(JSON.parse(TransactionSignature(testJSON).toJSON())).to.deep.equal(JSON.parse(testJSON)); + }); + + it('can deserialize a previously known object', function() { + expect(TransactionSignature(testObject).toObject()).to.deep.equal(testObject); + }); + }); + + /* jshint maxlen: 500 */ + var testJSON = '{"publicKey":"0223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5e","prevTxId":"a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458","outputIndex":0,"inputIndex":0,"signature":"3045022100c728eac064154edba15d4f3e6cbd9be6da3498f80a783ab3391f992b4d9d71ca0220729eff4564dc06aa1d80ab73100540fe5ebb6f280b4a87bc32399f861a7b2563","sigtype":1}'; + var testObject = JSON.parse(testJSON); + +});