transaction signing

This commit is contained in:
Matias Alejo Garcia 2015-02-05 15:50:18 -03:00
parent 471a9e7c1a
commit 4bf541c7b1
7 changed files with 270 additions and 63 deletions

View File

@ -26,9 +26,16 @@ function Copayer(opts) {
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() {
if (!this.xPubKey) return null;
return HDPublicKey.fromString(this.xPubKey).derive(MESSAGE_SIGNING_PATH).publicKey.toString();
return this.getPublicKey(MESSAGE_SIGNING_PATH);
};
Copayer.fromObj = function(obj) {

View File

@ -24,10 +24,10 @@ function TxProposal(opts) {
this.requiredSignatures = opts.requiredSignatures;
this.maxRejections = opts.maxRejections;
this.status = 'pending';
this.actions = [];
this.actions = {};
};
TxProposal.fromObj = function (obj) {
TxProposal.fromObj = function(obj) {
var x = new TxProposal();
x.version = obj.version;
@ -44,15 +44,16 @@ TxProposal.fromObj = function (obj) {
x.status = obj.status;
x.txid = obj.txid;
x.inputPaths = obj.inputPaths;
x.actions = _.map(obj.actions, function(action) {
return new TxProposalAction(action);
x.actions = obj.actions;
_.each(x.actions, function(action, copayerId) {
x.actions[copayerId] = new TxProposalAction(action);
});
return x;
};
TxProposal.prototype._updateStatus = function () {
TxProposal.prototype._updateStatus = function() {
if (this.status != 'pending') return;
if (this.isRejected()) {
@ -63,52 +64,88 @@ TxProposal.prototype._updateStatus = function () {
};
TxProposal.prototype._getBitcoreTx = function () {
return new Bitcore.Transaction()
.from(this.inputs)
.to(this.toAddress, this.amount)
TxProposal.prototype._getBitcoreTx = function(n) {
var self = this;
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);
t._updateChangeOutput();
return t;
};
TxProposal.prototype.addAction = function (copayerId, type, signature) {
TxProposal.prototype.addAction = function(copayerId, type, signatures) {
var action = new TxProposalAction({
copayerId: copayerId,
type: type,
signature: signature,
signatures: signatures,
});
this.actions.push(action);
this.actions[copayerId] = action;
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();
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);
this.addAction(copayerId, 'accept', signature);
TxProposal.prototype.sign = function(copayerId, signatures) {
this.addAction(copayerId, 'accept', signatures);
};
TxProposal.prototype.reject = function (copayerId) {
TxProposal.prototype.reject = function(copayerId) {
this.addAction(copayerId, 'reject');
};
TxProposal.prototype.isAccepted = function () {
var votes = _.countBy(this.actions, 'type');
TxProposal.prototype.isAccepted = function() {
var votes = _.countBy(_.values(this.actions), 'type');
return votes['accept'] >= this.requiredSignatures;
};
TxProposal.prototype.isRejected = function () {
var votes = _.countBy(this.actions, 'type');
TxProposal.prototype.isRejected = function() {
var votes = _.countBy(_.values(this.actions), 'type');
return votes['reject'] > this.maxRejections;
};
TxProposal.prototype.setBroadcasted = function (txid) {
TxProposal.prototype.setBroadcasted = function(txid) {
this.txid = txid;
this.status = 'broadcasted';
};

View File

@ -5,8 +5,8 @@ function TxProposalAction(opts) {
this.createdOn = Math.floor(Date.now() / 1000);
this.copayerId = opts.copayerId;
this.type = opts.type || (opts.signature ? 'accept' : 'reject');
this.signature = opts.signature;
this.type = opts.type || (opts.signatures ? 'accept' : 'reject');
this.signatures = opts.signatures;
};
TxProposalAction.fromObj = function (obj) {
@ -15,7 +15,7 @@ TxProposalAction.fromObj = function (obj) {
x.createdOn = obj.createdOn;
x.copayerId = obj.copayerId;
x.type = obj.type;
x.signature = obj.signature;
x.signatures = obj.signatures;
return x;
};

View File

@ -100,6 +100,12 @@ Wallet.prototype._getBitcoreNetwork = function() {
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) {
var path = this.addressManager.getNewAddressPath(isChange);

View File

@ -472,37 +472,50 @@ CopayServer.prototype.signTx = function(opts, cb) {
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId', 'signatures']);
self.getTx({
walletId: opts.walletId,
id: opts.txProposalId
}, function(err, txp) {
self.getWallet({
id: opts.walletId
}, function(err, wallet) {
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.storage.storeTx(opts.walletId, txp, function(err) {
self.getTx({
walletId: opts.walletId,
id: opts.txProposalId
}, function(err, txp) {
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') {
self._broadcastTx(txp.rawTx, function(err, txid) {
if (err) return cb(err);
var copayer = wallet.getCopayer(opts.copayerId);
tx.setBroadcasted(txid);
self.storage.storeTx(opts.walletId, txp, function(err) {
if (!txp.checkSignatures(opts.signatures, copayer.xPubKey))
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);
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 {
return cb(null, txp);
}
} else {
return cb(null, txp);
}
});
});
});
};

View File

@ -170,14 +170,11 @@ helpers.clientSign = function(tx, xpriv, n) {
.sign(privs);
var signatures = [];
//console.log('Bitcore Transaction:', t); //TODO
_.each(privs, function(p) {
var s = t.getSignatures(p)[0].signature.toDER().toString('hex');
// console.log('\n## Priv key:', p);
// console.log('\t\t->> signature ->>', s); //TODO
signatures.push(s);
});
var s = t.getSignatures(p)[0].signature.toDER().toString('hex');
signatures.push(s);
});
//
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({
walletId: '123'
}, function(err, txs) {
var tx = txs[0];
tx.id.should.equal(txid);
//
var signatures = helpers.clientSign(tx, someXPrivKey[0], wallet.n);
console.log('[integration.js.992:signatures:]',signatures); //TODO
server.signTx({
walletId: '123',
copayerId: '1',
txProposalId: txid,
signatures: signatures,
}, function(err) {
should.not.exist(err);
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();
});
});
});
});
});

108
test/txproposal.js Normal file
View File

@ -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": []
}
};