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 });
|
||||
};
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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';
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -472,6 +472,11 @@ CopayServer.prototype.signTx = function(opts, cb) {
|
|||
|
||||
Utils.checkRequired(opts, ['walletId', 'copayerId', 'txProposalId', 'signatures']);
|
||||
|
||||
self.getWallet({
|
||||
id: opts.walletId
|
||||
}, function(err, wallet) {
|
||||
if (err) return cb(err);
|
||||
|
||||
self.getTx({
|
||||
walletId: opts.walletId,
|
||||
id: opts.txProposalId
|
||||
|
@ -481,8 +486,15 @@ CopayServer.prototype.signTx = function(opts, cb) {
|
|||
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 (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'));
|
||||
|
||||
var copayer = wallet.getCopayer(opts.copayerId);
|
||||
|
||||
if (!txp.checkSignatures(opts.signatures, copayer.xPubKey))
|
||||
return cb(new ClientError('BADSIGNATURES', 'Bad signatures'));
|
||||
|
||||
txp.sign(opts.copayerId, opts.signatures);
|
||||
|
||||
|
@ -505,6 +517,7 @@ CopayServer.prototype.signTx = function(opts, cb) {
|
|||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
//
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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