transaction signing
This commit is contained in:
parent
471a9e7c1a
commit
4bf541c7b1
|
@ -26,9 +26,16 @@ function Copayer(opts) {
|
||||||
this.addressManager = new AddressManager({ copayerIndex: opts.copayerIndex });
|
this.addressManager = new AddressManager({ copayerIndex: opts.copayerIndex });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Copayer.prototype.getPublicKey = function(path) {
|
||||||
|
return HDPublicKey
|
||||||
|
.fromString(this.xPubKey)
|
||||||
|
.derive(path)
|
||||||
|
.publicKey
|
||||||
|
.toString();
|
||||||
|
};
|
||||||
|
|
||||||
Copayer.prototype.getSigningPubKey = function() {
|
Copayer.prototype.getSigningPubKey = function() {
|
||||||
if (!this.xPubKey) return null;
|
return this.getPublicKey(MESSAGE_SIGNING_PATH);
|
||||||
return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Copayer.fromObj = function(obj) {
|
Copayer.fromObj = function(obj) {
|
||||||
|
|
|
@ -24,10 +24,10 @@ function TxProposal(opts) {
|
||||||
this.requiredSignatures = opts.requiredSignatures;
|
this.requiredSignatures = opts.requiredSignatures;
|
||||||
this.maxRejections = opts.maxRejections;
|
this.maxRejections = opts.maxRejections;
|
||||||
this.status = 'pending';
|
this.status = 'pending';
|
||||||
this.actions = [];
|
this.actions = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.fromObj = function (obj) {
|
TxProposal.fromObj = function(obj) {
|
||||||
var x = new TxProposal();
|
var x = new TxProposal();
|
||||||
|
|
||||||
x.version = obj.version;
|
x.version = obj.version;
|
||||||
|
@ -44,15 +44,16 @@ TxProposal.fromObj = function (obj) {
|
||||||
x.status = obj.status;
|
x.status = obj.status;
|
||||||
x.txid = obj.txid;
|
x.txid = obj.txid;
|
||||||
x.inputPaths = obj.inputPaths;
|
x.inputPaths = obj.inputPaths;
|
||||||
x.actions = _.map(obj.actions, function(action) {
|
x.actions = obj.actions;
|
||||||
return new TxProposalAction(action);
|
_.each(x.actions, function(action, copayerId) {
|
||||||
|
x.actions[copayerId] = new TxProposalAction(action);
|
||||||
});
|
});
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
TxProposal.prototype._updateStatus = function () {
|
TxProposal.prototype._updateStatus = function() {
|
||||||
if (this.status != 'pending') return;
|
if (this.status != 'pending') return;
|
||||||
|
|
||||||
if (this.isRejected()) {
|
if (this.isRejected()) {
|
||||||
|
@ -63,52 +64,88 @@ TxProposal.prototype._updateStatus = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
TxProposal.prototype._getBitcoreTx = function () {
|
TxProposal.prototype._getBitcoreTx = function(n) {
|
||||||
return new Bitcore.Transaction()
|
var self = this;
|
||||||
.from(this.inputs)
|
|
||||||
.to(this.toAddress, this.amount)
|
var t = new Bitcore.Transaction();
|
||||||
|
_.each(this.inputs, function(i) {
|
||||||
|
t.from(i, i.publicKeys, self.requiredSignatures)
|
||||||
|
});
|
||||||
|
|
||||||
|
t.to(this.toAddress, this.amount)
|
||||||
.change(this.changeAddress);
|
.change(this.changeAddress);
|
||||||
|
|
||||||
|
t._updateChangeOutput();
|
||||||
|
return t;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
TxProposal.prototype.addAction = function (copayerId, type, signature) {
|
TxProposal.prototype.addAction = function(copayerId, type, signatures) {
|
||||||
var action = new TxProposalAction({
|
var action = new TxProposalAction({
|
||||||
copayerId: copayerId,
|
copayerId: copayerId,
|
||||||
type: type,
|
type: type,
|
||||||
signature: signature,
|
signatures: signatures,
|
||||||
});
|
});
|
||||||
this.actions.push(action);
|
this.actions[copayerId] = action;
|
||||||
this._updateStatus();
|
this._updateStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: no sure we should receive xpub or a list of pubkeys (pre derived)
|
||||||
|
TxProposal.prototype.checkSignatures = function(signatures, xpub) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
TxProposal.prototype._checkSignature = function (signatures) {
|
|
||||||
var t = this._getBitcoreTx();
|
var t = this._getBitcoreTx();
|
||||||
t.applyS
|
|
||||||
|
|
||||||
|
if (signatures.length != this.inputs.length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var oks = 0,
|
||||||
|
i = 0,
|
||||||
|
x = new Bitcore.HDPublicKey(xpub);
|
||||||
|
|
||||||
|
_.each(signatures, function(signatureHex) {
|
||||||
|
var input = self.inputs[i];
|
||||||
|
try {
|
||||||
|
var signature = Bitcore.crypto.Signature.fromString(signatureHex);
|
||||||
|
var pub = x.derive(self.inputPaths[i]).publicKey;
|
||||||
|
var s = {
|
||||||
|
inputIndex: i,
|
||||||
|
signature: signature,
|
||||||
|
sigtype: Bitcore.crypto.Signature.SIGHASH_ALL,
|
||||||
|
publicKey: pub,
|
||||||
|
};
|
||||||
|
i++;
|
||||||
|
|
||||||
|
t.applySignature(s);
|
||||||
|
oks++;
|
||||||
|
} catch (e) {
|
||||||
|
// TODO only for debug now
|
||||||
|
console.log('DEBUG ONLY:',e.message); //TODO
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return oks === t.inputs.length;
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.prototype.sign = function (copayerId, signatures) {
|
|
||||||
this._checkSignature(signature);
|
TxProposal.prototype.sign = function(copayerId, signatures) {
|
||||||
this.addAction(copayerId, 'accept', signature);
|
this.addAction(copayerId, 'accept', signatures);
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.prototype.reject = function (copayerId) {
|
TxProposal.prototype.reject = function(copayerId) {
|
||||||
this.addAction(copayerId, 'reject');
|
this.addAction(copayerId, 'reject');
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.prototype.isAccepted = function () {
|
TxProposal.prototype.isAccepted = function() {
|
||||||
var votes = _.countBy(this.actions, 'type');
|
var votes = _.countBy(_.values(this.actions), 'type');
|
||||||
return votes['accept'] >= this.requiredSignatures;
|
return votes['accept'] >= this.requiredSignatures;
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.prototype.isRejected = function () {
|
TxProposal.prototype.isRejected = function() {
|
||||||
var votes = _.countBy(this.actions, 'type');
|
var votes = _.countBy(_.values(this.actions), 'type');
|
||||||
return votes['reject'] > this.maxRejections;
|
return votes['reject'] > this.maxRejections;
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposal.prototype.setBroadcasted = function (txid) {
|
TxProposal.prototype.setBroadcasted = function(txid) {
|
||||||
this.txid = txid;
|
this.txid = txid;
|
||||||
this.status = 'broadcasted';
|
this.status = 'broadcasted';
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,8 +5,8 @@ function TxProposalAction(opts) {
|
||||||
|
|
||||||
this.createdOn = Math.floor(Date.now() / 1000);
|
this.createdOn = Math.floor(Date.now() / 1000);
|
||||||
this.copayerId = opts.copayerId;
|
this.copayerId = opts.copayerId;
|
||||||
this.type = opts.type || (opts.signature ? 'accept' : 'reject');
|
this.type = opts.type || (opts.signatures ? 'accept' : 'reject');
|
||||||
this.signature = opts.signature;
|
this.signatures = opts.signatures;
|
||||||
};
|
};
|
||||||
|
|
||||||
TxProposalAction.fromObj = function (obj) {
|
TxProposalAction.fromObj = function (obj) {
|
||||||
|
@ -15,7 +15,7 @@ TxProposalAction.fromObj = function (obj) {
|
||||||
x.createdOn = obj.createdOn;
|
x.createdOn = obj.createdOn;
|
||||||
x.copayerId = obj.copayerId;
|
x.copayerId = obj.copayerId;
|
||||||
x.type = obj.type;
|
x.type = obj.type;
|
||||||
x.signature = obj.signature;
|
x.signatures = obj.signatures;
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
};
|
};
|
||||||
|
|
|
@ -100,6 +100,12 @@ Wallet.prototype._getBitcoreNetwork = function() {
|
||||||
return this.isTestnet ? Bitcore.Networks.testnet : Bitcore.Networks.livenet;
|
return this.isTestnet ? Bitcore.Networks.testnet : Bitcore.Networks.livenet;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Wallet.prototype.getPublicKey = function(copayerId, path) {
|
||||||
|
var copayer = this.getCopayer(copayerId);
|
||||||
|
return copayer.getPublicKey(path);
|
||||||
|
};
|
||||||
|
|
||||||
Wallet.prototype.createAddress = function(isChange) {
|
Wallet.prototype.createAddress = function(isChange) {
|
||||||
var path = this.addressManager.getNewAddressPath(isChange);
|
var path = this.addressManager.getNewAddressPath(isChange);
|
||||||
|
|
||||||
|
|
|
@ -472,37 +472,50 @@ CopayServer.prototype.signTx = function(opts, cb) {
|
||||||
|
|
||||||
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId', 'signatures']);
|
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId', 'signatures']);
|
||||||
|
|
||||||
self.getTx({
|
self.getWallet({
|
||||||
walletId: opts.walletId,
|
id: opts.walletId
|
||||||
id: opts.txProposalId
|
}, function(err, wallet) {
|
||||||
}, function(err, txp) {
|
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
if (!txp) return cb(new ClientError('Transaction proposal not found'));
|
|
||||||
var action = _.find(txp.actions, {
|
|
||||||
copayerId: opts.copayerId
|
|
||||||
});
|
|
||||||
if (action) return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal'));
|
|
||||||
if (txp.status != 'pending') return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending'));
|
|
||||||
|
|
||||||
txp.sign(opts.copayerId, opts.signatures);
|
self.getTx({
|
||||||
|
walletId: opts.walletId,
|
||||||
self.storage.storeTx(opts.walletId, txp, function(err) {
|
id: opts.txProposalId
|
||||||
|
}, function(err, txp) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
if (!txp) return cb(new ClientError('Transaction proposal not found'));
|
||||||
|
var action = _.find(txp.actions, {
|
||||||
|
copayerId: opts.copayerId
|
||||||
|
});
|
||||||
|
if (action)
|
||||||
|
return cb(new ClientError('CVOTED', 'Copayer already voted on this transaction proposal'));
|
||||||
|
if (txp.status != 'pending')
|
||||||
|
return cb(new ClientError('TXNOTPENDING', 'The transaction proposal is not pending'));
|
||||||
|
|
||||||
if (txp.status == 'accepted') {
|
var copayer = wallet.getCopayer(opts.copayerId);
|
||||||
self._broadcastTx(txp.rawTx, function(err, txid) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
|
|
||||||
tx.setBroadcasted(txid);
|
if (!txp.checkSignatures(opts.signatures, copayer.xPubKey))
|
||||||
self.storage.storeTx(opts.walletId, txp, function(err) {
|
return cb(new ClientError('BADSIGNATURES', 'Bad signatures'));
|
||||||
|
|
||||||
|
txp.sign(opts.copayerId, opts.signatures);
|
||||||
|
|
||||||
|
self.storage.storeTx(opts.walletId, txp, function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
if (txp.status == 'accepted') {
|
||||||
|
self._broadcastTx(txp.rawTx, function(err, txid) {
|
||||||
if (err) return cb(err);
|
if (err) return cb(err);
|
||||||
|
|
||||||
return cb(null, txp);
|
tx.setBroadcasted(txid);
|
||||||
|
self.storage.storeTx(opts.walletId, txp, function(err) {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
return cb(null, txp);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
return cb(null, txp);
|
||||||
return cb(null, txp);
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -170,14 +170,11 @@ helpers.clientSign = function(tx, xpriv, n) {
|
||||||
.sign(privs);
|
.sign(privs);
|
||||||
|
|
||||||
var signatures = [];
|
var signatures = [];
|
||||||
//console.log('Bitcore Transaction:', t); //TODO
|
|
||||||
_.each(privs, function(p) {
|
_.each(privs, function(p) {
|
||||||
var s = t.getSignatures(p)[0].signature.toDER().toString('hex');
|
var s = t.getSignatures(p)[0].signature.toDER().toString('hex');
|
||||||
// console.log('\n## Priv key:', p);
|
signatures.push(s);
|
||||||
// console.log('\t\t->> signature ->>', s); //TODO
|
});
|
||||||
signatures.push(s);
|
//
|
||||||
});
|
|
||||||
|
|
||||||
return signatures;
|
return signatures;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -980,28 +977,67 @@ describe('Copay server', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should sign a TX', function(done) {
|
it('should sign a TX with multiple inputs, different paths', function(done) {
|
||||||
server.getPendingTxs({
|
server.getPendingTxs({
|
||||||
walletId: '123'
|
walletId: '123'
|
||||||
}, function(err, txs) {
|
}, function(err, txs) {
|
||||||
var tx = txs[0];
|
var tx = txs[0];
|
||||||
tx.id.should.equal(txid);
|
tx.id.should.equal(txid);
|
||||||
|
|
||||||
//
|
|
||||||
var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n);
|
var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n);
|
||||||
console.log('[integration.js.992:signatures:]',signatures); //TODO
|
|
||||||
server.signTx({
|
server.signTx({
|
||||||
walletId: '123',
|
walletId: '123',
|
||||||
copayerId: '1',
|
copayerId: '1',
|
||||||
txProposalId: txid,
|
txProposalId: txid,
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
|
should.not.exist(err);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail if one signature is broken', function(done) {
|
||||||
|
server.getPendingTxs({
|
||||||
|
walletId: '123'
|
||||||
|
}, function(err, txs) {
|
||||||
|
var tx = txs[0];
|
||||||
|
tx.id.should.equal(txid);
|
||||||
|
|
||||||
|
var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n);
|
||||||
|
signatures[0]=1;
|
||||||
|
|
||||||
|
server.signTx({
|
||||||
|
walletId: '123',
|
||||||
|
copayerId: '1',
|
||||||
|
txProposalId: txid,
|
||||||
|
signatures: signatures,
|
||||||
|
}, function(err) {
|
||||||
|
err.message.should.contain('signatures');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should fail on invalids signature', function(done) {
|
||||||
|
server.getPendingTxs({
|
||||||
|
walletId: '123'
|
||||||
|
}, function(err, txs) {
|
||||||
|
var tx = txs[0];
|
||||||
|
tx.id.should.equal(txid);
|
||||||
|
|
||||||
|
var signatures = ['11', '22', '33', '44'];
|
||||||
|
server.signTx({
|
||||||
|
walletId: '123',
|
||||||
|
copayerId: '1',
|
||||||
|
txProposalId: txid,
|
||||||
|
signatures: signatures,
|
||||||
|
}, function(err) {
|
||||||
|
err.message.should.contain('signatures');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var _ = require('lodash');
|
||||||
|
var chai = require('chai');
|
||||||
|
var sinon = require('sinon');
|
||||||
|
var should = chai.should();
|
||||||
|
var TXP = require('../lib/model/txproposal');
|
||||||
|
var Bitcore = require('bitcore');
|
||||||
|
|
||||||
|
|
||||||
|
describe('TXProposal', function() {
|
||||||
|
|
||||||
|
describe('#fromObj', function() {
|
||||||
|
it('should create a TXP', function() {
|
||||||
|
var txp = TXP.fromObj(aTXP());
|
||||||
|
should.exist(txp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('#_getBitcoreTx', function() {
|
||||||
|
it('should create a valid bitcore TX', function() {
|
||||||
|
var txp = TXP.fromObj(aTXP());
|
||||||
|
var t = txp._getBitcoreTx();
|
||||||
|
should.exist(t);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('#sign', function() {
|
||||||
|
it('should sign 2-2', function() {
|
||||||
|
var txp = TXP.fromObj(aTXP());
|
||||||
|
txp.sign('1', theSignatures);
|
||||||
|
txp.isAccepted().should.equal(false);
|
||||||
|
txp.isRejected().should.equal(false);
|
||||||
|
txp.sign('2', theSignatures);
|
||||||
|
txp.isAccepted().should.equal(true);
|
||||||
|
txp.isRejected().should.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#reject', function() {
|
||||||
|
it('should reject 2-2', function() {
|
||||||
|
var txp = TXP.fromObj(aTXP());
|
||||||
|
txp.reject('1');
|
||||||
|
txp.isAccepted().should.equal(false);
|
||||||
|
txp.isRejected().should.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('#reject & #sign', function() {
|
||||||
|
it('should finally reject', function() {
|
||||||
|
var txp = TXP.fromObj(aTXP());
|
||||||
|
txp.sign('1', theSignatures);
|
||||||
|
txp.isAccepted().should.equal(false);
|
||||||
|
txp.isRejected().should.equal(false);
|
||||||
|
txp.reject('2');
|
||||||
|
txp.isAccepted().should.equal(false);
|
||||||
|
txp.isRejected().should.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('#checkSignatures', function() {
|
||||||
|
it('should check signatures', function() {
|
||||||
|
var txp = TXP.fromObj(aTXP());
|
||||||
|
var xpriv = new Bitcore.HDPrivateKey(theXPriv);
|
||||||
|
var priv = xpriv.derive(txp.inputPaths[0]).privateKey;
|
||||||
|
|
||||||
|
var t = txp._getBitcoreTx();
|
||||||
|
t.sign(priv);
|
||||||
|
|
||||||
|
var s = t.getSignatures(priv)[0].signature.toDER().toString('hex');
|
||||||
|
var xpub = new Bitcore.HDPublicKey(xpriv);
|
||||||
|
|
||||||
|
var res = txp.checkSignatures([s], xpub);
|
||||||
|
res.should.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var theXPriv = 'xprv9s21ZrQH143K2rMHbXTJmWTuFx6ssqn1vyRoZqPkCXYchBSkp5ey8kMJe84sxfXq5uChWH4gk94rWbXZt2opN9kg4ufKGvUM7HQSLjnoh7e';
|
||||||
|
var theSignatures = ['3045022100896aeb8db75fec22fddb5facf791927a996eb3aee23ee6deaa15471ea46047de02204c0c33f42a9d3ff93d62738712a8c8a5ecd21b45393fdd144e7b01b5a186f1f9'];
|
||||||
|
|
||||||
|
var aTXP = function() {
|
||||||
|
return {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"createdOn": 1423146231,
|
||||||
|
"id": "75c34f49-1ed6-255f-e9fd-0c71ae75ed1e",
|
||||||
|
"creatorId": "1",
|
||||||
|
"toAddress": "18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7",
|
||||||
|
"amount": 50000000,
|
||||||
|
"changeAddress": "3CauZ5JUFfmSAx2yANvCRoNXccZ3YSUjXH",
|
||||||
|
"inputs": [{
|
||||||
|
"txid": "6ee699846d2d6605f96d20c7cc8230382e5da43342adb11b499bbe73709f06ab",
|
||||||
|
"vout": 8,
|
||||||
|
"satoshis": 100000000,
|
||||||
|
"scriptPubKey": "a914a8a9648754fbda1b6c208ac9d4e252075447f36887",
|
||||||
|
"address": "3H4pNP6J4PW4NnvdrTg37VvZ7h2QWuAwtA",
|
||||||
|
"path": "m/2147483647/0/1",
|
||||||
|
"publicKeys": ["0319008ffe1b3e208f5ebed8f46495c056763f87b07930a7027a92ee477fb0cb0f", "03b5f035af8be40d0db5abb306b7754949ab39032cf99ad177691753b37d101301"]
|
||||||
|
}],
|
||||||
|
"inputPaths": ["m/2147483647/0/1"],
|
||||||
|
"requiredSignatures": 2,
|
||||||
|
"maxRejections": 0,
|
||||||
|
"status": "pending",
|
||||||
|
"actions": []
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue