add a reject / creation backoff time

This commit is contained in:
Matias Alejo Garcia 2015-06-12 16:05:33 -03:00
parent 6f13667912
commit caafaf25f6
3 changed files with 260 additions and 54 deletions

View File

@ -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,7 +790,13 @@ 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'));
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);
@ -809,6 +855,7 @@ WalletService.prototype.createTx = function(opts, cb) {
});
});
});
});
};
/**
@ -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

View File

@ -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;

View File

@ -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) {