From b7073abb0eb2dc60ca407a74db4264b2aae84193 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 7 Jan 2019 16:40:32 +0000 Subject: [PATCH] FIX: better locks (rel #3) --- class/Lock.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++ class/index.js | 1 + controllers/api.js | 39 +++++++++++++++++++++----------------- 3 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 class/Lock.js diff --git a/class/Lock.js b/class/Lock.js new file mode 100644 index 0000000..288cf2f --- /dev/null +++ b/class/Lock.js @@ -0,0 +1,47 @@ +const crypto = require('crypto'); + +export class Lock { + /** + * + * @param {Redis} redis + * @param {String} lock_key + */ + constructor(redis, lock_key) { + this._redis = redis; + this._lock_key = lock_key; + } + + /** + * Tries to obtain lock in single-threaded Redis. + * Returns TRUE if success. + * + * @returns {Promise} + */ + async obtainLock() { + if (await this._redis.get(this._lock_key)) { + // someone already has the lock + return false; + } + + // trying to set the lock: + let buffer = crypto.randomBytes(10); + const randomValue = buffer.toString('hex'); + await this._redis.set(this._lock_key, randomValue); + + // checking if it was set: + let value = await this._redis.get(this._lock_key); + if (value !== randomValue) { + // someone else managed to obtain this lock + return false; + } + + // success - got lock + await this._redis.expire(this._lock_key, 2 * 60); + // lock expires in 2 mins just for any case + return true; + } + + async releaseLock() { + await this._redis.del(this._lock_key); + } +} diff --git a/class/index.js b/class/index.js index f6b9f36..4bb7d4d 100644 --- a/class/index.js +++ b/class/index.js @@ -1 +1,2 @@ export * from './User'; +export * from './Lock'; diff --git a/controllers/api.js b/controllers/api.js index ccdfc49..dff5eca 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -1,4 +1,4 @@ -import { User } from '../class/User'; +import { User, Lock } from '../class/'; const config = require('../config'); let express = require('express'); let router = express.Router(); @@ -113,12 +113,20 @@ router.post('/payinvoice', async function(req, res) { if (!req.body.invoice) return errorBadArguments(res); let freeAmount = false; if (req.body.amount) freeAmount = parseInt(req.body.amount); - const lock_key = 'invoice_paying_for_' + u.getUserId(); + + // obtaining a lock + let lock = new Lock(redis, 'invoice_paying_for_' + u.getUserId()); + if (!(await lock.obtainLock())) { + return errorTryAgainLater(res); + } let userBalance = await u.getBalance(); lightning.decodePayReq({ pay_req: req.body.invoice }, async function(err, info) { - if (err) return errorNotAValidInvoice(res); + if (err) { + await lock.releaseLock(); + return errorNotAValidInvoice(res); + } if (+info.num_satoshis === 0) { // 'tip' invoices @@ -132,13 +140,10 @@ router.post('/payinvoice', async function(req, res) { // this is internal invoice // now, receiver add balance let userid_payee = await u.getUseridByPaymentHash(info.payment_hash); - if (!userid_payee) return errorGeneralServerError(res); - - if (await redis.get(lock_key)) { - return errorTryAgainLater(res); + if (!userid_payee) { + await lock.releaseLock(); + return errorGeneralServerError(res); } - await redis.set(lock_key, 1); - await redis.expire(lock_key, 2 * 60); let UserPayee = new User(redis); UserPayee._userid = userid_payee; // hacky, fixme @@ -159,10 +164,12 @@ router.post('/payinvoice', async function(req, res) { await UserPayee.setPaymentHashPaid(info.payment_hash); - await redis.del(lock_key, 1); + await lock.releaseLock(); return res.send(info); } + // else - regular lightning network payment: + var call = lightning.sendPayment(); call.on('data', function(payment) { // payment callback @@ -172,32 +179,30 @@ router.post('/payinvoice', async function(req, res) { payment.pay_req = req.body.invoice; payment.decoded = info; u.savePaidLndInvoice(payment); - redis.del(lock_key); + lock.releaseLock(); res.send(payment); } else { // payment failed - redis.del(lock_key); + lock.releaseLock(); return errorLnd(res); } }); if (!info.num_satoshis && !info.num_satoshis) { // tip invoice, but someone forgot to specify amount + await lock.releaseLock(); return errorBadArguments(res); } let inv = { payment_request: req.body.invoice, amt: info.num_satoshis }; // amt is used only for 'tip' invoices try { - if (await redis.get(lock_key)) { - return errorTryAgainLater(res); - } - await redis.set(lock_key, 1); - await redis.expire(lock_key, 2 * 60); logger.log('/payinvoice', [req.id, 'before write', JSON.stringify(inv)]); call.write(inv); } catch (Err) { logger.log('/payinvoice', [req.id, 'exception', JSON.stringify(Err)]); + await lock.releaseLock(); return errorLnd(res); } } else { + await lock.releaseLock(); return errorNotEnougBalance(res); } });