Merge pull request #12 from rubensayshi/bare-multisig
Add support for bare multisig
This commit is contained in:
commit
c6c9021201
|
@ -687,6 +687,33 @@ Script.buildMultisigOut = function(publicKeys, threshold, opts) {
|
|||
return script;
|
||||
};
|
||||
|
||||
/**
|
||||
* A new 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 and array of signature buffers to append to the script
|
||||
* @param {Object=} opts
|
||||
* @param {boolean=} opts.noSorting don't sort the given public keys before creating the script (false by default)
|
||||
* @param {Script=} opts.cachedMultisig don't recalculate the redeemScript
|
||||
*
|
||||
* @returns {Script}
|
||||
*/
|
||||
Script.buildMultisigIn = function(pubkeys, threshold, signatures, opts) {
|
||||
$.checkArgument(_.isArray(pubkeys));
|
||||
$.checkArgument(_.isNumber(threshold));
|
||||
$.checkArgument(_.isArray(signatures));
|
||||
opts = opts || {};
|
||||
var s = new Script();
|
||||
s.add(Opcode.OP_0);
|
||||
_.each(signatures, function(signature) {
|
||||
$.checkArgument(BufferUtil.isBuffer(signature), 'Signatures must be an array of Buffers');
|
||||
// TODO: allow signatures to be an array of Signature objects
|
||||
s.add(signature);
|
||||
});
|
||||
return s;
|
||||
};
|
||||
|
||||
/**
|
||||
* A new P2SH Multisig input script for the given public keys, requiring m of those public keys to spend
|
||||
*
|
||||
|
|
|
@ -4,3 +4,4 @@ module.exports.Input = require('./input');
|
|||
module.exports.Output = require('./output');
|
||||
module.exports.UnspentOutput = require('./unspentoutput');
|
||||
module.exports.Signature = require('./signature');
|
||||
module.exports.Sighash = require('./sighash');
|
||||
|
|
|
@ -2,4 +2,5 @@ module.exports = require('./input');
|
|||
|
||||
module.exports.PublicKey = require('./publickey');
|
||||
module.exports.PublicKeyHash = require('./publickeyhash');
|
||||
module.exports.MultiSig = require('./multisig.js');
|
||||
module.exports.MultiSigScriptHash = require('./multisigscripthash.js');
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var inherits = require('inherits');
|
||||
var Transaction = require('../transaction');
|
||||
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 PublicKey = require('../../publickey');
|
||||
var BufferUtil = require('../../util/buffer');
|
||||
var TransactionSignature = require('../signature');
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
function MultiSigInput(input, pubkeys, threshold, signatures) {
|
||||
Input.apply(this, arguments);
|
||||
var self = this;
|
||||
pubkeys = pubkeys || input.publicKeys;
|
||||
threshold = threshold || input.threshold;
|
||||
signatures = signatures || input.signatures;
|
||||
this.publicKeys = _.sortBy(pubkeys, function(publicKey) { return publicKey.toString('hex'); });
|
||||
$.checkState(Script.buildMultisigOut(this.publicKeys, threshold).equals(this.output.script),
|
||||
'Provided public keys don\'t match to the provided output script');
|
||||
this.publicKeyIndex = {};
|
||||
_.each(this.publicKeys, function(publicKey, index) {
|
||||
self.publicKeyIndex[publicKey.toString()] = index;
|
||||
});
|
||||
this.threshold = threshold;
|
||||
// Empty array of signatures
|
||||
this.signatures = signatures ? this._deserializeSignatures(signatures) : new Array(this.publicKeys.length);
|
||||
}
|
||||
inherits(MultiSigInput, Input);
|
||||
|
||||
MultiSigInput.prototype.toObject = function() {
|
||||
var obj = Input.prototype.toObject.apply(this, arguments);
|
||||
obj.threshold = this.threshold;
|
||||
obj.publicKeys = _.map(this.publicKeys, function(publicKey) { return publicKey.toString(); });
|
||||
obj.signatures = this._serializeSignatures();
|
||||
return obj;
|
||||
};
|
||||
|
||||
MultiSigInput.prototype._deserializeSignatures = function(signatures) {
|
||||
return _.map(signatures, function(signature) {
|
||||
if (!signature) {
|
||||
return undefined;
|
||||
}
|
||||
return new TransactionSignature(signature);
|
||||
});
|
||||
};
|
||||
|
||||
MultiSigInput.prototype._serializeSignatures = function() {
|
||||
return _.map(this.signatures, function(signature) {
|
||||
if (!signature) {
|
||||
return undefined;
|
||||
}
|
||||
return signature.toObject();
|
||||
});
|
||||
};
|
||||
|
||||
MultiSigInput.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(new TransactionSignature({
|
||||
publicKey: privateKey.publicKey,
|
||||
prevTxId: self.prevTxId,
|
||||
outputIndex: self.outputIndex,
|
||||
inputIndex: index,
|
||||
signature: Sighash.sign(transaction, privateKey, sigtype, index, self.output.script),
|
||||
sigtype: sigtype
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
MultiSigInput.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;
|
||||
};
|
||||
|
||||
MultiSigInput.prototype._updateScript = function() {
|
||||
this.setScript(Script.buildMultisigIn(
|
||||
this.publicKeys,
|
||||
this.threshold,
|
||||
this._createSignatures()
|
||||
));
|
||||
return this;
|
||||
};
|
||||
|
||||
MultiSigInput.prototype._createSignatures = function() {
|
||||
return _.map(
|
||||
_.filter(this.signatures, function(signature) { return !_.isUndefined(signature); }),
|
||||
function(signature) {
|
||||
return BufferUtil.concat([
|
||||
signature.signature.toDER(),
|
||||
BufferUtil.integerAsSingleByteBuffer(signature.sigtype)
|
||||
]);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
MultiSigInput.prototype.clearSignatures = function() {
|
||||
this.signatures = new Array(this.publicKeys.length);
|
||||
this._updateScript();
|
||||
};
|
||||
|
||||
MultiSigInput.prototype.isFullySigned = function() {
|
||||
return this.countSignatures() === this.threshold;
|
||||
};
|
||||
|
||||
MultiSigInput.prototype.countMissingSignatures = function() {
|
||||
return this.threshold - this.countSignatures();
|
||||
};
|
||||
|
||||
MultiSigInput.prototype.countSignatures = function() {
|
||||
return _.reduce(this.signatures, function(sum, signature) {
|
||||
return sum + (!!signature);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
MultiSigInput.prototype.publicKeysWithoutSignature = function() {
|
||||
var self = this;
|
||||
return _.filter(this.publicKeys, function(publicKey) {
|
||||
return !(self.signatures[self.publicKeyIndex[publicKey.toString()]]);
|
||||
});
|
||||
};
|
||||
|
||||
MultiSigInput.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
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Buffer[]} signatures
|
||||
* @param {PublicKey[]} publicKeys
|
||||
* @param {Transaction} transaction
|
||||
* @param {Integer} inputIndex
|
||||
* @param {Input} input
|
||||
* @returns {TransactionSignature[]}
|
||||
*/
|
||||
MultiSigInput.normalizeSignatures = function(transaction, input, inputIndex, signatures, publicKeys) {
|
||||
return publicKeys.map(function (pubKey) {
|
||||
var signatureMatch = null;
|
||||
signatures = signatures.filter(function (signatureBuffer) {
|
||||
if (signatureMatch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var signature = new TransactionSignature({
|
||||
signature: Signature.fromTxFormat(signatureBuffer),
|
||||
publicKey: pubKey,
|
||||
prevTxId: input.prevTxId,
|
||||
outputIndex: input.outputIndex,
|
||||
inputIndex: inputIndex,
|
||||
sigtype: Signature.SIGHASH_ALL
|
||||
});
|
||||
|
||||
signature.signature.nhashtype = signature.sigtype;
|
||||
var isMatch = Sighash.verify(
|
||||
transaction,
|
||||
signature.signature,
|
||||
signature.publicKey,
|
||||
signature.inputIndex,
|
||||
input.output.script
|
||||
);
|
||||
|
||||
if (isMatch) {
|
||||
signatureMatch = signature;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return signatureMatch ? signatureMatch : null;
|
||||
});
|
||||
};
|
||||
|
||||
MultiSigInput.OPCODES_SIZE = 1; // 0
|
||||
MultiSigInput.SIGNATURE_SIZE = 73; // size (1) + DER (<=72)
|
||||
|
||||
MultiSigInput.prototype._estimateSize = function() {
|
||||
return MultiSigInput.OPCODES_SIZE +
|
||||
this.threshold * MultiSigInput.SIGNATURE_SIZE;
|
||||
};
|
||||
|
||||
module.exports = MultiSigInput;
|
|
@ -20,6 +20,7 @@ var Input = require('./input');
|
|||
var PublicKeyHashInput = Input.PublicKeyHash;
|
||||
var PublicKeyInput = Input.PublicKey;
|
||||
var MultiSigScriptHashInput = Input.MultiSigScriptHash;
|
||||
var MultiSigInput = Input.MultiSig;
|
||||
var Output = require('./output');
|
||||
var Script = require('../script');
|
||||
var PrivateKey = require('../privatekey');
|
||||
|
@ -137,7 +138,7 @@ Transaction.prototype._getHash = function() {
|
|||
* * `disableAll`: disable all checks
|
||||
* * `disableSmallFees`: disable checking for fees that are too small
|
||||
* * `disableLargeFees`: disable checking for fees that are too large
|
||||
* * `disableNotFullySigned`: disable checking if all inputs are fully signed
|
||||
* * `disableIsFullySigned`: disable checking if all inputs are fully signed
|
||||
* * `disableDustOutputs`: disable checking if there are no outputs that are dust amounts
|
||||
* * `disableMoreOutputThanInput`: disable checking if the transaction spends more bitcoins than the sum of the input amounts
|
||||
* @return {string}
|
||||
|
@ -577,8 +578,16 @@ Transaction.prototype._fromNonP2SH = function(utxo) {
|
|||
Transaction.prototype._fromMultisigUtxo = function(utxo, pubkeys, threshold) {
|
||||
$.checkArgument(threshold <= pubkeys.length,
|
||||
'Number of required signatures must be greater than the number of public keys');
|
||||
var clazz;
|
||||
utxo = new UnspentOutput(utxo);
|
||||
this.addInput(new MultiSigScriptHashInput({
|
||||
if (utxo.script.isMultisigOut()) {
|
||||
clazz = MultiSigInput;
|
||||
} else if (utxo.script.isScriptHashOut()) {
|
||||
clazz = MultiSigScriptHashInput;
|
||||
} else {
|
||||
throw new Error("@TODO");
|
||||
}
|
||||
this.addInput(new clazz({
|
||||
output: new Output({
|
||||
script: utxo.script,
|
||||
satoshis: utxo.satoshis
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
'use strict';
|
||||
/* jshint unused: false */
|
||||
|
||||
var should = require('chai').should();
|
||||
var expect = require('chai').expect;
|
||||
var _ = require('lodash');
|
||||
|
||||
var bitcore = require('../../..');
|
||||
var Transaction = bitcore.Transaction;
|
||||
var PrivateKey = bitcore.PrivateKey;
|
||||
var Address = bitcore.Address;
|
||||
var Script = bitcore.Script;
|
||||
var Signature = bitcore.crypto.Signature;
|
||||
var MultiSigInput = bitcore.Transaction.Input.MultiSig;
|
||||
|
||||
describe('MultiSigInput', function() {
|
||||
|
||||
var privateKey1 = new PrivateKey('KwF9LjRraetZuEjR8VqEq539z137LW5anYDUnVK11vM3mNMHTWb4');
|
||||
var privateKey2 = new PrivateKey('L4PqnaPTCkYhAqH3YQmefjxQP6zRcF4EJbdGqR8v6adtG9XSsadY');
|
||||
var privateKey3 = new PrivateKey('L4CTX79zFeksZTyyoFuPQAySfmP7fL3R41gWKTuepuN7hxuNuJwV');
|
||||
var public1 = privateKey1.publicKey;
|
||||
var public2 = privateKey2.publicKey;
|
||||
var public3 = privateKey3.publicKey;
|
||||
var address = new Address('33zbk2aSZYdNbRsMPPt6jgy6Kq1kQreqeb');
|
||||
|
||||
var output = {
|
||||
txId: '66e64ef8a3b384164b78453fa8c8194de9a473ba14f89485a0e433699daec140',
|
||||
outputIndex: 0,
|
||||
script: new Script("5221025c95ec627038e85b5688a9b3d84d28c5ebe66e8c8d697d498e20fe96e3b1ab1d2102cdddfc974d41a62f1f80081deee70592feb7d6e6cf6739d6592edbe7946720e72103c95924e02c240b5545089c69c6432447412b58be43fd671918bd184a5009834353ae"),
|
||||
satoshis: 1000000
|
||||
};
|
||||
it('can count missing signatures', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(output, [public1, public2, public3], 2)
|
||||
.to(address, 1000000);
|
||||
var input = transaction.inputs[0];
|
||||
|
||||
input.countSignatures().should.equal(0);
|
||||
|
||||
transaction.sign(privateKey1);
|
||||
input.countSignatures().should.equal(1);
|
||||
input.countMissingSignatures().should.equal(1);
|
||||
input.isFullySigned().should.equal(false);
|
||||
|
||||
transaction.sign(privateKey2);
|
||||
input.countSignatures().should.equal(2);
|
||||
input.countMissingSignatures().should.equal(0);
|
||||
input.isFullySigned().should.equal(true);
|
||||
});
|
||||
it('can count missing signatures, signed with key 3 and 1', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(output, [public1, public2, public3], 2)
|
||||
.to(address, 1000000);
|
||||
var input = transaction.inputs[0];
|
||||
|
||||
input.countSignatures().should.equal(0);
|
||||
|
||||
transaction.sign(privateKey3);
|
||||
input.countSignatures().should.equal(1);
|
||||
input.countMissingSignatures().should.equal(1);
|
||||
input.isFullySigned().should.equal(false);
|
||||
|
||||
transaction.sign(privateKey1);
|
||||
input.countSignatures().should.equal(2);
|
||||
input.countMissingSignatures().should.equal(0);
|
||||
input.isFullySigned().should.equal(true);
|
||||
});
|
||||
it('returns a list of public keys with missing signatures', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(output, [public1, public2, public3], 2)
|
||||
.to(address, 1000000);
|
||||
var input = transaction.inputs[0];
|
||||
|
||||
_.all(input.publicKeysWithoutSignature(), function(publicKeyMissing) {
|
||||
var serialized = publicKeyMissing.toString();
|
||||
return serialized === public1.toString() ||
|
||||
serialized === public2.toString() ||
|
||||
serialized === public3.toString();
|
||||
}).should.equal(true);
|
||||
transaction.sign(privateKey1);
|
||||
_.all(input.publicKeysWithoutSignature(), function(publicKeyMissing) {
|
||||
var serialized = publicKeyMissing.toString();
|
||||
return serialized === public2.toString() ||
|
||||
serialized === public3.toString();
|
||||
}).should.equal(true);
|
||||
});
|
||||
it('can clear all signatures', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(output, [public1, public2, public3], 2)
|
||||
.to(address, 1000000)
|
||||
.sign(privateKey1)
|
||||
.sign(privateKey2);
|
||||
|
||||
var input = transaction.inputs[0];
|
||||
input.isFullySigned().should.equal(true);
|
||||
input.clearSignatures();
|
||||
input.isFullySigned().should.equal(false);
|
||||
});
|
||||
it('can estimate how heavy is the output going to be', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(output, [public1, public2, public3], 2)
|
||||
.to(address, 1000000);
|
||||
var input = transaction.inputs[0];
|
||||
input._estimateSize().should.equal(147);
|
||||
});
|
||||
it('uses SIGHASH_ALL by default', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(output, [public1, public2, public3], 2)
|
||||
.to(address, 1000000);
|
||||
var input = transaction.inputs[0];
|
||||
var sigs = input.getSignatures(transaction, privateKey1, 0);
|
||||
sigs[0].sigtype.should.equal(Signature.SIGHASH_ALL);
|
||||
});
|
||||
it('roundtrips to/from object', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(output, [public1, public2, public3], 2)
|
||||
.to(address, 1000000)
|
||||
.sign(privateKey1);
|
||||
var input = transaction.inputs[0];
|
||||
var roundtrip = new MultiSigInput(input.toObject());
|
||||
roundtrip.toObject().should.deep.equal(input.toObject());
|
||||
});
|
||||
it('roundtrips to/from object when not signed', function() {
|
||||
var transaction = new Transaction()
|
||||
.from(output, [public1, public2, public3], 2)
|
||||
.to(address, 1000000);
|
||||
var input = transaction.inputs[0];
|
||||
var roundtrip = new MultiSigInput(input.toObject());
|
||||
roundtrip.toObject().should.deep.equal(input.toObject());
|
||||
});
|
||||
it('can parse list of signature buffers, from TX signed with key 1 and 2', function() {
|
||||
var transaction = new Transaction("010000000140c1ae9d6933e4a08594f814ba73a4e94d19c8a83f45784b1684b3a3f84ee666000000009200473044022012bd2f15e56ab1b63d5ee23e194ed995ad4b81a21bcb8e0d913e5e791c07f7280220278bdb6b54cdc608193c869affe28dc2f700902218122770faff25c56142102b01483045022100e74e9955e042aca36f4f3ad907a0926c5b85e5d9608b0678a78a9cbc0259c7a2022053ff761e5f9a80558db7023e45c4979ac3c19a423f0184fb0596d3da308cc4b501ffffffff0140420f000000000017a91419438da7d16709643be5abd8df62ca4034a489a78700000000");
|
||||
|
||||
var inputObj = transaction.inputs[0].toObject();
|
||||
inputObj.output = output;
|
||||
transaction.inputs[0] = new Transaction.Input(inputObj);
|
||||
|
||||
inputObj.signatures = MultiSigInput.normalizeSignatures(
|
||||
transaction,
|
||||
transaction.inputs[0],
|
||||
0,
|
||||
transaction.inputs[0].script.chunks.slice(1).map(function(s) { return s.buf; }),
|
||||
[public1, public2, public3]
|
||||
);
|
||||
|
||||
transaction.inputs[0] = new MultiSigInput(inputObj, [public1, public2, public3], 2);
|
||||
|
||||
transaction.inputs[0].signatures[0].publicKey.should.deep.equal(public1);
|
||||
transaction.inputs[0].signatures[1].publicKey.should.deep.equal(public2);
|
||||
should.equal(transaction.inputs[0].signatures[2], undefined);
|
||||
transaction.inputs[0].isValidSignature(transaction, transaction.inputs[0].signatures[0]).should.be.true;
|
||||
transaction.inputs[0].isValidSignature(transaction, transaction.inputs[0].signatures[1]).should.be.true;
|
||||
});
|
||||
it('can parse list of signature buffers, from TX signed with key 3 and 1', function() {
|
||||
var transaction = new Transaction("010000000140c1ae9d6933e4a08594f814ba73a4e94d19c8a83f45784b1684b3a3f84ee666000000009300483045022100fc39ce4f51b2766ec8e978296e0594ea4578a3eb2543722fd4053e92bf16e6b1022030f739868397a881b019508b9c725a5c69a3652cb8928027748e93e67dfaef5501483045022100e74e9955e042aca36f4f3ad907a0926c5b85e5d9608b0678a78a9cbc0259c7a2022053ff761e5f9a80558db7023e45c4979ac3c19a423f0184fb0596d3da308cc4b501ffffffff0140420f000000000017a91419438da7d16709643be5abd8df62ca4034a489a78700000000");
|
||||
|
||||
var inputObj = transaction.inputs[0].toObject();
|
||||
inputObj.output = output;
|
||||
transaction.inputs[0] = new Transaction.Input(inputObj);
|
||||
|
||||
inputObj.signatures = MultiSigInput.normalizeSignatures(
|
||||
transaction,
|
||||
transaction.inputs[0],
|
||||
0,
|
||||
transaction.inputs[0].script.chunks.slice(1).map(function(s) { return s.buf; }),
|
||||
[public1, public2, public3]
|
||||
);
|
||||
|
||||
transaction.inputs[0] = new MultiSigInput(inputObj, [public1, public2, public3], 2);
|
||||
|
||||
transaction.inputs[0].signatures[0].publicKey.should.deep.equal(public1);
|
||||
should.equal(transaction.inputs[0].signatures[1], undefined);
|
||||
transaction.inputs[0].signatures[2].publicKey.should.deep.equal(public3);
|
||||
transaction.inputs[0].isValidSignature(transaction, transaction.inputs[0].signatures[0]).should.be.true;
|
||||
transaction.inputs[0].isValidSignature(transaction, transaction.inputs[0].signatures[2]).should.be.true;
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue