diff --git a/js/models/core/PublicKeyRing.js b/js/models/core/PublicKeyRing.js index c89c2f0ac..09a38d903 100644 --- a/js/models/core/PublicKeyRing.js +++ b/js/models/core/PublicKeyRing.js @@ -297,9 +297,10 @@ PublicKeyRing.prototype.forPaths = function(paths) { }; +// returns pubkey -> copayerId. PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) { - var inKeyMap = {}, ret = []; + var inKeyMap = {}, ret = {}; for(var i in pubkeys ){ inKeyMap[pubkeys[i]] = 1; }; @@ -309,7 +310,7 @@ PublicKeyRing.prototype.copayersForPubkeys = function(pubkeys, paths) { for(var copayerIndex in keys[i] ){ var kHex = keys[i][copayerIndex].toString('hex'); if (inKeyMap[kHex]) { - ret.push(this.copayerIds[copayerIndex]); + ret[kHex] =this.copayerIds[copayerIndex]; delete inKeyMap[kHex]; } } diff --git a/js/models/core/TxProposal.js b/js/models/core/TxProposal.js index da5181d0b..38fd61a43 100644 --- a/js/models/core/TxProposal.js +++ b/js/models/core/TxProposal.js @@ -11,16 +11,16 @@ var buffertools = bitcore.buffertools; var preconditions = require('preconditions').instance(); var VERSION = 1; -var CORE_FIELDS = ['builderObj','inputChainPaths', 'version']; +var CORE_FIELDS = ['builderObj', 'inputChainPaths', 'version']; function TxProposal(opts) { preconditions.checkArgument(opts); - preconditions.checkArgument(opts.inputChainPaths,'no inputChainPaths'); - preconditions.checkArgument(opts.creator,'no creator'); - preconditions.checkArgument(opts.createdTs,'no createdTs'); - preconditions.checkArgument(opts.builder,'no builder'); - preconditions.checkArgument(opts.inputChainPaths,'no inputChainPaths'); + preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths'); + preconditions.checkArgument(opts.creator, 'no creator'); + preconditions.checkArgument(opts.createdTs, 'no createdTs'); + preconditions.checkArgument(opts.builder, 'no builder'); + preconditions.checkArgument(opts.inputChainPaths, 'no inputChainPaths'); this.inputChainPaths = opts.inputChainPaths; this.version = opts.version; @@ -38,11 +38,63 @@ function TxProposal(opts) { this.sentTxid = opts.sentTxid || null; this.comment = opts.comment || null; this.readonly = opts.readonly || null; - - this.sync(); + this._sync(); } + +TxProposal.prototype._check = function() { + + if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { + throw new Error('Invalid tx proposal'); + } + + var tx = this.builder.build(); + if (!tx.ins.length) + throw new Error('Invalid tx proposal: no ins'); + + for (var i in tx.ins) { + var scriptSig = tx.ins[i].s; + if (!scriptSig || !scriptSig.length) { + throw new Error('Invalid tx proposal: no signatures'); + } + } + + for (var i = 0; i < tx.ins.length; i++) { + var hashType = tx.getHashType(i); + if (hashType && hashType !== Transaction.SIGHASH_ALL) + throw new Error('Invalid tx proposal: bad signatures'); + } +}; + + +TxProposal.prototype._updateSignedBy = function() { + this._inputSignatures = []; + + var tx = this.builder.build(); + for (var i in tx.ins) { + var scriptSig = new Script(tx.ins[i].s); + var signatureCount = scriptSig.countSignatures(); + var info = TxProposal._infoFromRedeemScript(scriptSig); + var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL); + var signatureIndexes = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash); + if (signatureIndexes.length !== signatureCount) + throw new Error('Invalid signature'); + this._inputSignatures[i] = signatureIndexes.map(function(i) { + var r = info.keys[i].toString('hex'); + return r; + }); + }; +}; + +TxProposal.prototype._sync = function() { + this._check(); + this._updateSignedBy(); + return this; +} + + TxProposal.prototype.getId = function() { + preconditions.checkState(this.builder); return this.builder.build().getNormalizedHash().toString('hex'); }; @@ -54,23 +106,15 @@ TxProposal.prototype.toObj = function() { }; -TxProposal.prototype.toObjForNetwork = function() { - var o = this.toObj; - - var newOutput = {}; - CORE_FIELDS.forEach(function(k){ - newOutput[k] = o[k]; +TxProposal.trim = function() { + var o = this.toObj(); + var ret = {}; + CORE_FIELDS.forEach(function(k) { + ret[k] = o[k]; }); - return newOutput; + return ret; }; -TxProposal.prototype.sync = function() { - this._check(); - this._updateSignedBy(); - return this; -} - - // fromObj => from a trusted source TxProposal.fromObj = function(o, forceOpts) { preconditions.checkArgument(o.builderObj); @@ -93,17 +137,6 @@ TxProposal.fromObj = function(o, forceOpts) { return new TxProposal(o); }; -TxProposal.fromObjUntrusted = function(o, forceOpts, senderId) { - var newInput = {}; - CORE_FIELDS.forEach(function(k){ - newInput[k] = o[k]; - }); - if (newInput.version !== VERSION) - throw new Error('Peer using different version'); - - return TxProposal.fromObj(newInput, forceOpts, senderId); -}; - TxProposal._formatKeys = function(keys) { @@ -158,49 +191,6 @@ TxProposal._infoFromRedeemScript = function(s) { }; }; -TxProposal.prototype._updateSignedBy = function() { - this._inputSignatures = []; - - var tx = this.builder.build(); - for (var i in tx.ins) { - var scriptSig = new Script(tx.ins[i].s); - var signatureCount = scriptSig.countSignatures(); - var info = TxProposal._infoFromRedeemScript(scriptSig); - var txSigHash = tx.hashForSignature(info.script, parseInt(i), Transaction.SIGHASH_ALL); - var signatureIndexes = TxProposal._verifySignatures(info.keys, scriptSig, txSigHash); - if (signatureIndexes.length !== signatureCount) - throw new Error('Invalid signature'); - this._inputSignatures[i] = signatureIndexes.map(function(i) { - var r = info.keys[i].toString('hex'); - return r; - }); - }; -}; - -TxProposal.prototype._check = function() { - - if (this.builder.signhash && this.builder.signhash !== Transaction.SIGHASH_ALL) { - throw new Error('Invalid tx proposal'); - } - - var tx = this.builder.build(); - if (!tx.ins.length) - throw new Error('Invalid tx proposal: no ins'); - - for(var i in tx.ins){ - var scriptSig = tx.ins[i].s; - if (!scriptSig || !scriptSig.length) { - throw new Error('Invalid tx proposal: no signatures'); - } - } - - for (var i = 0; i < tx.ins.length; i++) { - var hashType = tx.getHashType(i); - if (hashType && hashType !== Transaction.SIGHASH_ALL) - throw new Error('Invalid tx proposal: bad signatures'); - } -}; - TxProposal.prototype.mergeBuilder = function(incoming) { var b0 = this.builder; var b1 = incoming.builder; @@ -213,12 +203,12 @@ TxProposal.prototype.mergeBuilder = function(incoming) { TxProposal.prototype.setSeen = function(copayerId) { - if (!this.seenBy[copayerId]) + if (!this.seenBy[copayerId]) this.seenBy[copayerId] = Date.now(); }; TxProposal.prototype.setRejected = function(copayerId) { - if (!this.rejectedBy[copayerId] && !this.signedBy) + if (!this.rejectedBy[copayerId] && !this.signedBy) this.rejectedBy[copayerId] = Date.now(); }; @@ -227,55 +217,78 @@ TxProposal.prototype.setSent = function(sentTxid) { this.sentTs = Date.now(); }; -/* OTDO - events.push({ -type: 'seen', -cId: k, -txId: ntxid -}); -events.push({ -type: 'signed', -cId: k, -txId: ntxid -}); -events.push({ -type: 'rejected', -cId: k, -txId: ntxid -}); -ret.events = this.mergeMetadata(incoming); -*/ TxProposal.prototype._allSignatures = function() { var ret = {}; - for(var i in this._inputSignatures) + for (var i in this._inputSignatures) for (var j in this._inputSignatures[i]) ret[this._inputSignatures[i][j]] = true; return ret; }; + +TxProposal.prototype.setCopayers = function(senderId, keyMap, readOnlyPeers) { + var newCopayers = {}, + oldCopayers = {}, newSignedBy = {}, readOnlyPeers = {}, isNew = 1; + + for(var k in this.signedBy) { + oldCopayers[k] = 1; + isNew = 0; + }; + + if (isNew == 0 && (!this.creator || !this.createdTs)) + throw new Error('Existing TX has no creator'); + + if (isNew == 0 && (!this.signedBy[this.creator])) + throw new Error('Existing TX is not signed by creator'); + + var iSig = this._inputSignatures[0]; + for(var i in iSig){ + var copayerId = keyMap[iSig[i]]; + if (!copayerId) + throw new Error('Found unknown signature') + + if (oldCopayers[copayerId]) { + //Already have it. Do nothing + } else { + newCopayers[copayerId] = Date.now(); + delete oldCopayers[i]; + } + } + + if (!newCopayers[senderId] && !readOnlyPeers[senderId]) + throw new Error('TX must have a (new) senders signature') + + if (isNew && Object.keys(newCopayers).length>1) + throw new Error('New TX must have only 1 signature'); + + // Handler creator / createdTs. + // from senderId, and must be signed by senderId + if (isNew) { + this.creator = Object.keys(newCopayers)[0]; + this.createdTs = Date.now(); + } + + //Ended. Update this. + for(var i in newCopayers) { + this.signedBy[i] = newCopayers[i]; + } + + // signedBy has preference over rejectedBy + for(var i in this.signedBy) { + delete this.rejectedBy[i]; + } + + return Object.keys(newCopayers); +}; + // merge will not merge any metadata. TxProposal.prototype.merge = function(incoming) { - var ret = {}; - var newSignatures = []; - incoming.sync(); - - var prevInputSignatures = this._allSignatures(); - - ret.hasChanged = this.mergeBuilder(incoming); - this._updateSignedBy(); - - if (ret.hasChanged) - for(var i in this._inputSignatures) - for (var j in this._inputSignatures[i]) - if (!prevInputSignatures[this._inputSignatures[i][j]]) - newSignatures.push(this._inputSignatures[i][j]); - - ret.newSignatures = newSignatures; - - return ret; + var hasChanged = this.mergeBuilder(incoming); + this._sync(); + return hasChanged; }; //This should be on bitcore / Transaction diff --git a/js/models/core/TxProposals.js b/js/models/core/TxProposals.js index 71cfb443b..e12c0b9db 100644 --- a/js/models/core/TxProposals.js +++ b/js/models/core/TxProposals.js @@ -56,21 +56,30 @@ TxProposals.prototype.toObj = function() { }; -TxProposals.prototype.merge = function(inTxp, allowedPubKeys) { - var myTxps = this.txps; +TxProposals.prototype.merge = function(inObj, senderId, copayersForPubkeys, builderOpts) { + var safeObj = inObj.trimUntrustedObj(); + var incomingTx = TxProposal.fromObj(safeObj, builderOpts); + incomingTx._sync(); + var myTxps = this.txps; var ntxid = inTxp.getId(); - var ret = {}; + var ret = { + ntxid: ntxid + }; if (myTxps[ntxid]) { - var v0 = myTxps[ntxid]; - var v1 = inTxp; - ret = v0.merge(v1, allowedPubKeys); - } - else { - this.txps[ntxid] = inTxp; + + // Merge an existing txProposal + ret.hasChanged = myTxps[ntxid].merge(inTxp, allowedPubKeys); + + + } else { + // Create a new one ret.new = 1; + this.txps[ntxid] = inTxp; } + + ret.txp = this.txps[ntxid]; return ret; }; diff --git a/js/models/core/Wallet.js b/js/models/core/Wallet.js index 3f7583643..8646d5924 100644 --- a/js/models/core/Wallet.js +++ b/js/models/core/Wallet.js @@ -59,7 +59,7 @@ function Wallet(opts) { } -Wallet.builderOpts = { +Wallet.builderOpts = { lockTime: null, signhash: bitcore.Transaction.SIGNHASH_ALL, fee: null, @@ -132,72 +132,119 @@ Wallet.prototype._handlePublicKeyRing = function(senderId, data, isInbound) { Wallet.prototype._processProposalEvents = function(mergeInfo) { var ev = []; - if (mergeInfo.new) { - ev = { - type: 'new', - cid: senderId + if (mergeInfo) { + if (mergeInfo.new) { + ev = { + type: 'new', + cid: senderId + } + } else { + for (var i in mergeInfo.newCopayers) { + var copayerId = mergeInfo.newCopayers[i]; + ev.push({ + type: 'signed', + cid: copayerId + }); + } } } else { - for (var i in mergeInfo.newCopayers) { - var copayerId = mergeInfo.newCopayers[i]; - ev.push({ - type: 'signed', - cid: copayerId - }); - } + ev = { + type: 'corrupt', + cId: senderId, + error: e, + }; + } + if (ev) + this.emit('txProposalEvent', ev); +}; + + + +/* OTDO + events.push({ +type: 'signed', +cId: k, +txId: ntxid +}); +*/ +Wallet.prototype._getKeyMap = function(tpx, senderId) { + + this.publicKeyRing.copayersForPubkeys(txp._inputSignatures[0], txp.paths); + + var keyMapStr = JSON.stringify(keyMap); + // All inputs must be signed with the same copayers + for (var i in m.txp._inputSignatures) { + if (!i) continue; + var inputKeyMapStr = JSON.stringify( + this.publicKeyRing.copayersForPubkeys(txp._inputSignatures[i], txp.paths)); + + if (inputKeyMapStr !== keyMapStr) + throw new Error('found inputs with different signatures in Tx from:' + senderId); } - if (ev) - this.emit('txProposalEvent',ev); }; Wallet.prototype._handleTxProposal = function(senderId, data) { this.log('RECV TXPROPOSAL: ', data); - var mergeInfo, ntxid; + var m; try { - mergeInfo = this.txProposals.mergeFromObj(data.txProposal, senderId, Wallet.builderOpts); - mergeInfo.newCopayers=[]; - for (var i in mergeInfo.newSignatures) { - var k = mergeInfo.newSignatures[i]; - mergeInfo.newCopayers.push(this.getCopayerIdFromPubKey(k)); - }; - ntxid = mergeInfo.inTxp.getId(); + m = this.txProposals.mergeObj(senderId, data.txProposal, Wallet.builderOpts); + + var keyMap = this._getKeyMap(m.tpx,senderId); + ret.newCopayers = m.txp.setCopayers(senderId, keyMap); + } catch (e) { - var corruptEvent = { - type: 'corrupt', - cId: senderId, - error: e, - }; - this.emit('txProposalEvent', corruptEvent); - return; + this.log('Corrupt TX proposal received', senderId, e); //TODO } - this.sendSeen(ntxid); - if (mergeInfo.hasChanged) - this.sendTxProposal(ntxid); + if (m) { + this.emit('txProposalsUpdated'); + this.store(); - this.emit('txProposalsUpdated'); - this.store(); - this._processProposalEvents(senderId, mergeInfo); + this.sendSeen(m.ntxid); + + if (m.hasChanged) + this.sendTxProposal(m.ntxid); + } + + this._processProposalEvents(senderId, m); }; Wallet.prototype._handleReject = function(senderId, data, isInbound) { this.log('RECV REJECT:', data); - // TODO check that has not signed. - // - this.txProposals.txps[data.ntxid].setRejected(senderId); - this.emit('txProposalsUpdated'); + + var txp = this.txProposals.txps[data.ntxid]; + + if (!txp) + throw new Error('Received Reject for an unkwown TX from:' + senderId); + + if (txp.signedBy[senderId]) + throw new Error('Received Reject for an already signed TX from:' + senderId); + + txp.setRejected(senderId); this.store(); + this.emit('txProposalsUpdated'); + this.emit('txProposalEvent', { + type: 'rejected', + cId: senderId, + txId: data.ntxid, + }); }; Wallet.prototype._handleSeen = function(senderId, data, isInbound) { this.log('RECV SEEN:', data); this.txProposals.txps[data.ntxid].setSeen(senderId); - this.emit('txProposalsUpdated'); this.store(); + this.emit('txProposalsUpdated'); + this.emit('txProposalEvent', { + type: 'seen', + cId: senderId, + txId: data.ntxid, + }); + }; @@ -245,8 +292,10 @@ Wallet.prototype._handleData = function(senderId, data, isInbound) { break; case 'reject': this._handleReject(senderId, data, isInbound); + break; case 'seen': - this._handleReject(senderId, data, isInbound); + this._handleSeen(senderId, data, isInbound); + break; case 'txProposal': this._handleTxProposal(senderId, data, isInbound); break; @@ -796,7 +845,7 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos }; } - for (var k in Wallet.builderOpts){ + for (var k in Wallet.builderOpts) { opts[k] = Wallet.builderOpts[k]; } @@ -821,8 +870,8 @@ Wallet.prototype.createTxSync = function(toAddress, amountSatStr, comment, utxos var tx = b.build(); - if (!tx.countInputSignatures(0)) - throw new Error ('Could not sign generated tx'); + if (!tx.countInputSignatures(0)) + throw new Error('Could not sign generated tx'); var me = {}; me[myId] = now; diff --git a/test/test.TxProposal.js b/test/test.TxProposal.js index 79435a318..33c671519 100644 --- a/test/test.TxProposal.js +++ b/test/test.TxProposal.js @@ -161,28 +161,30 @@ describe('TxProposal', function() { }).should.throw('script'); }); it('#_verifyScriptSig, no signatures', function() { - var ret = TxProposal._verifySignatures( keyBuf, validScriptSig, new Buffer(32)); + var ret = TxProposal._verifySignatures(keyBuf, validScriptSig, new Buffer(32)); ret.length.should.equal(0); }); it('#_verifyScriptSig, two signatures', function() { // Data taken from bitcore's TransactionBuilder test var txp = dummyProposal; var tx = dummyProposal.builder.build(); - var ret = TxProposal._verifySignatures(pubkeys,validScriptSig, tx.hashForSignature()); + var ret = TxProposal._verifySignatures(pubkeys, validScriptSig, tx.hashForSignature()); ret.should.deep.equal([0, 3]); }); it('#_infoFromRedeemScript', function() { var info = TxProposal._infoFromRedeemScript(validScriptSig); var keys = info.keys; keys.length.should.equal(5); - for(var i in keys){ + for (var i in keys) { keys[i].toString('hex').should.equal(pubkeys[i].toString('hex')); } Buffer.isBuffer(info.script.getBuffer()).should.equal(true); }); it('#_updateSignedBy', function() { var txp = dummyProposal; - txp._inputSignatures.should.deep.equal([[ '03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3' ]]); + txp._inputSignatures.should.deep.equal([ + ['03197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d', '03a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e3'] + ]); }); describe('#_check', function() { var txp = dummyProposal; @@ -193,28 +195,38 @@ describe('TxProposal', function() { }); it('FAIL ins', function() { txp.builder.tx.ins = []; - (function() { txp._check();} ).should.throw('no ins'); + (function() { + txp._check(); + }).should.throw('no ins'); txp.builder.tx.ins = backup; }); it('FAIL signhash SINGLE', function() { - sinon.stub(txp.builder.tx,'getHashType').returns(Transaction.SIGHASH_SINGLE); - (function() { txp._check();} ).should.throw('signatures'); + sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_SINGLE); + (function() { + txp._check(); + }).should.throw('signatures'); txp.builder.tx.getHashType.restore(); }); it('FAIL signhash NONE', function() { - sinon.stub(txp.builder.tx,'getHashType').returns(Transaction.SIGHASH_NONE); - (function() { txp._check();} ).should.throw('signatures'); + sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_NONE); + (function() { + txp._check(); + }).should.throw('signatures'); txp.builder.tx.getHashType.restore(); }); it('FAIL signhash ANYONECANPAY', function() { - sinon.stub(txp.builder.tx,'getHashType').returns(Transaction.SIGHASH_ANYONECANPAY); - (function() { txp._check();} ).should.throw('signatures'); + sinon.stub(txp.builder.tx, 'getHashType').returns(Transaction.SIGHASH_ANYONECANPAY); + (function() { + txp._check(); + }).should.throw('signatures'); txp.builder.tx.getHashType.restore(); }); it('FAIL no signatures', function() { var backup = txp.builder.tx.ins[0].s; txp.builder.tx.ins[0].s = undefined; - (function() { txp._check();} ).should.throw('no signatures'); + (function() { + txp._check(); + }).should.throw('no signatures'); txp.builder.tx.ins[0].s = backup; }); }); @@ -222,39 +234,179 @@ describe('TxProposal', function() { var txp = dummyProposal; var backup = txp.builder.tx.ins; it('with self', function() { - var ret = txp.merge(txp); - ret.newSignatures.length.should.equal(0); - ret.hasChanged.should.equal(false); + var hasChanged = txp.merge(txp); + hasChanged.should.equal(false); }); it('with less signatures', function() { var backup = txp.builder.vanilla.scriptSig[0]; txp.builder.merge = function() { // 3 signatures. - this.vanilla.scriptSig=['0048304502207d8e832bd576c93300e53ab6cbd68641961bec60690c358fd42d8e42b7d7d687022100a1daa89923efdb4c9b615d065058d9e1644f67000694a7d0806759afa7bef19b014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; - this.tx.ins[0].s=new Buffer(this.vanilla.scriptSig[0],'hex'); + this.vanilla.scriptSig = ['0048304502207d8e832bd576c93300e53ab6cbd68641961bec60690c358fd42d8e42b7d7d687022100a1daa89923efdb4c9b615d065058d9e1644f67000694a7d0806759afa7bef19b014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; + this.tx.ins[0].s = new Buffer(this.vanilla.scriptSig[0], 'hex'); }; - var ret = txp.merge(txp); - ret.hasChanged.should.equal(true); - ret.newSignatures.length.should.equal(0); + var hasChanged = txp.merge(txp); + hasChanged.should.equal(true); txp.builder.vanilla.scriptSig = [backup]; - txp.builder.tx.ins[0].s = new Buffer(backup,'hex'); + txp.builder.tx.ins[0].s = new Buffer(backup, 'hex'); }); it('with more signatures', function() { txp.builder.merge = function() { // 3 signatures. - this.vanilla.scriptSig=['00483045022100f75bd3eb92d8c9be9a94d848bbd1985fc0eaf4c47fb470a0b222881802a1f03802204eb239ae3082779b1ec4f2e69baa0362494071e707e1696c14ad23c8f2e184e20148304502201981482db0f369ce943293b6fec06a0347918663c766a79d4cbd0457801768d1022100aedf8d7c51d55a9ddbdcc0067ed6b648b77ce9660447bbcf4e2c209698efa0a30148304502203f0ddad47757f8705cb40e7c706590d2e2028a7027ffdb26dd208fd6155e0d28022100ccd206f9b969ab7f88ee4c5c6cee48c800a62dda024c5a8de7eb8612b833a0c0014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; - this.tx.ins[0].s=new Buffer(this.vanilla.scriptSig[0],'hex'); + this.vanilla.scriptSig = ['00483045022100f75bd3eb92d8c9be9a94d848bbd1985fc0eaf4c47fb470a0b222881802a1f03802204eb239ae3082779b1ec4f2e69baa0362494071e707e1696c14ad23c8f2e184e20148304502201981482db0f369ce943293b6fec06a0347918663c766a79d4cbd0457801768d1022100aedf8d7c51d55a9ddbdcc0067ed6b648b77ce9660447bbcf4e2c209698efa0a30148304502203f0ddad47757f8705cb40e7c706590d2e2028a7027ffdb26dd208fd6155e0d28022100ccd206f9b969ab7f88ee4c5c6cee48c800a62dda024c5a8de7eb8612b833a0c0014cad532103197599f6e209cefef07da2fddc6fe47715a70162c531ffff8e611cef23dfb70d210380a29968851f93af55e581c43d9ef9294577a439a3ca9fc2bc47d1ca2b3e9127210392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed032103a94351fecc4328bb683bf93a1aa67378374904eac5980c7966723a51897c56e32103e085eb6fa1f20b2722c16161144314070a2c316a9cae2489fd52ce5f63fff6e455ae']; + this.tx.ins[0].s = new Buffer(this.vanilla.scriptSig[0], 'hex'); }; - var ret = txp.merge(txp); - ret.hasChanged.should.equal(true); - ret.newSignatures.length.should.equal(1); - ret.newSignatures[0].should.equal('0392dccb2ed470a45984811d6402fdca613c175f8f3e4eb8e2306e8ccd7d0aed03'); + var hasChanged = txp.merge(txp); + hasChanged.should.equal(true); }); }); + describe('#setCopayers', function() { + it("should fails if Tx has no creator", function() { + var txp = dummyProposal; + txp.signedBy = { + 'hugo': 1 + }; + delete txp['creator']; + (function() { + txp.setCopayers('juan', { + pk1: 'pepe' + }) + }).should.throw('no creator'); + }); + it("should fails if Tx is not signed by creator", function() { + var txp = dummyProposal; + txp.creator = 'creator'; + txp.signedBy = { + 'hugo': 1 + }; + txp._inputSignatures = [ + ['pkX'] + ]; + (function() { + txp.setCopayers('juan', { + pk1: 'pepe' + }) + }).should.throw('creator'); + }); + + + it("should fails if Tx has unmapped signatures", function() { + var txp = dummyProposal; + txp.creator = 'creator'; + txp.signedBy = { + creator: 1 + }; + txp._inputSignatures = [ + ['pk0', 'pkX'] + ]; + (function() { + txp.setCopayers('juan', { + pk1: 'pepe' + }) + }).should.throw('unknown sig'); + }); + + it("should be signed by sender", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp._inputSignatures = [ + ['pk1', 'pk0'] + ]; + txp.signedBy = { + 'creator': Date.now() + }; + (function() { + txp.setCopayers('juan', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }) + }).should.throw('senders sig'); + }); + + + it("should set signedBy (trivial case)", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp._inputSignatures = [ + ['pk1', 'pk0'] + ]; + txp.signedBy = { + 'creator': Date.now() + }; + txp.setCopayers('pepe', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }) + Object.keys(txp.signedBy).length.should.equal(2); + txp.signedBy['pepe'].should.gte(ts); + txp.signedBy['creator'].should.gte(ts); + }); + it("should assign creator", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp._inputSignatures = [ + ['pk0'] + ]; + txp.signedBy = {}; + delete txp['creator']; + delete txp['creatorTs']; + txp.setCopayers('creator', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }) + Object.keys(txp.signedBy).length.should.equal(1); + txp.creator.should.equal('creator'); + txp.createdTs.should.gte(ts); + }) + it("New tx should have only 1 signature", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp.signedBy = {}; + delete txp['creator']; + delete txp['creatorTs']; + txp._inputSignatures = [ + ['pk0', 'pk1'] + ]; + (function() { + txp.setCopayers( + 'creator', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }, { + 'creator2': 1 + } + ); + }).should.throw('only 1'); + }) + + it("if signed, should not change ts", function() { + var txp = dummyProposal; + var ts = Date.now(); + txp._inputSignatures = [ + ['pk0', 'pk1'] + ]; + txp.creator = 'creator'; + txp.signedBy = { + 'creator': 1 + }; + txp.setCopayers('pepe', { + pk0: 'creator', + pk1: 'pepe', + pk2: 'john' + }) + Object.keys(txp.signedBy).length.should.equal(2); + txp.creator.should.equal('creator'); + txp.signedBy['creator'].should.equal(1); + txp.signedBy['pepe'].should.gte(ts); + }) + }); }); });