From caafaf25f62a782946fb8a7ce05735661deb490d Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 12 Jun 2015 16:05:33 -0300 Subject: [PATCH 1/4] add a reject / creation backoff time --- lib/server.js | 144 +++++++++++++++++++++++------------- lib/storage.js | 23 ++++++ test/integration/server.js | 147 ++++++++++++++++++++++++++++++++++++- 3 files changed, 260 insertions(+), 54 deletions(-) diff --git a/lib/server.js b/lib/server.js index a1355e5..74d8c4f 100644 --- a/lib/server.js +++ b/lib/server.js @@ -49,6 +49,19 @@ function WalletService() { this.notifyTicker = 0; }; + +// Time after which a Tx proposal can be erased by any copayer. in seconds +WalletService.deleteLockTime = 24 * 3600; + +// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in seconds. +WalletService.backoffTime = 2 * 60; + +// Fund scanning parameters +WalletService.scanConfig = { + SCAN_WINDOW: 20, + DERIVATION_DELAY: 10, // in milliseconds +}; + /** * Initializes global settings for all instances. * @param {Object} opts @@ -731,6 +744,33 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { }); }; + +WalletService.prototype._canCreateTx = function(copayerId, cb) { + var self = this; + self.storage.fetchLastTxs(self.walletId, copayerId, 5, function(err, txs) { + if (err) return cb(err); + + if (!txs.length) + return cb(null, true); + + var lastRejections = _.takeWhile(txs, {status: 'rejected'}); + + if (!lastRejections.length) + return cb(null, true); + + var lastTxTs = txs[0].createdOn; + var now = Math.floor(Date.now() / 1000); + var timeSinceLastRejection = now - lastTxTs; + var backoffTime = WalletService.backoffTime * lastRejections.length; + + if (timeSinceLastRejection <= backoffTime) + log.debug('Not allowing to create TX: timeSinceLastRejection/backoffTime', timeSinceLastRejection, backoffTime); + + return cb(null, timeSinceLastRejection > backoffTime); + }); +}; + + /** * Creates a new transaction proposal. * @param {Object} opts @@ -750,59 +790,66 @@ WalletService.prototype.createTx = function(opts, cb) { self._runLocked(cb, function(cb) { self.getWallet({}, function(err, wallet) { if (err) return cb(err); - if (!wallet.isComplete()) return cb(new ClientError('Wallet is not complete')); + if (!wallet.isComplete()) + return cb(new ClientError('Wallet is not complete')); - var copayer = wallet.getCopayer(self.copayerId); - var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); - if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey)) - return cb(new ClientError('Invalid proposal signature')); - - var toAddress; - try { - toAddress = new Bitcore.Address(opts.toAddress); - } catch (ex) { - return cb(new ClientError('INVALIDADDRESS', 'Invalid address')); - } - if (toAddress.network != wallet.getNetworkName()) - return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); - - if (opts.amount <= 0) - return cb(new ClientError('Invalid amount')); - - if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) - return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); - - - var changeAddress = wallet.createAddress(true); - - var txp = Model.TxProposal.create({ - walletId: self.walletId, - creatorId: self.copayerId, - toAddress: opts.toAddress, - amount: opts.amount, - message: opts.message, - proposalSignature: opts.proposalSignature, - payProUrl: opts.payProUrl, - changeAddress: changeAddress, - requiredSignatures: wallet.m, - requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), - }); - - self._selectTxInputs(txp, function(err) { + self._canCreateTx(self.copayerId, function(err, canCreate) { if (err) return cb(err); + if (!canCreate) + return cb(new ClientError('NOTALLOWEDTOCREATETX', 'Cannot create TX proposal during backoff time')); + + var copayer = wallet.getCopayer(self.copayerId); + var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); + if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey)) + return cb(new ClientError('Invalid proposal signature')); - $.checkState(txp.inputs); + var toAddress; + try { + toAddress = new Bitcore.Address(opts.toAddress); + } catch (ex) { + return cb(new ClientError('INVALIDADDRESS', 'Invalid address')); + } + if (toAddress.network != wallet.getNetworkName()) + return cb(new ClientError('INVALIDADDRESS', 'Incorrect address network')); - self.storage.storeAddressAndWallet(wallet, changeAddress, function(err) { + if (opts.amount <= 0) + return cb(new ClientError('Invalid amount')); + + if (opts.amount < Bitcore.Transaction.DUST_AMOUNT) + return cb(new ClientError('DUSTAMOUNT', 'Amount below dust threshold')); + + + var changeAddress = wallet.createAddress(true); + + var txp = Model.TxProposal.create({ + walletId: self.walletId, + creatorId: self.copayerId, + toAddress: opts.toAddress, + amount: opts.amount, + message: opts.message, + proposalSignature: opts.proposalSignature, + payProUrl: opts.payProUrl, + changeAddress: changeAddress, + requiredSignatures: wallet.m, + requiredRejections: Math.min(wallet.m, wallet.n - wallet.m + 1), + }); + + self._selectTxInputs(txp, function(err) { if (err) return cb(err); - self.storage.storeTx(wallet.id, txp, function(err) { + $.checkState(txp.inputs); + + self.storage.storeAddressAndWallet(wallet, changeAddress, function(err) { if (err) return cb(err); - self._notify('NewTxProposal', { - amount: opts.amount - }, function() { - return cb(null, txp); + self.storage.storeTx(wallet.id, txp, function(err) { + if (err) return cb(err); + + self._notify('NewTxProposal', { + amount: opts.amount + }, function() { + return cb(null, txp); + }); }); }); }); @@ -1339,13 +1386,6 @@ WalletService.prototype.getTxHistory = function(opts, cb) { }); }; -// in seconds -WalletService.deleteLockTime = 24 * 3600; - -WalletService.scanConfig = { - SCAN_WINDOW: 20, - DERIVATION_DELAY: 10, // in milliseconds -}; /** * Scan the blockchain looking for addresses having some activity diff --git a/lib/storage.js b/lib/storage.js index 19eee01..0fc680b 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -171,6 +171,29 @@ Storage.prototype.fetchTx = function(walletId, txProposalId, cb) { }; + +Storage.prototype.fetchLastTxs = function(walletId, creatorId, limit, cb) { + var self = this; + + this.db.collection(collections.TXS).find({ + walletId: walletId, + creatorId: creatorId, + }, { + limit: limit || 5 + }).sort({ + createdOn: -1 + }).toArray(function(err, result) { + if (err) return cb(err); + if (!result) return cb(); + var txs = _.map(result, function(tx) { + return Model.TxProposal.fromObj(tx); + }); + return cb(null, txs); + }); +}; + + + Storage.prototype.fetchPendingTxs = function(walletId, cb) { var self = this; diff --git a/test/integration/server.js b/test/integration/server.js index 1da0b45..68808b0 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1674,6 +1674,149 @@ describe('Wallet service', function() { }); }); + describe('#createTx backoff time', function() { + var server, wallet, txid; + + beforeEach(function(done) { + helpers.createAndJoinWallet(2, 2, function(s, w) { + server = s; + wallet = w; + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + + should.not.exist(err); + should.exist(tx); + txid = tx.id; + done(); + }); + }); + }); + }); + + it('should fail to create inmediatly after a rejection', function(done) { + async.series([ + + function(next) { + server.getPendingTxs({}, function(err, txs) { + var tx = txs[0]; + tx.id.should.equal(txid); + next(); + }); + }, + function(next) { + server.rejectTx({ + txProposalId: txid, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + err.code.should.equal('NOTALLOWEDTOCREATETX'); + next(); + }); + }); + } + ], done); + }); + + it('should allow to create after backoffTime', function(done) { + async.series([ + + function(next) { + server.getPendingTxs({}, function(err, txs) { + var tx = txs[0]; + tx.id.should.equal(txid); + next(); + }); + }, + function(next) { + server.rejectTx({ + txProposalId: txid, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + next(); + }); + }, + function(next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + err.code.should.equal('NOTALLOWEDTOCREATETX'); + next(); + }); + }); + }, + function(next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + + var clock = sinon.useFakeTimers(Date.now() + 2000 + WalletService.backoffTime * 1000); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + clock.restore(); + next(); + }); + }); + }, + ], done); + }); + it('should not allow to create after backoffTime and 2 rejections', function(done) { + async.series([ + + function(next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + next(); + }); + }); + }, + function(next) { + server.getPendingTxs({}, function(err, tx) { + should.not.exist(err); + server.rejectTx({ + txProposalId: tx[0].id, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + server.rejectTx({ + txProposalId: tx[1].id, + reason: 'some other reason', + }, function(err) { + should.not.exist(err); + + next(); + }); + }); + }); + }, + function(next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + + var clock = sinon.useFakeTimers(Date.now() + 2000 + WalletService.backoffTime * 1000); + server.createTx(txOpts, function(err, tx) { + err.code.should.equal('NOTALLOWEDTOCREATETX'); + clock.restore(); + next(); + }); + }); + }, + ], done); + }); + + + }); + + describe('#signTx', function() { var server, wallet, txid; @@ -2808,7 +2951,7 @@ describe('Wallet service', function() { server.getPendingTxs({}, function(err, txs) { should.not.exist(err); - txs[0].deleteLockTime.should.be.above(WalletService.deleteLockTime-10); + txs[0].deleteLockTime.should.be.above(WalletService.deleteLockTime - 10); var clock = sinon.useFakeTimers(Date.now() + 1 + 24 * 3600 * 1000); server.removePendingTx({ @@ -2833,7 +2976,7 @@ describe('Wallet service', function() { }, function(err) { should.not.exist(err); - var clock = sinon.useFakeTimers(Date.now() + 1 + 24 * 3600 * 1000); + var clock = sinon.useFakeTimers(Date.now() + 2000 + WalletService.deleteLockTime * 1000); server2.removePendingTx({ txProposalId: txp.id }, function(err) { From 895c52badadc37cf61c67b187ee49a63bd33f6e8 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Fri, 12 Jun 2015 16:11:42 -0300 Subject: [PATCH 2/4] exponential backoff --- lib/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index 74d8c4f..4879f32 100644 --- a/lib/server.js +++ b/lib/server.js @@ -761,7 +761,7 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { var lastTxTs = txs[0].createdOn; var now = Math.floor(Date.now() / 1000); var timeSinceLastRejection = now - lastTxTs; - var backoffTime = WalletService.backoffTime * lastRejections.length; + var backoffTime = Math.pow(WalletService.backoffTime,lastRejections.length); if (timeSinceLastRejection <= backoffTime) log.debug('Not allowing to create TX: timeSinceLastRejection/backoffTime', timeSinceLastRejection, backoffTime); From 4569f1d3c56100732750a30e4458c3282eb1a43b Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 13 Jun 2015 12:03:04 -0300 Subject: [PATCH 3/4] backoff time only active after backoffOffset --- lib/server.js | 23 +++-- test/integration/server.js | 203 ++++++++++++++++++++----------------- 2 files changed, 127 insertions(+), 99 deletions(-) diff --git a/lib/server.js b/lib/server.js index 4879f32..60856c1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -53,8 +53,11 @@ function WalletService() { // Time after which a Tx proposal can be erased by any copayer. in seconds WalletService.deleteLockTime = 24 * 3600; -// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in seconds. -WalletService.backoffTime = 2 * 60; +// Allowed consecutive txp rejections before backoff is applied. +WalletService.backoffOffset = 3; + +// Time a copayer need to wait to create a new TX after her tx previous proposal we rejected. (incremental). in Minutes. +WalletService.backoffTimeMinutes = 2; // Fund scanning parameters WalletService.scanConfig = { @@ -750,18 +753,22 @@ WalletService.prototype._canCreateTx = function(copayerId, cb) { self.storage.fetchLastTxs(self.walletId, copayerId, 5, function(err, txs) { if (err) return cb(err); - if (!txs.length) + if (!txs.length) return cb(null, true); - var lastRejections = _.takeWhile(txs, {status: 'rejected'}); + var lastRejections = _.takeWhile(txs, { + status: 'rejected' + }); - if (!lastRejections.length) + var exceededRejections = lastRejections.length - WalletService.backoffOffset; + if (exceededRejections <= 0) return cb(null, true); + var lastTxTs = txs[0].createdOn; var now = Math.floor(Date.now() / 1000); var timeSinceLastRejection = now - lastTxTs; - var backoffTime = Math.pow(WalletService.backoffTime,lastRejections.length); + var backoffTime = 60 * Math.pow(WalletService.backoffTimeMinutes, exceededRejections); if (timeSinceLastRejection <= backoffTime) log.debug('Not allowing to create TX: timeSinceLastRejection/backoffTime', timeSinceLastRejection, backoffTime); @@ -795,9 +802,9 @@ WalletService.prototype.createTx = function(opts, cb) { self._canCreateTx(self.copayerId, function(err, canCreate) { if (err) return cb(err); - if (!canCreate) + if (!canCreate) return cb(new ClientError('NOTALLOWEDTOCREATETX', 'Cannot create TX proposal during backoff time')); - + var copayer = wallet.getCopayer(self.copayerId); var hash = WalletUtils.getProposalHash(opts.toAddress, opts.amount, opts.message, opts.payProUrl); if (!self._verifySignature(hash, opts.proposalSignature, copayer.requestPubKey)) diff --git a/test/integration/server.js b/test/integration/server.js index 68808b0..038f725 100644 --- a/test/integration/server.js +++ b/test/integration/server.js @@ -1675,43 +1675,67 @@ describe('Wallet service', function() { }); describe('#createTx backoff time', function() { - var server, wallet, txid; + var server, wallet; beforeEach(function(done) { helpers.createAndJoinWallet(2, 2, function(s, w) { server = s; wallet = w; - helpers.stubUtxos(server, wallet, _.range(1, 9), function() { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); - server.createTx(txOpts, function(err, tx) { - - should.not.exist(err); - should.exist(tx); - txid = tx.id; - done(); - }); - }); + done(); }); }); - it('should fail to create inmediatly after a rejection', function(done) { + it('should allow to create inmediatly after a 3 rejections', function(done) { async.series([ function(next) { - server.getPendingTxs({}, function(err, txs) { - var tx = txs[0]; - tx.id.should.equal(txid); - next(); - }); + async.each([0, 1, 2], function(i, a_next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + server.rejectTx({ + txProposalId: tx.id, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + a_next(); + }); + }); + }); + }, next); }, function(next) { - server.rejectTx({ - txProposalId: txid, - reason: 'some reason', - }, function(err) { - should.not.exist(err); - next(); + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + next(); + }); }); + } + ], done); + }); + + it('should NOT allow to create inmediatly after a 4 rejections', function(done) { + async.series([ + + function(next) { + async.each([0, 1, 2, 3], function(i, a_next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + server.rejectTx({ + txProposalId: tx.id, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + a_next(); + }); + }); + }); + }, next); }, function(next) { helpers.stubUtxos(server, wallet, _.range(1, 9), function() { @@ -1725,87 +1749,42 @@ describe('Wallet service', function() { ], done); }); - it('should allow to create after backoffTime', function(done) { + it('should allow to create inmediatly after a 4 rejections after backofftime', function(done) { async.series([ function(next) { - server.getPendingTxs({}, function(err, txs) { - var tx = txs[0]; - tx.id.should.equal(txid); - next(); - }); - }, - function(next) { - server.rejectTx({ - txProposalId: txid, - reason: 'some reason', - }, function(err) { - should.not.exist(err); - next(); - }); - }, - function(next) { - helpers.stubUtxos(server, wallet, _.range(1, 9), function() { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); - server.createTx(txOpts, function(err, tx) { - err.code.should.equal('NOTALLOWEDTOCREATETX'); - next(); - }); - }); - }, - function(next) { - helpers.stubUtxos(server, wallet, _.range(1, 9), function() { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); - - var clock = sinon.useFakeTimers(Date.now() + 2000 + WalletService.backoffTime * 1000); - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - clock.restore(); - next(); - }); - }); - }, - ], done); - }); - it('should not allow to create after backoffTime and 2 rejections', function(done) { - async.series([ - - function(next) { - helpers.stubUtxos(server, wallet, _.range(1, 9), function() { - var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); - server.createTx(txOpts, function(err, tx) { - should.not.exist(err); - next(); - }); - }); - }, - function(next) { - server.getPendingTxs({}, function(err, tx) { - should.not.exist(err); - server.rejectTx({ - txProposalId: tx[0].id, - reason: 'some reason', - }, function(err) { - should.not.exist(err); - server.rejectTx({ - txProposalId: tx[1].id, - reason: 'some other reason', - }, function(err) { + async.each([0, 1, 2, 3], function(i, a_next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { should.not.exist(err); - - next(); + server.rejectTx({ + txProposalId: tx.id, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + a_next(); + }); }); }); + }, next); + }, + function(next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + err.code.should.equal('NOTALLOWEDTOCREATETX'); + next(); + }); }); }, function(next) { helpers.stubUtxos(server, wallet, _.range(1, 9), function() { var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); - - var clock = sinon.useFakeTimers(Date.now() + 2000 + WalletService.backoffTime * 1000); + var clock = sinon.useFakeTimers(Date.now() + WalletService.backoffTimeMinutes * 60 * 1000 + 2000); server.createTx(txOpts, function(err, tx) { - err.code.should.equal('NOTALLOWEDTOCREATETX'); clock.restore(); + should.not.exist(err); next(); }); }); @@ -1814,6 +1793,47 @@ describe('Wallet service', function() { }); + it('should NOT allow to create after a 5 rejections after backofftime', function(done) { + async.series([ + + function(next) { + async.each([0, 1, 2, 3, 4], function(i, a_next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + should.not.exist(err); + server.rejectTx({ + txProposalId: tx.id, + reason: 'some reason', + }, function(err) { + should.not.exist(err); + a_next(); + }); + }); + }); + }, next); + }, + function(next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + server.createTx(txOpts, function(err, tx) { + next(); + }); + }); + }, + function(next) { + helpers.stubUtxos(server, wallet, _.range(1, 9), function() { + var txOpts = helpers.createProposalOpts('18PzpUFkFZE8zKWUPvfykkTxmB9oMR8qP7', 10, null, TestData.copayers[0].privKey_1H_0); + var clock = sinon.useFakeTimers(Date.now() + WalletService.backoffTimeMinutes * 60 * 1000 + 2000); + server.createTx(txOpts, function(err, tx) { + clock.restore(); + err.code.should.equal('NOTALLOWEDTOCREATETX'); + next(); + }); + }); + }, + ], done); + }); }); @@ -2437,6 +2457,7 @@ describe('Wallet service', function() { next(); }); }, function(err) { + clock.restore(); return done(err); }); }); From 20e4dd8da0324755ef5cdbe401149d3b9381bf34 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Sat, 13 Jun 2015 12:06:43 -0300 Subject: [PATCH 4/4] update fetched txs --- lib/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index 60856c1..bdad5ba 100644 --- a/lib/server.js +++ b/lib/server.js @@ -750,7 +750,7 @@ WalletService.prototype._selectTxInputs = function(txp, cb) { WalletService.prototype._canCreateTx = function(copayerId, cb) { var self = this; - self.storage.fetchLastTxs(self.walletId, copayerId, 5, function(err, txs) { + self.storage.fetchLastTxs(self.walletId, copayerId, 5 + WalletService.backoffOffset, function(err, txs) { if (err) return cb(err); if (!txs.length)