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,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

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