From 0bdf806d36bf5fcfb128cf6def49b52db96fa43d Mon Sep 17 00:00:00 2001 From: sairajzero Date: Mon, 7 Sep 2020 21:27:34 +0530 Subject: [PATCH 1/8] update stdop and beautify --- app/index.html | 812 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 535 insertions(+), 277 deletions(-) diff --git a/app/index.html b/app/index.html index 25e500d..3aab935 100644 --- a/app/index.html +++ b/app/index.html @@ -7624,25 +7624,35 @@ Bitcoin.Util = { //Returns public-key from private-key getPubKeyHex: function (privateKeyHex) { + if (!privateKeyHex) + return null; var key = new Bitcoin.ECKey(privateKeyHex); - if (key.priv == null) { - alert("Invalid Private key"); - return; - } + if (key.priv == null) + return null; key.setCompressed(true); var pubkeyHex = key.getPubKeyHex(); return pubkeyHex; }, - //Returns flo-ID from public-key - getFloIDfromPubkeyHex: function (pubkeyHex) { - var key = new Bitcoin.ECKey().setPub(pubkeyHex); - var floID = key.getBitcoinAddress(); - return floID; + //Returns flo-ID from public-key or private-key + getFloID: function (keyHex) { + if(!pubkeyHex) + return null; + try { + var key = new Bitcoin.ECKey(privateKeyHex); + if (key.priv == null) + key.setPub(pubkeyHex); + var floID = key.getBitcoinAddress(); + return floID; + } catch (e) { + return null; + } }, //Verify the private-key for the given public-key or flo-ID verifyPrivKey: function (privateKeyHex, pubKey_floID, isfloID = true) { + if(!privateKeyHex || !pubKey_floID) + return false; try { var key = new Bitcoin.ECKey(privateKeyHex); if (key.priv == null) @@ -7661,6 +7671,8 @@ Bitcoin.Util = { //Check if the given Address is valid or not validateAddr: function (inpAddr) { + if(!inpAddr) + return false; try { var addr = new Bitcoin.Address(inpAddr); return true; @@ -7698,7 +7710,7 @@ Bitcoin.Util = { } return false; } catch { - return false + return false; } } } @@ -7745,7 +7757,7 @@ Bitcoin.Util = { //Promised function to get data from API promisedAPI: function (apicall) { return new Promise((resolve, reject) => { - //console.log(apicall) + console.log(apicall) this.util.fetch_api(apicall) .then(result => resolve(result)) .catch(error => reject(error)); @@ -7762,22 +7774,24 @@ Bitcoin.Util = { }, //Write Data into blockchain - writeData: function (senderAddr, Data, PrivKey, receiverAddr = floGlobals.adminID) { + writeData: function (senderAddr, data, privKey, receiverAddr = floGlobals.adminID) { return new Promise((resolve, reject) => { - this.sendTx(senderAddr, receiverAddr, floGlobals.sendAmt, PrivKey, Data) + if (typeof data != "string") + data = JSON.stringify(data); + this.sendTx(senderAddr, receiverAddr, floGlobals.sendAmt, privKey, data) .then(txid => resolve(txid)) .catch(error => reject(error)) }); }, //Send Tx to blockchain - sendTx: function (senderAddr, receiverAddr, sendAmt, PrivKey, floData = '') { + sendTx: function (senderAddr, receiverAddr, sendAmt, privKey, floData = '') { return new Promise((resolve, reject) => { if (!floCrypto.validateAddr(senderAddr)) reject(`Invalid address : ${senderAddr}`); else if (!floCrypto.validateAddr(receiverAddr)) reject(`Invalid address : ${receiverAddr}`); - if (PrivKey.length < 1 || !floCrypto.verifyPrivKey(PrivKey, senderAddr)) + if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr)) reject("Invalid Private key!"); else if (typeof sendAmt !== 'number' || sendAmt <= 0) reject(`Invalid sendAmt : ${sendAmt}`); @@ -7801,8 +7815,8 @@ Bitcoin.Util = { var change = utxoAmt - sendAmt - fee; if (change > 0) trx.addoutput(senderAddr, change); - trx.addflodata(floData); - var signedTxHash = trx.sign(PrivKey, 1); + trx.addflodata(floData.replace(/\n/g, ' ')); + var signedTxHash = trx.sign(privKey, 1); this.broadcastTx(signedTxHash) .then(txid => resolve(txid)) .catch(error => reject(error)) @@ -7812,11 +7826,237 @@ Bitcoin.Util = { }); }, + //merge all UTXOs of a given floID into a single UTXO + mergeUTXOs: function (floID, privKey, floData = '') { + return new Promise((resolve, reject) => { + if (!floCrypto.validateAddr(floID)) + return reject(`Invalid floID`); + if (!floCrypto.verifyPrivKey(privKey, floID)) + return reject("Invalid Private Key") + + var trx = bitjs.transaction(); + var utxoAmt = 0.0; + var fee = floGlobals.fee; + this.promisedAPI(`api/addr/${floID}/utxo`).then(utxos => { + for (var i = utxos.length - 1; i >= 0; i--) { + if (utxos[i].confirmations) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i] + .scriptPubKey) + utxoAmt += utxos[i].amount; + } + } + trx.addoutput(floID, utxoAmt - fee); + trx.addflodata(floData.replace(/\n/g, ' ')); + var signedTxHash = trx.sign(privKey, 1); + this.broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }) + }, + + /**Write data into blockchain from (and/or) to multiple floID + * @param {Array} senderPrivKeys List of sender private-keys + * @param {string} data FLO data of the txn + * @param {Array} receivers List of receivers + * @param {boolean} preserveRatio (optional) preserve ratio or equal contribution + * @return {Promise} + */ + writeDataMultiple: function (senderPrivKeys, data, receivers = [floGlobals.adminID], preserveRatio = true) { + return new Promise((resolve, reject) => { + if (!Array.isArray(senderPrivKeys)) + return reject("Invalid senderPrivKeys: SenderPrivKeys must be Array") + if (!preserveRatio) { + let tmp = {}; + let amount = (floGlobals.sendAmt * receivers.length) / senderPrivKeys.length; + senderPrivKeys.forEach(key => tmp[key] = amount); + senderPrivKeys = tmp + } + if (!Array.isArray(receivers)) + return reject("Invalid receivers: Receivers must be Array") + else { + let tmp = {}; + let amount = floGlobals.sendAmt; + receivers.forEach(floID => tmp[floID] = amount); + receivers = tmp + } + if (typeof data != "string") + data = JSON.stringify(data); + this.sendTxMultiple(senderPrivKeys, receivers, data) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }) + }, + + /**Send Tx from (and/or) to multiple floID + * @param {Array or Object} senderPrivKeys List of sender private-key (optional: with coins to be sent) + * @param {Object} receivers List of receivers with respective amount to be sent + * @param {string} floData FLO data of the txn + * @return {Promise} + */ + sendTxMultiple: function (senderPrivKeys, receivers, floData = '') { + return new Promise((resolve, reject) => { + + let senders = {}, + preserveRatio; + //check for argument validations + try { + let invalids = { + InvalidSenderPrivKeys: [], + InvalidSenderAmountFor: [], + InvalidReceiverIDs: [], + InvalidReceiveAmountFor: [] + } + let inputVal = 0, + outputVal = 0; + //Validate sender privatekeys (and send amount if passed) + //conversion when only privateKeys are passed (preserveRatio mode) + if (Array.isArray(senderPrivKeys)) { + senderPrivKeys.forEach(key => { + try { + if (!key) + invalids.InvalidSenderPrivKeys.push(key); + else { + let floID = floCrypto.getFloID(key); + senders[floID] = { + wif: key + } + } + } catch (error) { + invalids.InvalidSenderPrivKeys.push(key) + } + }) + preserveRatio = true; + } + //conversion when privatekeys are passed with send amount + else { + for (let key in senderPrivKeys) { + try { + if (!key) + invalids.InvalidSenderPrivKeys.push(key); + else { + if (typeof senderPrivKeys[key] !== 'number' || senderPrivKeys[ + key] <= 0) + invalids.InvalidSenderAmountFor.push(key) + else + inputVal += senderPrivKeys[key]; + let floID = floCrypto.getFloID(key); + senders[floID] = { + wif: key, + coins: senderPrivKeys[key] + } + } + } catch (error) { + invalids.InvalidSenderPrivKeys.push(key) + } + } + preserveRatio = false; + } + //Validate the receiver IDs and receive amount + for (let floID in receivers) { + if (!floCrypto.validateAddr(floID)) + invalids.InvalidReceiverIDs.push(floID) + if (typeof receivers[floID] !== 'number' || receivers[floID] <= 0) + invalids.InvalidReceiveAmountFor.push(floID) + else + outputVal += receivers[floID]; + } + //Reject if any invalids are found + for (let i in invalids) + if (!invalids[i].length) + delete invalids[i]; + if (Object.keys(invalids).length) + return reject(invalids); + //Reject if given inputVal and outputVal are not equal + if (!preserveRatio && inputVal != outputVal) + return reject( + `Input Amount (${inputVal}) not equal to Output Amount (${outputVal})`) + } catch (error) { + return reject(error) + } + //Get balance of senders + let promises = [] + for (let floID in senders) + promises.push(this.getBalance(floID)) + Promise.all(promises).then(results => { + let totalBalance = 0, + totalFee = floGlobals.fee, + balance = {}; + //Divide fee among sender if not for preserveRatio + if (!preserveRatio) + var dividedFee = totalFee / Object.keys(senders).length; + //Check if balance of each sender is sufficient enough + let insufficient = []; + for (let floID in senders) { + balance[floID] = parseFloat(results.shift()); + if (isNaN(balance[floID]) || (preserveRatio && balance[floID] <= + totalFee) || (!preserveRatio && balance[floID] < senders[floID] + .coins + dividedFee)) + insufficient.push(floID) + totalBalance += balance[floID]; + } + if (insufficient.length) + return reject({ + InsufficientBalance: insufficient + }) + //Calculate totalSentAmount and check if totalBalance is sufficient + let totalSendAmt = totalFee; + for (floID in receivers) + totalSendAmt += receivers[floID]; + if (totalBalance < totalSendAmt) + return reject("Insufficient total Balance") + //Get the UTXOs of the senders + let promises = [] + for (floID in senders) + promises.push(this.promisedAPI(`api/addr/${floID}/utxo`)) + Promise.all(promises).then(results => { + let wifSeq = []; + var trx = bitjs.transaction(); + for (floID in senders) { + let utxos = results.shift(); + let sendAmt; + if (preserveRatio) { + let ratio = (balance[floID] / totalBalance); + sendAmt = totalSendAmt * ratio; + } else + sendAmt = senders[floID].coins + dividedFee; + let wif = senders[floID].wif; + let utxoAmt = 0.0; + for (let i = utxos.length - 1; + (i >= 0) && (utxoAmt < sendAmt); i--) { + if (utxos[i].confirmations) { + trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i] + .scriptPubKey) + wifSeq.push(wif); + utxoAmt += utxos[i].amount; + } + } + if (utxoAmt < sendAmt) + return reject("Insufficient balance:" + floID); + let change = (utxoAmt - sendAmt); + if (change > 0) + trx.addoutput(floID, change); + } + for (floID in receivers) + trx.addoutput(floID, receivers[floID]); + trx.addflodata(floData.replace(/\n/g, ' ')); + for (let i = 0; i < wifSeq.length; i++) + trx.signinput(i, wifSeq[i], 1); + var signedTxHash = trx.serialize(); + this.broadcastTx(signedTxHash) + .then(txid => resolve(txid)) + .catch(error => reject(error)) + }).catch(error => reject(error)) + }).catch(error => reject(error)) + }) + }, + //Broadcast signed Tx in blockchain using API broadcastTx: function (signedTxHash) { return new Promise((resolve, reject) => { var request = new XMLHttpRequest(); var url = this.util.serverList[this.util.curPos] + 'api/tx/send'; + console.log(url) if (signedTxHash.length < 1) reject("Empty Signature"); else { @@ -7866,7 +8106,7 @@ Bitcoin.Util = { filter : custom filter funtion for floData (eg . filter: d => {return d[0] == '$'}) */ readData: function (addr, options = {}) { - options.limit = options.limit | 1000 + options.limit = options.limit | 0 options.ignoreOld = options.ignoreOld | 0 return new Promise((resolve, reject) => { this.promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => { @@ -7881,13 +8121,17 @@ Bitcoin.Util = { if (options.sentOnly && response.items[i].vin[0].addr !== addr) continue; - if (options.pattern && !response.items[i].floData - .startsWith(options.pattern, 0) && !response.items[i] - .floData.startsWith(options.pattern, 2)) - continue; - if (options.contains && !response.items[i].floData.includes( - options.contains)) - continue; + if (options.pattern) { + try { + let jsonContent = JSON.parse(response.items[i] + .floData) + if (!Object.keys(jsonContent).includes(options + .pattern)) + continue; + } catch (error) { + continue; + } + } if (options.filter && !options.filter(response.items[i] .floData)) continue; @@ -8097,7 +8341,7 @@ Bitcoin.Util = { this.connect(snID) .then(node => resolve(node)) .catch(error => { - if(reverse) + if (reverse) var nxtNode = this.kBucket.prevNode(snID); else var nxtNode = this.kBucket.nextNode(snID); @@ -8215,17 +8459,16 @@ Bitcoin.Util = { else { //Serving Users //Delete request from receiver - if(data.delete){ + if (data.delete) { let closeNode = floSupernode.kBucket.closestNode(data.from); - if (floGlobals.serveList.includes(closeNode) && - data.senderID == floCrypto.getFloIDfromPubkeyHex(data.pubKey) && - floCrypto.verifySign(JSON.stringify(data.delete), data.sign, data.pubKey)) { + if (floGlobals.serveList.includes(closeNode) && + data.senderID == floCrypto.getFloIDfromPubkeyHex(data.pubKey) && + floCrypto.verifySign(JSON.stringify(data.delete), data.sign, data.pubKey)) { //start the deletion process //indicate backup nodes to delete data } - } - else{ + } else { let closeNode = floSupernode.kBucket.closestNode(data.receiverID) if (floGlobals.serveList.includes(closeNode) && data.senderID == floCrypto.getFloIDfromPubkeyHex(data.pubKey) && @@ -8241,14 +8484,15 @@ Bitcoin.Util = { type: data.type, comment: data.comment } - compactIDB.addData(floGlobals.diskList.includes(value.application) ? value.application : floGlobals + compactIDB.addData(floGlobals.diskList.includes(value.application) ? value.application : + floGlobals .defaultDisk, value, key, `SN_${closeNode}`) sendBackupData(key, value, closeNode); } } } - - if((refreshData.countdown--) <=0 ) + + if ((refreshData.countdown--) <= 0) refreshData(); } catch (error) { console.log(error.message); @@ -8268,7 +8512,7 @@ Bitcoin.Util = { const compactIDB = { setDefaultDB: function (dbName) { - this.dbName = dbName; + this.defaultDB = dbName; }, initDB: function (dbName, objectStores = {}, version = null, removeStores = []) { @@ -8313,7 +8557,7 @@ Bitcoin.Util = { }); }, - openDB: function (dbName = this.dbName) { + openDB: function (dbName = this.defaultDB) { return new Promise((resolve, reject) => { var idb = indexedDB.open(dbName); idb.onerror = (event) => reject("Error in opening IndexedDB!"); @@ -8321,7 +8565,7 @@ Bitcoin.Util = { }); }, - deleteDB: function (dbName = this.dbName) { + deleteDB: function (dbName = this.defaultDB) { return new Promise((resolve, reject) => { var deleteReq = indexedDB.deleteDatabase(dbName);; deleteReq.onerror = (event) => reject("Error deleting database!"); @@ -8329,7 +8573,7 @@ Bitcoin.Util = { }); }, - writeData: function (obsName, data, key = false, dbName = this.dbName) { + writeData: function (obsName, data, key = false, dbName = this.defaultDB) { return new Promise((resolve, reject) => { this.openDB(dbName).then(db => { var obs = db.transaction(obsName, "readwrite").objectStore(obsName); @@ -8343,7 +8587,7 @@ Bitcoin.Util = { }); }, - addData: function (obsName, data, key = false, dbName = this.dbName) { + addData: function (obsName, data, key = false, dbName = this.defaultDB) { return new Promise((resolve, reject) => { this.openDB(dbName).then(db => { var obs = db.transaction(obsName, "readwrite").objectStore(obsName); @@ -8357,7 +8601,7 @@ Bitcoin.Util = { }); }, - removeData: function (obsName, key, dbName = this.dbName) { + removeData: function (obsName, key, dbName = this.defaultDB) { return new Promise((resolve, reject) => { this.openDB(dbName).then(db => { var obs = db.transaction(obsName, "readwrite").objectStore(obsName); @@ -8383,7 +8627,7 @@ Bitcoin.Util = { }); }, - readData: function (obsName, key, dbName = this.dbName) { + readData: function (obsName, key, dbName = this.defaultDB) { return new Promise((resolve, reject) => { this.openDB(dbName).then(db => { var obs = db.transaction(obsName, "readonly").objectStore(obsName); @@ -8397,7 +8641,7 @@ Bitcoin.Util = { }); }, - readAllData: function (obsName, dbName = this.dbName) { + readAllData: function (obsName, dbName = this.defaultDB) { return new Promise((resolve, reject) => { this.openDB(dbName).then(db => { var obs = db.transaction(obsName, "readonly").objectStore(obsName); @@ -8419,7 +8663,7 @@ Bitcoin.Util = { }); }, - searchData: function (obsName, options = {}, dbName = this.dbName) { + searchData: function (obsName, options = {}, dbName = this.defaultDB) { options.lowerKey = options.atKey || options.lowerKey || 0 options.upperKey = options.atKey || options.upperKey || false options.patternEval = options.patternEval || ((k, v) => { @@ -8473,7 +8717,8 @@ Bitcoin.Util = { if (myFloID in floGlobals.supernodes) { initIndexedDBforSupernodeDataStorage(myFloID).then(result => { console.log(result) - refreshData.countdown = floGlobals.supernodeConfig.refreshDelay; + refreshData.countdown = floGlobals.supernodeConfig + .refreshDelay; floSupernode.initSupernode(serverPwd, myFloID).then( async result => { console.log(result) @@ -8481,13 +8726,13 @@ Bitcoin.Util = { floGlobals.storedList.push(myFloID) await sleep(5000); connectToAllBackupSupernode() - .then(result => console.log(result)) - .catch(error => console.error(error)) - .finally(async _ => { - console.log(result) - await sleep(2000); - indicateSupernodeUp(); - }) + .then(result => console.log(result)) + .catch(error => console.error(error)) + .finally(async _ => { + console.log(result) + await sleep(2000); + indicateSupernodeUp(); + }) }).catch(error => console.error(error)) }).catch(error => console.error(error)) } @@ -8653,7 +8898,7 @@ Bitcoin.Util = { "comment" ]; var idbObj = {} - for( let d of floGlobals.diskList) { + for (let d of floGlobals.diskList) { idbObj[d] = { indexes: {} } @@ -8724,56 +8969,58 @@ Bitcoin.Util = { function readSupernodeConfigFromAPI(flag = true) { return new Promise((resolve, reject) => { - compactIDB.readData("lastTx", floGlobals.adminID).then(lastTx => { - floBlockchainAPI.readData(floGlobals.adminID, { - ignoreOld: lastTx, - sendOnly: true, - pattern: "SuperNodeStorage" - }).then(result => { - let promises = [] - let newNodes = [] - let delNodes = [] - for (var i = result.data.length - 1; i >= 0; i--) { - var content = JSON.parse(result.data[i]).SuperNodeStorage; - for (sn in content.removeNodes) { - promises.push(compactIDB.removeData("supernodes", sn)) - delNodes.push(sn) - } - for (sn in content.addNodes) { - promises.push(compactIDB.writeData("supernodes", content - .addNodes[sn], sn)) - newNodes.push(sn) - } - for (c in content.config) - promises.push(compactIDB.writeData("config", content - .config[c], c)) - for (app in content.application) - promises.push(compactIDB.writeData("applications", - content.application[app], app)) + compactIDB.readData("lastTx", floGlobals.adminID).then(lastTx => { + floBlockchainAPI.readData(floGlobals.adminID, { + ignoreOld: lastTx, + sendOnly: true, + pattern: "SuperNodeStorage" + }).then(result => { + let promises = [] + let newNodes = [] + let delNodes = [] + for (var i = result.data.length - 1; i >= 0; i--) { + var content = JSON.parse(result.data[i]).SuperNodeStorage; + for (sn in content.removeNodes) { + promises.push(compactIDB.removeData("supernodes", sn)) + delNodes.push(sn) } - compactIDB.writeData("lastTx", result.totalTxs, floGlobals.adminID); - Promise.all(promises).then(results => { - readDataFromIDB().then(result => { - migrateData(newNodes, delNodes, flag).then(result => { - console.info(result) - resolve("Read Supernode Data from Blockchain") - }).catch(error => reject(error)) + for (sn in content.addNodes) { + promises.push(compactIDB.writeData("supernodes", content + .addNodes[sn], sn)) + newNodes.push(sn) + } + for (c in content.config) + promises.push(compactIDB.writeData("config", content + .config[c], c)) + for (app in content.application) + promises.push(compactIDB.writeData("applications", + content.application[app], app)) + } + compactIDB.writeData("lastTx", result.totalTxs, floGlobals.adminID); + Promise.all(promises).then(results => { + readDataFromIDB().then(result => { + migrateData(newNodes, delNodes, flag).then( + result => { + console.info(result) + resolve( + "Read Supernode Data from Blockchain") }).catch(error => reject(error)) }).catch(error => reject(error)) }).catch(error => reject(error)) }).catch(error => reject(error)) - }) + }).catch(error => reject(error)) + }) } function readDataFromIDB() { const dataList = { - supernodes: "supernodes", + supernodes: "supernodes", supernodeConfig: "config", applicationList: "applications" } - const readIDB = function(name, obs){ + const readIDB = function (name, obs) { return new Promise((res, rej) => { compactIDB.readAllData(obs).then(data => { floGlobals[name] = data; @@ -8784,12 +9031,12 @@ Bitcoin.Util = { return new Promise((resolve, reject) => { let promises = [] - for(let d in dataList) + for (let d in dataList) promises.push(readIDB(d, dataList[d])) Promise.all(promises) - .then(results => resolve("Read data from IDB")) - .catch(error => reject(error)) - }) + .then(results => resolve("Read data from IDB")) + .catch(error => reject(error)) + }) } function readAppSubAdminListFromAPI() { @@ -8841,9 +9088,9 @@ Bitcoin.Util = { /*Supernode Backup and migration functions*/ function connectToAllBackupSupernode(curNode = myFloID, i = 0) { return new Promise((resolve, reject) => { - if (floGlobals.backupNodes.length > i){ + if (floGlobals.backupNodes.length > i) { let rmNodes = floGlobals.backupNodes.splice(i, floGlobals.backupNodes.length); - for(node of rmNodes) + for (node of rmNodes) node.wsConn.close(); } if (i >= floGlobals.supernodeConfig.backupDepth) @@ -8855,15 +9102,15 @@ Bitcoin.Util = { else { let flag = false; initateBackupWebsocket(nxtNode).then(node => { - console.warn(`Connected to backup node: ${node.floID}`) - floGlobals.backupNodes[i] = node - flag = true; - }).catch(error => console.error(error)) - .finally( _ => { - connectToAllBackupSupernode(nxtNode, flag ? i + 1 : i) - .then(result => resolve(result)) - .catch(error => reject(error)) - }) + console.warn(`Connected to backup node: ${node.floID}`) + floGlobals.backupNodes[i] = node + flag = true; + }).catch(error => console.error(error)) + .finally(_ => { + connectToAllBackupSupernode(nxtNode, flag ? i + 1 : i) + .then(result => resolve(result)) + .catch(error => reject(error)) + }) } } }) @@ -8871,74 +9118,74 @@ Bitcoin.Util = { function initateBackupWebsocket(nodeID) { return new Promise((resolve, reject) => { - console.log("Attempting to connect to backupNode:", nodeID) - floSupernode.connect(nodeID).then(node => { - node.wsConn.onmessage = (evt) => { - if(evt.data === "$-") - replaceOfflineBackupNode(nodeID); - } - node.wsConn.onclose = (evt) => { - let i = floGlobals.backupNodes.map(d => d.floID).indexOf(nodeID); - if(i !== -1) - initateBackupWebsocket(nodeID) - .then(bNode => floGlobals.backupNodes[i] = bNode) - .catch(error => replaceOfflineBackupNode(nodeID)) - } - backupNode = { - floID: node.snID, - wsConn: node.wsConn - } - resolve(backupNode); - }).catch(error => reject(error)) + console.log("Attempting to connect to backupNode:", nodeID) + floSupernode.connect(nodeID).then(node => { + node.wsConn.onmessage = (evt) => { + if (evt.data === "$-") + replaceOfflineBackupNode(nodeID); + } + node.wsConn.onclose = (evt) => { + let i = floGlobals.backupNodes.map(d => d.floID).indexOf(nodeID); + if (i !== -1) + initateBackupWebsocket(nodeID) + .then(bNode => floGlobals.backupNodes[i] = bNode) + .catch(error => replaceOfflineBackupNode(nodeID)) + } + backupNode = { + floID: node.snID, + wsConn: node.wsConn + } + resolve(backupNode); + }).catch(error => reject(error)) }) } function replaceOfflineBackupNode(offlineNodeID) { //remove offline node and add the immediate next available node var index = floGlobals.backupNodes.map(d => d.floID).indexOf(offlineNodeID); - if (index === -1) //return if offineNode is not a backupNode - return + if (index === -1) //return if offineNode is not a backupNode + return floGlobals.backupNodes.splice(index, 1); //connect to next node available var len = floGlobals.backupNodes.length connectToAllBackupSupernode(len == 0 ? offlineNodeID : floGlobals.backupNodes[len - 1], len).then(result => { - console.log(result) - //inform the newly connected node to store backups of all serving - for( let sn of floGlobals.serveList){ - var sendData = { - from: myFloID, - sn_msg: { - type: "startBackupStore", - snID: sn, - time: Date.now() - } - } - sendData.sign = floCrypto.signData(JSON.stringify(sendData.sn_msg), myPrivKey) - floGlobals.backupNodes[len].wsConn.send(JSON.stringify(sendData)) - } - }).catch(error => console.error(error)) - .finally ( _ => { - if (index === 0) { - //start serving the dead node - if (floGlobals.backupNodes.length === 0){ - for(let sn in floGlobals.supernodes) - startBackupServe(sn) - } - //inform the immediate next node of the dead to start serving it - else { + console.log(result) + //inform the newly connected node to store backups of all serving + for (let sn of floGlobals.serveList) { var sendData = { from: myFloID, sn_msg: { - type: "startBackupServe", - snID: offlineNodeID, + type: "startBackupStore", + snID: sn, time: Date.now() } } sendData.sign = floCrypto.signData(JSON.stringify(sendData.sn_msg), myPrivKey) - floGlobals.backupNodes[0].wsConn.send(JSON.stringify(sendData)) + floGlobals.backupNodes[len].wsConn.send(JSON.stringify(sendData)) } - } - }) + }).catch(error => console.error(error)) + .finally(_ => { + if (index === 0) { + //start serving the dead node + if (floGlobals.backupNodes.length === 0) { + for (let sn in floGlobals.supernodes) + startBackupServe(sn) + } + //inform the immediate next node of the dead to start serving it + else { + var sendData = { + from: myFloID, + sn_msg: { + type: "startBackupServe", + snID: offlineNodeID, + time: Date.now() + } + } + sendData.sign = floCrypto.signData(JSON.stringify(sendData.sn_msg), myPrivKey) + floGlobals.backupNodes[0].wsConn.send(JSON.stringify(sendData)) + } + } + }) } function processDataFromSupernode(data) { @@ -8987,7 +9234,7 @@ Bitcoin.Util = { sn_msg: sn_msg, sign: floCrypto.signData(JSON.stringify(sn_msg), myPrivKey) } - for(let node of floGlobals.backupNodes) + for (let node of floGlobals.backupNodes) node.wsConn.send(JSON.stringify(data)) } @@ -9042,7 +9289,7 @@ Bitcoin.Util = { function requestBackupData(from, snID) { var promises = [] - for(let i in floGlobals.diskList) + for (let i in floGlobals.diskList) promises[i] = compactIDB.searchData(floGlobals.diskList[i], { lastOnly: true }, `SN_${snID}`) @@ -9062,8 +9309,8 @@ Bitcoin.Util = { } function storeBackupData(data) { - if (floGlobals.storedList.includes(data.snID) && - floSupernode.kBucket.closestNode(data.value.receiverID) === data.snID) { + if (floGlobals.storedList.includes(data.snID) && + floSupernode.kBucket.closestNode(data.value.receiverID) === data.snID) { compactIDB.addData( floGlobals.diskList.includes(data.value.application) ? data.value.application : floGlobals .defaultDisk, @@ -9074,7 +9321,7 @@ Bitcoin.Util = { function indicateSupernodeUp() { console.log("Indicating supernode is up") - if(floGlobals.backupNodes.length){ + if (floGlobals.backupNodes.length) { //inform all other nodes var data = { from: myFloID, @@ -9085,14 +9332,14 @@ Bitcoin.Util = { } data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey) let dataStr = JSON.stringify(data) - for(let sn in floGlobals.supernodes){ - if(sn !== myFloID){ + for (let sn in floGlobals.supernodes) { + if (sn !== myFloID) { floSupernode.connect(sn) - .then(node => { - node.wsConn.send(dataStr); - console.info('data sent to :'+ sn) - node.wsConn.close(); - }).catch(error => console.error(error)) + .then(node => { + node.wsConn.send(dataStr); + console.info('data sent to :' + sn) + node.wsConn.close(); + }).catch(error => console.error(error)) } } //Inform backup nodes to store self @@ -9105,8 +9352,9 @@ Bitcoin.Util = { //request self data from backup requestBackupData(floGlobals.backupNodes[0].floID, myFloID) } else { - let nodeList = floSupernode.kBucket.prevNode(myFloID, floSupernode.kBucket.supernodeKBucket.toArray().length-1) - for(sn of nodeList){ + let nodeList = floSupernode.kBucket.prevNode(myFloID, floSupernode.kBucket.supernodeKBucket.toArray() + .length - 1) + for (sn of nodeList) { startBackupStore(sn); startBackupServe(sn); } @@ -9125,81 +9373,85 @@ Bitcoin.Util = { if (floGlobals.serveList.includes(snID)) { stopBackupServe(snID); //inform the revived node to serve the other applicable dead nodes - let iNodes = floSupernode.kBucket.innerNodes(snID, myFloID) - data.sn_msg.time = Date.now(); - for (let sn of floGlobals.serveList.slice()){ - if (!iNodes.includes(sn) && sn != myFloID) { - data.sn_msg.snID = sn; - data.sn_msg.type = "startBackupStore"; - data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey); - node.wsConn.send(JSON.stringify(data)) - data.sn_msg.type = "startBackupServe"; - data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey); - node.wsConn.send(JSON.stringify(data)) - stopBackupServe(sn); - } - } - } - - if (floGlobals.backupNodes.length < floGlobals.supernodeConfig.backupDepth) { - //when less supernodes available, just connect to the revived node - let nxtNodes = floSupernode.kBucket.nextNode(myFloID, floSupernode.kBucket.supernodeKBucket.toArray().length) - var index = floGlobals.backupNodes.length - for (let i in floGlobals.backupNodes) { - if (snID == floGlobals.backupNodes[i].floID) { //revived node is already connected - index = false - break; - } else if (nxtNodes.indexOf(snID) < nxtNodes.indexOf(floGlobals.backupNodes[i].floID)) { - index = i; - break; - } - } - if (index !== false) { - initateBackupWebsocket(snID).then(result => { - floGlobals.backupNodes.splice(index, 0, result) // add revived node as backup node - //inform node on the list of backups to store - data.sn_msg.time = Date.now(); - data.sn_msg.type = "startBackupStore"; - for(let sn of floGlobals.serveList){ - data.sn_msg.snID = sn; - data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey); - node.wsConn.send(JSON.stringify(data)) - } - }).catch(error => console.error(error)) - } - } else { - //connect to the revived node as backup if needed - let nxtNodes = floSupernode.kBucket.nextNode(myFloID, floSupernode.kBucket.supernodeKBucket.toArray() - .length) - var index = false - for (let i in floGlobals.backupNodes) { - if (snID == floGlobals.backupNodes[i].floID) //revived node is already connected - break; - else if (nxtNodes.indexOf(snID) < nxtNodes.indexOf(floGlobals.backupNodes[i].floID)) { - index = i; - break; - } - } - console.info(index) - if (index !== false) { - initateBackupWebsocket(snID).then(result => { - floGlobals.backupNodes.splice(index, 0, result) // add revived node as backup node - let rmNode = floGlobals.backupNodes.pop() // remove the last extra backup node - //inform node on the list of backups to store and inform removed node to stop storing - data.sn_msg.time = Date.now(); - for(let sn of floGlobals.serveList){ + let iNodes = floSupernode.kBucket.innerNodes(snID, myFloID) + data.sn_msg.time = Date.now(); + for (let sn of floGlobals.serveList.slice()) { + if (!iNodes.includes(sn) && sn != myFloID) { data.sn_msg.snID = sn; data.sn_msg.type = "startBackupStore"; data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey); - node.wsConn.send(JSON.stringify(data)) //start for connected backup node - data.sn_msg.type = "stopBackupStore"; + node.wsConn.send(JSON.stringify(data)) + data.sn_msg.type = "startBackupServe"; data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey); - rmNode.wsConn.send(JSON.stringify(data)) //stop for removed backup node + node.wsConn.send(JSON.stringify(data)) + stopBackupServe(sn); } - - }).catch(error => console.error(error)) + } + } + + if (floGlobals.backupNodes.length < floGlobals.supernodeConfig.backupDepth) { + //when less supernodes available, just connect to the revived node + let nxtNodes = floSupernode.kBucket.nextNode(myFloID, floSupernode.kBucket.supernodeKBucket + .toArray().length) + var index = floGlobals.backupNodes.length + for (let i in floGlobals.backupNodes) { + if (snID == floGlobals.backupNodes[i].floID) { //revived node is already connected + index = false + break; + } else if (nxtNodes.indexOf(snID) < nxtNodes.indexOf(floGlobals.backupNodes[i].floID)) { + index = i; + break; + } + } + if (index !== false) { + initateBackupWebsocket(snID).then(result => { + floGlobals.backupNodes.splice(index, 0, + result) // add revived node as backup node + //inform node on the list of backups to store + data.sn_msg.time = Date.now(); + data.sn_msg.type = "startBackupStore"; + for (let sn of floGlobals.serveList) { + data.sn_msg.snID = sn; + data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey); + node.wsConn.send(JSON.stringify(data)) + } + }).catch(error => console.error(error)) + } + } else { + //connect to the revived node as backup if needed + let nxtNodes = floSupernode.kBucket.nextNode(myFloID, floSupernode.kBucket.supernodeKBucket + .toArray() + .length) + var index = false + for (let i in floGlobals.backupNodes) { + if (snID == floGlobals.backupNodes[i].floID) //revived node is already connected + break; + else if (nxtNodes.indexOf(snID) < nxtNodes.indexOf(floGlobals.backupNodes[i].floID)) { + index = i; + break; + } + } + console.info(index) + if (index !== false) { + initateBackupWebsocket(snID).then(result => { + floGlobals.backupNodes.splice(index, 0, + result) // add revived node as backup node + let rmNode = floGlobals.backupNodes.pop() // remove the last extra backup node + //inform node on the list of backups to store and inform removed node to stop storing + data.sn_msg.time = Date.now(); + for (let sn of floGlobals.serveList) { + data.sn_msg.snID = sn; + data.sn_msg.type = "startBackupStore"; + data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey); + node.wsConn.send(JSON.stringify(data)) //start for connected backup node + data.sn_msg.type = "stopBackupStore"; + data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey); + rmNode.wsConn.send(JSON.stringify(data)) //stop for removed backup node + } + + }).catch(error => console.error(error)) + } } - } }) } @@ -9221,7 +9473,7 @@ Bitcoin.Util = { var index = floGlobals.serveList.indexOf(snID); if (index !== -1 && snID !== myFloID) { floGlobals.serveList.splice(index, 1); - if(floGlobals.backupNodes.length == floGlobals.supernodeConfig.backupDepth){ + if (floGlobals.backupNodes.length == floGlobals.supernodeConfig.backupDepth) { //indicate the last backup node to stop storing the revived's backup var lastIndex = floGlobals.backupNodes.length - 1 if (lastIndex !== -1) { @@ -9236,7 +9488,7 @@ Bitcoin.Util = { data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey) floGlobals.backupNodes[lastIndex].wsConn.send(JSON.stringify(data)) } - } + } console.warn("BackupServe stopped for " + snID); } } @@ -9245,7 +9497,7 @@ Bitcoin.Util = { if (!floGlobals.storedList.includes(snID)) { floGlobals.storedList.push(snID) initIndexedDBforSupernodeDataStorage(snID).then(result => { - if(from) + if (from) requestBackupData(from, snID); console.warn("BackupStore started for " + snID); }).catch(error => console.error(error)) @@ -9268,36 +9520,37 @@ Bitcoin.Util = { floSupernode.kBucket.launch().then(result => { intimateNodes(flag); //intimate all other nodes connectToAllBackupSupernode() - .then(result => console.log(result)) - .catch(error => console.error(error)) - .finally(async _ => { - await sleep(300000); - newNodes.forEach(node => { - //transfer data to new node if required - let prevNode = floSupernode.kBucket.prevNode(node); - if (floGlobals.serveList.includes(prevNode)) - transferData(prevNode, node) - let nextNode = floSupernode.kBucket.nextNode(node); - if (floGlobals.serveList.includes(nextNode)) - transferData(nextNode, node) + .then(result => console.log(result)) + .catch(error => console.error(error)) + .finally(async _ => { + await sleep(300000); + newNodes.forEach(node => { + //transfer data to new node if required + let prevNode = floSupernode.kBucket.prevNode(node); + if (floGlobals.serveList.includes(prevNode)) + transferData(prevNode, node) + let nextNode = floSupernode.kBucket.nextNode(node); + if (floGlobals.serveList.includes(nextNode)) + transferData(nextNode, node) + }) + delNodes.forEach(node => { + //split the data to its prev and next nodes + let KB = floSupernode.kBucket.constructKB(Object.keys( + floGlobals + .supernodes).concat(node).sort()); + let prevNode = floSupernode.kBucket.prevNode(node, 1, KB); + let nextNode = floSupernode.kBucket.nextNode(node, 1, KB); + if (floGlobals.serveList.includes(nextNode)) + splitData(node, prevNode, nextNode); + }) + resolve("Data migration") }) - delNodes.forEach(node => { - //split the data to its prev and next nodes - let KB = floSupernode.kBucket.constructKB(Object.keys(floGlobals - .supernodes).concat(node).sort()); - let prevNode = floSupernode.kBucket.prevNode(node, 1, KB); - let nextNode = floSupernode.kBucket.nextNode(node, 1, KB); - if (floGlobals.serveList.includes(nextNode)) - splitData(node, prevNode, nextNode); - }) - resolve("Data migration") - }) }).catch(error => reject(error)) }) } - function intimateNodes(flag){ - if(!flag) //skip intimate if already intimated + function intimateNodes(flag) { + if (!flag) //skip intimate if already intimated return; //intimate all nodes that there is change in supernode list var data = { @@ -9309,18 +9562,18 @@ Bitcoin.Util = { } data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), myPrivKey) let dataStr = JSON.stringify(data) - for(let sn in floGlobals.supernodes){ - if(sn !== myFloID){ + for (let sn in floGlobals.supernodes) { + if (sn !== myFloID) { floSupernode.connect(sn) - .then(node => { - node.wsConn.send(dataStr); - node.wsConn.close(); - }).catch(error => console.error(error)) + .then(node => { + node.wsConn.send(dataStr); + node.wsConn.close(); + }).catch(error => console.error(error)) } } } - function iniateRefresh(){ + function iniateRefresh() { readSupernodeConfigFromAPI(false) .then(result => console.log(result)) .catch(error => console.error(error)) @@ -9334,7 +9587,8 @@ Bitcoin.Util = { compactIDB.readAllData(obs, `SN_${snID}`).then(result => { let promises = [] for (let k in result) { - if (floSupernode.kBucket.closestNode(result[k].receiverID) === + if (floSupernode.kBucket.closestNode(result[k] + .receiverID) === toID) { var data = { from: myFloID, @@ -9345,10 +9599,12 @@ Bitcoin.Util = { value: result[k] } } - data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), + data.sign = floCrypto.signData(JSON.stringify(data + .sn_msg), myPrivKey) node.wsConn.send(JSON.stringify(data)); - promises.push(compactIDB.removeData(obs, k, `SN_${snID}`)) + promises.push(compactIDB.removeData(obs, k, + `SN_${snID}`)) } } Promise.all(promises).then(r => {}).catch(e => {}) @@ -9393,7 +9649,8 @@ Bitcoin.Util = { compactIDB.readAllData(obs, `SN_${snID}`).then(result => { let promises = [] for (let k in result) { - if (floSupernode.kBucket.closestNode(result[k].receiverID) === + if (floSupernode.kBucket.closestNode(result[k] + .receiverID) === prev) { var data = { from: myFloID, @@ -9404,7 +9661,8 @@ Bitcoin.Util = { value: result[k] } } - data.sign = floCrypto.signData(JSON.stringify(data.sn_msg), + data.sign = floCrypto.signData(JSON.stringify(data + .sn_msg), myPrivKey) node.wsConn.send(JSON.stringify(data)); } else From 4e58e7c9c205afad8d20d73b20e3e726d8e87fec Mon Sep 17 00:00:00 2001 From: sairajzero Date: Wed, 9 Sep 2020 20:54:59 +0530 Subject: [PATCH 2/8] Kbucket sub-module fix --- app/index.html | 263 +++++++++++++++++++++++-------------------------- 1 file changed, 121 insertions(+), 142 deletions(-) diff --git a/app/index.html b/app/index.html index 3aab935..30db3a9 100644 --- a/app/index.html +++ b/app/index.html @@ -25,7 +25,7 @@ FLO: ['https://explorer.mediciland.com/', 'https://livenet.flocha.in/', 'https://flosight.duckdns.org/', 'http://livenet-explorer.floexperiments.com'], FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/'] }, - adminID: "FEzk75EGMPEQMrCuPosGiwuK162hcEu49E", + SNStorageID: "FEzk75EGMPEQMrCuPosGiwuK162hcEu49E", //sendAmt: 0.001, //fee: 0.0005, @@ -8157,160 +8157,136 @@ Bitcoin.Util = { //kBucket object kBucket: { - supernodeKBucket: null, - decodeBase58Address: function (address) { - let k = bitjs.Base58.decode(address) - k.shift() - k.splice(-4, 4) - return Crypto.util.bytesToHex(k) - }, - floIdToKbucketId: function (address) { - const decodedId = this.decodeBase58Address(address); - const nodeIdBigInt = new BigInteger(decodedId, 16); - const nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned(); - const nodeIdNewInt8Array = new Uint8Array(nodeIdBytes); - return nodeIdNewInt8Array; - }, + SNKB: null, + SNCO: null, - constructKB: function (list, refID = floGlobals.adminID) { - const KBoptions = { - localNodeId: this.floIdToKbucketId(refID) - } - let KB = new BuildKBucket(KBoptions); - for (var i = 0; i < list.length; i++) { - this.addNewNode(list[i], KB) - } - return KB; - }, + util: { + decodeID(floID) { + let k = bitjs.Base58.decode(floID) + k.shift() + k.splice(-4, 4) + const decodedId = Crypto.util.bytesToHex(k); + const nodeIdBigInt = new BigInteger(decodedId, 16); + const nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned(); + const nodeIdNewInt8Array = new Uint8Array(nodeIdBytes); + return nodeIdNewInt8Array; + }, + addNode: function (floID, KB) { + let decodedId = this.decodeID(floID); + const contact = { + id: decodedId, + floID: floID + }; + KB.add(contact) + }, + + removeNode: function (floID, KB) { + let decodedId = this.decodeID(floID); + KB.remove(decodedId) + }, + + isPresent: function (floID, KB) { + let kArray = KB.toArray().map(k => k.floID); + return kArray.includes(floID) + }, + + distanceOf: function (floID, KB) { + let decodedId = this.decodeID(floID); + return KB.distance(KB.localNodeId, decodedId); + }, + + closest: function (floID, n, KB) { + let decodedId = this.decodeID(floID); + return KB.closest(flo_addr, n) + }, + + constructKB: function (list, refID) { + const KBoptions = { + localNodeId: this.decodeID(refID) + }; + let KB = new BuildKBucket(KBoptions); + for (let id of list) + this.addNode(id, KB) + return KB; + } + }, launch: function () { return new Promise((resolve, reject) => { - let superNodeList = Object.keys(floGlobals.supernodes); - let masterID = floGlobals.adminID; try { - this.supernodeKBucket = this.constructKB(superNodeList, masterID); + let superNodeList = Object.keys(floGlobals.supernodes); + let masterID = floGlobals.SNStorageID; + this.SNKB = this.util.constructKB(superNodeList, masterID); + this.SNCO = superNodeList.map(sn => [this.util.distance(sn, this.SNKB), sn]) + .sort((a, b) => a[0] - b[0]) + .map(a => a[1]) + console.log(this.SNCO) + console.log(this.SNKB) resolve('Supernode KBucket formed'); } catch (error) { reject(error); } }); }, - addContact: function (id, floID, KB = this.supernodeKBucket) { - const contact = { - id: id, - floID: floID - }; - KB.add(contact) - }, - addNewNode: function (address, KB = this.supernodeKBucket) { - let decodedId = address; - try { - decodedId = this.floIdToKbucketId(address); - } catch (e) { - decodedId = address; + + innerNodes: function (id1, id2) { + if (!this.SNCO.includes(id1) || !this.SNCO.includes(id2)) + throw Error('Given nodes are not supernode'); + let iNodes = [] + for (let i = this.SNCO.indexOf(id1) + 1; this.SNCO[i] != id2; i++) { + if (i < this.SNCO.length) + iNodes.push(this.SNCO[i]) + else i = -1 } - this.addContact(decodedId, address, KB); - }, - isNodePresent: function (flo_id, KB = this.supernodeKBucket) { - let kArray = KB.toArray(); - let kArrayFloIds = kArray.map(k => k.floID); - if (kArrayFloIds.includes(flo_id)) - return true; - else - return false; + return iNodes }, - innerNodes: function (flo_addr1, flo_addr2, KB = this.supernodeKBucket) { - let kArrayFloIds = KB.toArray().map(k => k.floID); - var iNodes = [] - if (kArrayFloIds.includes(flo_addr1) && kArrayFloIds.includes(flo_addr2)) { - for (var i = kArrayFloIds.indexOf(flo_addr1) + 1; kArrayFloIds[i] != flo_addr2; i++) { - if (i >= kArrayFloIds.length) - i = -1 - else - iNodes.push(kArrayFloIds[i]) - } - return iNodes; - } else - throw Error('Given nodes are not in KBucket') + outterNodes: function (id1, id2) { + if (!this.SNCO.includes(id1) || !this.SNCO.includes(id2)) + throw Error('Given nodes are not supernode'); + let oNodes = [] + for (let i = this.SNCO.indexOf(id2) + 1; this.SNCO[i] != id1; i++) { + if (i < this.SNCO.length) + oNodes.push(this.SNCO[i]) + else i = -1 + } + return oNodes }, - outterNodes: function (flo_addr1, flo_addr2, KB = this.supernodeKBucket) { - let kArrayFloIds = KB.toArray().map(k => k.floID); - var oNodes = [] - if (kArrayFloIds.includes(flo_addr1) && kArrayFloIds.includes(flo_addr2)) { - for (var i = kArrayFloIds.indexOf(flo_addr2) + 1; kArrayFloIds[i] != flo_addr1; i++) { - if (i >= kArrayFloIds.length) - i = -1 - else - oNodes.push(kArrayFloIds[i]) - } - return oNodes - } else - throw Error('Given nodes are not in KBucket') + prevNode: function (id, n = 1) { + if (!this.SNCO.includes(id)) + throw Error('Given node is not supernode'); + if (!n) n = this.SNCO.length; + let pNodes = [] + for (let i = 0, j = this.SNCO.indexOf(id) - 1; i < n; j--) { + if (j == this.SNCO.indexOf(id)) + break; + else if (j > -1) + pNodes[i++] = this.SNCO[j] + else j = this.SNCO.length + } + return (n == 1 ? pNodes[0] : pNodes) }, - prevNode: function (flo_addr, n = 1, KB = this.supernodeKBucket) { - let kArrayFloIds = KB.toArray().map(k => k.floID); - let pos = kArrayFloIds.indexOf(flo_addr) - if (pos === -1) - return false; - var pNodes = [] - for (var i = 1; i <= n; i++) { - if (pos - i < 0) - var prev = pos - i + kArrayFloIds.length - else - var prev = pos - i - pNodes.push(kArrayFloIds[prev]) - } - switch (pNodes.length) { - case 0: - return null; - case 1: - return pNodes[0]; - default: - return pNodes; + nextNode: function (id, n = 1) { + if (!this.SNCO.includes(id)) + throw Error('Given node is not supernode'); + if (!n) n = this.SNCO.length; + let nNodes = [] + for (let i = 0, j = this.SNCO.indexOf(id) + 1; i < n; j++) { + if (j == this.SNCO.indexOf(id)) + break; + else if (j < this.SNCO.length) + nNodes[i++] = this.SNCO[j] + else j = -1 } + return (n == 1 ? nNodes[0] : nNodes) }, - nextNode: function (flo_addr, n = 1, KB = this.supernodeKBucket) { - let kArrayFloIds = KB.toArray().map(k => k.floID); - let pos = kArrayFloIds.indexOf(flo_addr) - if (pos === -1) - return false; - var nNodes = [] - for (var i = 1; i <= n; i++) { - if (pos + i >= kArrayFloIds.length) - var next = pos + i - kArrayFloIds.length - else - var next = pos + i - nNodes.push(kArrayFloIds[next]) - } - switch (nNodes.length) { - case 0: - return null; - case 1: - return nNodes[0]; - default: - return nNodes; - } - }, - - closestNode: function (flo_addr, n = 1, KB = this.supernodeKBucket) { - let isFloIdUint8 = flo_addr instanceof Uint8Array; - if (!isFloIdUint8) - flo_addr = this.floIdToKbucketId(flo_addr); - var cNode = KB.closest(flo_addr, n); - cNode = cNode.map(k => k.floID); - switch (cNode.length) { - case 0: - return null; - case 1: - return cNode[0]; - default: - return cNode; - } + closestNode: function (id, n = 1) { + let cNodes = this.util.closest(id, n).map(k => k.floID) + return (n == 1 ? cNodes[0] : cNodes) } }, @@ -8969,8 +8945,8 @@ Bitcoin.Util = { function readSupernodeConfigFromAPI(flag = true) { return new Promise((resolve, reject) => { - compactIDB.readData("lastTx", floGlobals.adminID).then(lastTx => { - floBlockchainAPI.readData(floGlobals.adminID, { + compactIDB.readData("lastTx", floGlobals.SNStorageID).then(lastTx => { + floBlockchainAPI.readData(floGlobals.SNStorageID, { ignoreOld: lastTx, sendOnly: true, pattern: "SuperNodeStorage" @@ -8996,7 +8972,7 @@ Bitcoin.Util = { promises.push(compactIDB.writeData("applications", content.application[app], app)) } - compactIDB.writeData("lastTx", result.totalTxs, floGlobals.adminID); + compactIDB.writeData("lastTx", result.totalTxs, floGlobals.SNStorageID); Promise.all(promises).then(results => { readDataFromIDB().then(result => { migrateData(newNodes, delNodes, flag).then( @@ -9253,6 +9229,7 @@ Bitcoin.Util = { var sn_msg = { type: "backupData", snID: snID, + time: Data.now(), key: key, value: value } @@ -9273,6 +9250,7 @@ Bitcoin.Util = { sn_msg: { type: "backupData", snID: snID, + time: Data.now(), key: k, value: result[k] } @@ -9337,7 +9315,7 @@ Bitcoin.Util = { floSupernode.connect(sn) .then(node => { node.wsConn.send(dataStr); - console.info('data sent to :' + sn) + console.info('Indicated:' + sn) node.wsConn.close(); }).catch(error => console.error(error)) } @@ -9352,7 +9330,7 @@ Bitcoin.Util = { //request self data from backup requestBackupData(floGlobals.backupNodes[0].floID, myFloID) } else { - let nodeList = floSupernode.kBucket.prevNode(myFloID, floSupernode.kBucket.supernodeKBucket.toArray() + let nodeList = floSupernode.kBucket.prevNode(myFloID, floSupernode.kBucket.SNKB.toArray() .length - 1) for (sn of nodeList) { startBackupStore(sn); @@ -9364,7 +9342,7 @@ Bitcoin.Util = { function nodeBackOnline(snID) { floSupernode.connect(snID).then(node => { - + console.info(`Node online: ${snID}`) var data = { from: myFloID, sn_msg: {} @@ -9391,7 +9369,7 @@ Bitcoin.Util = { if (floGlobals.backupNodes.length < floGlobals.supernodeConfig.backupDepth) { //when less supernodes available, just connect to the revived node - let nxtNodes = floSupernode.kBucket.nextNode(myFloID, floSupernode.kBucket.supernodeKBucket + let nxtNodes = floSupernode.kBucket.nextNode(myFloID, floSupernode.kBucket.SNKB .toArray().length) var index = floGlobals.backupNodes.length for (let i in floGlobals.backupNodes) { @@ -9419,7 +9397,7 @@ Bitcoin.Util = { } } else { //connect to the revived node as backup if needed - let nxtNodes = floSupernode.kBucket.nextNode(myFloID, floSupernode.kBucket.supernodeKBucket + let nxtNodes = floSupernode.kBucket.nextNode(myFloID, floSupernode.kBucket.SNKB .toArray() .length) var index = false @@ -9431,7 +9409,6 @@ Bitcoin.Util = { break; } } - console.info(index) if (index !== false) { initateBackupWebsocket(snID).then(result => { floGlobals.backupNodes.splice(index, 0, @@ -9595,6 +9572,7 @@ Bitcoin.Util = { sn_msg: { type: "backupData", snID: toID, + time: Data.now(), key: k, value: result[k] } @@ -9657,6 +9635,7 @@ Bitcoin.Util = { sn_msg: { type: "backupData", snID: prev, + time: Data.now(), key: k, value: result[k] } From 4a8e8bd2411e4e622e0ea97bf4ec1c4c82e69c93 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 17 Sep 2020 17:07:05 +0530 Subject: [PATCH 3/8] Websocket Improvement * Added gid and uid to websocket (mg_connection) Note: gid = group ID, uid = unique ID. * Replaced old ws msg processing functions with new ones * System functions: - sys_display: Displays in console/terminal - sys_unicast: unicast message to respective ws - sys_broadcast: broadcast message to all ws * Supernode to Users: - broadcast: Sends msg to all ws - groupcast: Sends msg to ws of affilated group - unicast: Sends msg to respective ws *User to Supernode: - forward: Forwards msg from user to supernode ws --- app/supernodeWSS.bin | Bin 176352 -> 176368 bytes util/mongoose.h | 2 + util/websocket.c | 189 ++++++++++++++++++++++--------------------- 3 files changed, 99 insertions(+), 92 deletions(-) diff --git a/app/supernodeWSS.bin b/app/supernodeWSS.bin index d620c0a7016ad1e8382dc0b530ae84739b79a04a..0300454530d24e1d6a39f403c4a0d5dd1011f9d2 100755 GIT binary patch delta 63237 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 a!&=AM`B}>TBfjFW)*U}=bmFkq?*9Non7w@f delta 63496 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 M{*cD$A}#d)0OrH()&Kwi diff --git a/util/mongoose.h b/util/mongoose.h index 0f85b18..4c5d0fb 100644 --- a/util/mongoose.h +++ b/util/mongoose.h @@ -3914,6 +3914,8 @@ struct mg_mgr { * Mongoose connection. */ struct mg_connection { + char gid[35], uid[6]; /* identifiers for groupcast and unicast*/ + struct mg_connection *next, *prev; /* mg_mgr::active_connections linkage */ struct mg_connection *listener; /* Set only for accept()-ed connections */ struct mg_mgr *mgr; /* Pointer to containing manager */ diff --git a/util/websocket.c b/util/websocket.c index db6f7b2..d501e86 100644 --- a/util/websocket.c +++ b/util/websocket.c @@ -8,121 +8,124 @@ static sig_atomic_t s_signal_received = 0; static struct mg_serve_http_opts s_http_server_opts; static char *s_http_port, *server_pwd; -static struct mg_connection *supernode_client = NULL; +static struct mg_connection *sn_c = NULL; //supernode_js_connection static void signal_handler(int sig_num) { signal(sig_num, signal_handler); // Reinstantiate signal handler s_signal_received = sig_num; } -//Display Message in console/terminal -static void display(struct mg_connection *nc, char type[25]){ +static int is_websocket(const struct mg_connection *nc) { + return nc->flags & MG_F_IS_WEBSOCKET; +} + +//System_display [wss => console] +static void sys_display(struct mg_connection *nc, char type[25]){ char addr[32]; mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr), MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); printf("%s\t%s\n", addr, type); } -//Broadcast incoming message (from nc to all) -static void broadcast(struct mg_connection *nc, const struct mg_str msg) { - char addr[32]; - mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr), MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); - printf("%s\tBroadcast\t[%d]\n", addr, (int)msg.len); +//System_unicast [wss => nc] +static void sys_unicast(struct mg_connection *nc, const struct mg_str msg) { + if (nc != NULL) + mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len); +} + +//System_broadcast [wss => all] +static void sys_broadcast(struct mg_connection *nc, const struct mg_str msg) { struct mg_connection *c; for (c = mg_next(nc->mgr, NULL); c != NULL; c = mg_next(nc->mgr, c)) { - if (c == nc) continue; /* Don't send to the sender. */ - mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, msg.len); + if (c == nc) continue; + mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len); } } -//Unicast message (to nc) -static void unicast(struct mg_connection *nc, const struct mg_str msg) { - char addr[32]; - mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr), MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); - printf("%s\tUnicast\t[%d]\n", addr, (int)msg.len); - if(nc != NULL) - mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT, msg.p, msg.len); -} - -//Forward message (from nc to supernode) -static void unicast_forward(struct mg_connection *nc, const struct mg_str msg) { - char addr[32]; - mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr), MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); - printf("%s\tForward\t[%d]\n", addr, (int)msg.len); - if(supernode_client != NULL) - mg_send_websocket_frame(supernode_client, WEBSOCKET_OP_TEXT, msg.p, msg.len); - else - printf("SuperNode client is offline!\n"); -} - -//Request message (from nc to supernode) -static void unicast_request(struct mg_connection *nc, const struct mg_str msg) { - if(supernode_client == NULL){ - printf("SuperNode client is offline!\n"); - return; - } - char addr[32], buf[500]; - mg_sock_addr_to_str(&nc->sa, addr, sizeof(addr), MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); - printf("%s\tRequest\t[%d]\n", addr, (int)msg.len-1); - snprintf(buf, sizeof(buf), "?%s %.*s", addr, (int) msg.len-1, msg.p+1); - mg_send_websocket_frame(supernode_client, WEBSOCKET_OP_TEXT, buf, strlen(buf)); -} - -//Reply message (from supernode) -static void unicast_reply(const struct mg_str msg) { - if(supernode_client == NULL){ - printf("SuperNode client is offline!\n"); - return; - } - //Get receiver address from msg - char receiverAddr[32]; - int index = (int)(strchr(msg.p, ' ') - msg.p) + 1; - snprintf(receiverAddr, sizeof(receiverAddr), "%.*s", index - 1, msg.p); - printf("%s\tReply\t[%d]\n", receiverAddr, (int)msg.len - index); - //send msg to receiver +//Broadcast incoming message [sn_c => all] +static void broadcast(const struct mg_str msg) { + printf("\nBROADCAST\t[%d]", (int)msg.len-1); struct mg_connection *c; - for (c = mg_next(supernode_client->mgr, NULL); c != NULL; c = mg_next(supernode_client->mgr, c)) { - char addr[32]; - mg_sock_addr_to_str(&c->sa, addr, sizeof(addr), MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT); - if (!strcmp(receiverAddr,addr)) - mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p + index, msg.len - index); + for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) { + if (c == sn_c) continue; + mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len); } } +//Groupcast incoming message [sn_c => gid] +static void groupcast(const struct mg_str msg) { + char gid[35]; + snprintf(gid, sizeof(gid), "%.*s", 34, &msg.p[1]); + printf("\nGROUPCAST\t[%d]", (int)msg.len - 36); + struct mg_connection *c; + for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) { + if (c == sn_c) continue; + else if (!strcmp(c->gid, gid)) + mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len); + } +} + +//Unicast incoming message [sn_c => uid-gid] +static void unicast(const struct mg_str msg) { + char uid[6], gid[35]; + snprintf(uid, sizeof(uid), "%.*s", 5, &msg.p[1]); + snprintf(gid, sizeof(gid), "%.*s", 34, &msg.p[7]); + printf("\nUNICAST\t[%d]", (int)msg.len - 42); + struct mg_connection *c; + for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) { + if (c == sn_c) continue; + else if (!strcmp(c->gid, gid) && !strcmp(c->uid, uid)) + mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len); + } +} + +//Forward incoming message [user => sn_c] +static void forward(const struct mg_str msg) { + printf("\nFORWARD\t[%d]", (int)msg.len - 41); + if(sn_c != NULL) + mg_send_websocket_frame(sn_c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len); + else + printf("\nWARNING: No supernode connected"); +} + static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) { switch (ev) { case MG_EV_WEBSOCKET_HANDSHAKE_DONE: { - /*New websocket connection*/ - display(nc, "+Connected+"); - if(supernode_client!=NULL) - unicast(nc, mg_mk_str("$+")); + /*New Websocket Connection*/ + sys_display(nc, "+Connected+"); + if (sn_c != NULL) + sys_unicast(nc, mg_mk_str("$+")); else - unicast(nc, mg_mk_str("$-")); + sys_unicast(nc, mg_mk_str("$-")); break; } case MG_EV_WEBSOCKET_FRAME: { + /* New Websocket Message*/ struct websocket_message *wm = (struct websocket_message *) ev_data; - /* New websocket message*/ struct mg_str d = {(char *) wm->data, wm->size}; - if (d.p[0] == '$'){ - char pass[100]; - snprintf(pass, sizeof(pass), "%.*s",(int)d.len-1, &d.p[1]); - if(!strcmp(pass,server_pwd)){ - if(supernode_client!=NULL) - unicast(supernode_client,mg_mk_str("$Another login is encountered! Please close/refresh this window")); - else - broadcast(nc, mg_mk_str("$+")); - supernode_client = nc; - unicast(supernode_client,mg_mk_str("$Access Granted!")); - display(nc, "*Became SuperNode*"); - }else - unicast(nc,mg_mk_str("$Access Denied!")); + if (d.p[0] == '$') { + if (sn_c == NULL) { + char pass[100]; + snprintf(pass, sizeof(pass), "%.*s",(int)d.len-1, &d.p[1]); + if (!strcmp(pass, server_pwd)) { + sn_c = nc; + sys_unicast(nc, mg_mk_str("$Access Granted")); + sys_display(nc, "*Supernode LoggedIn*"); + sys_broadcast(nc, mg_mk_str("$+")); + } else + sys_unicast(nc, mg_mk_str("$Access Denied")); + } else + sys_unicast(nc, mg_mk_str("$Access Locked")); + } else if (nc == sn_c) { + switch (d.p[0]) { + case '@': unicast(d); break; + case '#': groupcast(d); break; + default: broadcast(d); + } + } else { + snprintf(nc->gid, sizeof(nc->gid), "%.*s", 34, &d.p[0]); + snprintf(nc->uid, sizeof(nc->uid), "%.*s", 5, &d.p[35]); + forward(d); } - else if (d.p[0] == '?') - unicast_request(nc,d); - else if (nc == supernode_client) - unicast_reply(d); - else - unicast_forward(nc,d); break; } case MG_EV_HTTP_REQUEST: { @@ -130,13 +133,15 @@ static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) { break; } case MG_EV_CLOSE: { - /* Disconnect websocket*/ - if(nc == supernode_client){ - supernode_client = NULL; - display(nc,"!SuperNode Disconnected!"); - broadcast(nc, mg_mk_str("$-")); - }else - display(nc, "-Disconnected-"); + /* Websocket Disconnected */ + if (is_websocket(nc)) { + if(nc == sn_c) { + sn_c = NULL; + sys_display(nc,"!Supernode LoggedOut!"); + sys_broadcast(nc, mg_mk_str("$-")); + } else + sys_display(nc, "-Disconnected-"); + } break; } } From fd592da8ed219973d03eebb4715ffa0340a8bfbb Mon Sep 17 00:00:00 2001 From: sairajzero Date: Thu, 17 Sep 2020 17:45:20 +0530 Subject: [PATCH 4/8] Supernode Update Changes: - Changes for wss update Added: - Added time to data - errorFeedback: (if on)Feedback if any error in processing data from users. - live-request: When a new data is stored, sends it to all respective requestors of that floID. - deleteRequest: Users will now be able to delete the data received by them from cloud. (Note: user must be the receiverID of the data; sign verification ll be done). Improvements: - Improved data processing from wss. - Improved data-signature format (new format: "receiverID|time|application|type|message|comment"). - Time in data must be within the allowed delayDelta. - Feedback vectorclock of stored data to the sender. - Dedicated disk will now be applied to authorised apps instead of diskList (removed floGlobals.diskList) . Authorising apps will automatically create a new disk for the app and imports all data of the app from defaultDisk. . Unauthorising apps will automatically exports all data from app disk to defaultDisk and deletes the app disk. (Caution: Unauthorising an app will cause diskCleanUp to delete all data stored before deleteDelay). - Improved autoDeleteStoredData to diskCleanUp. For defaultDisk: deletes all data before deleteDelay, For authorised apps deletes data before deleteDelay sent 'from non-subAdmins' and/or 'to non-admin'. Bug fixes: - Minor bug fixes --- app/index.html | 603 ++++++++++++++++++++++++++++++------------------- 1 file changed, 375 insertions(+), 228 deletions(-) diff --git a/app/index.html b/app/index.html index 30db3a9..20dc343 100644 --- a/app/index.html +++ b/app/index.html @@ -31,14 +31,19 @@ //Required for Supernode operations supernodes: {}, //each supnernode must be stored as floID : {uri:,pubKey:} - diskList : ["General"], defaultDisk : "General", - applicationList:{}, + appList:{}, appSubAdmins:{}, serveList : [], storedList : [], - supernodeConfig : {}, - backupNodes : [] + backupNodes : [], + supernodeConfig : {} + /* List of supernode configurations (all blockchain controlled by SNStorageID) + backupDepth - (Interger) Number of backup nodes + refreshDelay - (Interger) Count of requests for triggering read-blockchain and autodelete + deleteDelay - (Interger) Maximum number of duration (milliseconds) an unauthorised data is stored + errorFeedback - (Boolean) Send error (if any) feedback to the requestor + */ } @@ -7532,7 +7537,7 @@ Bitcoin.Util = { }, //generate a random String within length (options : alphaNumeric chars only) - randString: function (length, alphaNumeric = false) { + randString: function (length, alphaNumeric = true) { var result = ''; if (alphaNumeric) var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -7630,20 +7635,18 @@ Bitcoin.Util = { if (key.priv == null) return null; key.setCompressed(true); - var pubkeyHex = key.getPubKeyHex(); - return pubkeyHex; + return key.getPubKeyHex(); }, //Returns flo-ID from public-key or private-key getFloID: function (keyHex) { - if(!pubkeyHex) + if(!keyHex) return null; try { - var key = new Bitcoin.ECKey(privateKeyHex); + var key = new Bitcoin.ECKey(keyHex); if (key.priv == null) - key.setPub(pubkeyHex); - var floID = key.getBitcoinAddress(); - return floID; + key.setPub(keyHex); + return key.getBitcoinAddress(); } catch (e) { return null; } @@ -8196,7 +8199,7 @@ Bitcoin.Util = { return KB.distance(KB.localNodeId, decodedId); }, - closest: function (floID, n, KB) { + closestOf: function (floID, n, KB) { let decodedId = this.decodeID(floID); return KB.closest(flo_addr, n) }, @@ -8218,7 +8221,7 @@ Bitcoin.Util = { let superNodeList = Object.keys(floGlobals.supernodes); let masterID = floGlobals.SNStorageID; this.SNKB = this.util.constructKB(superNodeList, masterID); - this.SNCO = superNodeList.map(sn => [this.util.distance(sn, this.SNKB), sn]) + this.SNCO = superNodeList.map(sn => [this.util.distanceOf(sn, this.SNKB), sn]) .sort((a, b) => a[0] - b[0]) .map(a => a[1]) console.log(this.SNCO) @@ -8285,7 +8288,7 @@ Bitcoin.Util = { }, closestNode: function (id, n = 1) { - let cNodes = this.util.closest(id, n).map(k => k.floID) + let cNodes = this.util.closestOf(id, n).map(k => k.floID) return (n == 1 ? cNodes[0] : cNodes) } }, @@ -8297,10 +8300,7 @@ Bitcoin.Util = { var wsConn = new WebSocket("wss://" + floGlobals.supernodes[snID].uri + "/ws"); wsConn.onmessage = (evt) => { if (evt.data == '$+') - resolve({ - snID, - wsConn - }) + resolve(wsConn) else if (evt.data == '$-') { wsConn.close(); reject(`${snID} is not active`) @@ -8328,33 +8328,6 @@ Bitcoin.Util = { }) }, - //Sends data to the supernode - sendData: function (data, floID) { - return new Promise((resolve, reject) => { - this.connectActive(floID).then(node => { - node.wsConn.send(data); - node.wsConn.close(); - resolve(`Data sent to supernode : ${node.snID}`) - }).catch(error => reject(error)); - }); - }, - - //Request data from supernode - requestData: function (request, floID) { - return new Promise((resolve, reject) => { - this.connectActive(floID).then(node => { - node.wsConn.onmessage = (evt) => { - if (evt.data[0] != '$') - resolve(evt.data); - else - reject(evt.data) - node.wsConn.close(); - } - node.wsConn.send(`?${request}`) - }).catch(error => reject(error)); - }); - }, - //Supernode initate (call this function only when client is authorized as supernode) initSupernode: function (pwd, floID) { return new Promise((resolve, reject) => { @@ -8375,13 +8348,13 @@ Bitcoin.Util = { if (evt.data[0] == '$') { console.log('Admin Message :', evt.data); if (evt.data == '$Access Granted!') - resolve("Access Granted! Initiated Supernode client"); + resolve("Access Granted: Initiated Supernode"); else if (evt.data == '$Access Denied!') - reject("Access Denied! Failed to initiate Supernode client"); - } else if (evt.data[0] == '?') - processIncomingRequest(evt.data.substr(1)); - else - processIncomingData(evt.data) + reject("Access Denied: Failed to initiate Supernode"); + else if (evt.data == '$Access Locked') + reject("Access Locked: Another instance of Supernode is active") + } else + processIncomingData(evt.data); }; this.supernodeClientWS.onerror = (evt) => { console.error('Error! Unable to connect supernode websocket!'); @@ -8395,84 +8368,126 @@ Bitcoin.Util = { } //Process incoming request from clients - function processIncomingRequest(request) { - console.log('Request :', request); + function processIncomingData(data) { + console.log(data); try { - request = request.split(" "); - requestor = request.shift(); - request = JSON.parse(request.join(" ")); - let closeNode = floSupernode.kBucket.closestNode(request.receiverID) - if (floGlobals.serveList.includes(closeNode)) { - var filterOptions = { - lowerKey: request.lowerVectorClock, - upperKey: request.upperVectorClock, - lastOnly: request.mostRecent, - atKey: request.atVectorClock, - patternEval: (k, v) => { - return (v.application == request.application && v.receiverID == request.receiverID && (! - request.comment || v.comment == request.comment) && (!request.type || v - .type == request.type) && (!request.senderIDs || request.senderIDs.includes( - v.senderID))) - } - } - compactIDB.searchData(floGlobals.diskList.includes(request.application) ? request.application : - floGlobals.defaultDisk, filterOptions, `SN_${closeNode}`) - .then(result => floSupernode.supernodeClientWS.send(`${requestor} ${JSON.stringify(result)}`)) - .catch(error => console.error(error)) + let gid = evt.data.substring(0, 34); + let uid = evt.data.substring(35, 40); + let data = JSON.parse(evt.data.substring(41)); + if (data.from in floGlobals.supernodes && data.sn_msg) + processTaskFromSupernode(gid, uid, data); + else { + let curTime = Date.now() + if(data.time > curTime + floGlobals.supernodeConfig.delayDelta || + data.time < curTime + floGlobals.supernodeConfig.delayDelta) + throw Error("Time deviation longer than allowed delay"); + else if (data.request) + processRequestFromUser(gid, uid, data); + else if (data.message) + processDataFromUser(gid, uid, data); + else if (data.delete) + processDeleteFromUser(gid, uid, data); } } catch (error) { - console.log(error.message) + console.error(error) + if(floGlobals.supernodeConfig.errorFeedback) + floSupernode.supernodeClientWS.send(`@${uid}#${gid}:${error}`) } } - //Process Incoming data - function processIncomingData(data) { - console.log('Data :', data); - try { - data = JSON.parse(data) - if (data.from in floGlobals.supernodes && data.sn_msg) - processDataFromSupernode(data); - else { //Serving Users - - //Delete request from receiver - if (data.delete) { - let closeNode = floSupernode.kBucket.closestNode(data.from); - if (floGlobals.serveList.includes(closeNode) && - data.senderID == floCrypto.getFloIDfromPubkeyHex(data.pubKey) && - floCrypto.verifySign(JSON.stringify(data.delete), data.sign, data.pubKey)) { - //start the deletion process - - //indicate backup nodes to delete data - } - } else { - let closeNode = floSupernode.kBucket.closestNode(data.receiverID) - if (floGlobals.serveList.includes(closeNode) && - data.senderID == floCrypto.getFloIDfromPubkeyHex(data.pubKey) && - floCrypto.verifySign(JSON.stringify(data.message), data.sign, data.pubKey)) { - var key = `${Date.now()}_${data.senderID}` - var value = { - senderID: data.senderID, - receiverID: data.receiverID, - pubKey: data.pubKey, - message: data.message, - sign: data.sign, - application: data.application, - type: data.type, - comment: data.comment - } - compactIDB.addData(floGlobals.diskList.includes(value.application) ? value.application : - floGlobals - .defaultDisk, value, key, `SN_${closeNode}`) - sendBackupData(key, value, closeNode); - } - } - } - - if ((refreshData.countdown--) <= 0) - refreshData(); - } catch (error) { - console.log(error.message); + //Process request from users + function processRequestFromUser(gid, uid, data) { + let request = data.request; + if (!floCrypto.validateAddr(request.receiverID)) + throw Error("Invalid receiverID") + let closeNode = floSupernode.kBucket.closestNode(request.receiverID) + if (!floGlobals.serveList.includes(closeNode)) + throw Error("Incorrect Supernode") + var filterOptions = { + lowerKey: request.lowerVectorClock, + upperKey: request.upperVectorClock, + lastOnly: request.mostRecent, + atKey: request.atVectorClock } + filterOptions.patternEval = ( + v.application == request.application && + v.receiverID == request.receiverID && + (!request.comment || v.comment == request.comment) && + (!request.type || v.type == request.type) && + (!request.senderIDs || request.senderIDs.includes(v.senderID)) + ) + compactIDB.searchData(request.application in floGlobals.appList ? request.application : + floGlobals.defaultDisk, filterOptions, `SN_${closeNode}`) + .then(result => floSupernode.supernodeClientWS.send(`@${uid}#${gid}:${JSON.stringify(result)}`)) + .catch(error => { + throw Error("Invalid request") + }) + } + + //Process data from users + function processDataFromUser(gid, uid, data) { + if (!floCrypto.validateAddr(data.receiverID)) + throw Error("Invalid receiverID") + let closeNode = floSupernode.kBucket.closestNode(data.receiverID) + if (!floGlobals.serveList.includes(closeNode)) + throw Error("Incorrect Supernode") + if (data.senderID !== floCrypto.getFloID(data.pubKey)) + throw Error("Invalid senderID/pubKey") + let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"] + .map(d => data[d]).join("|") + if (!floCrypto.verifySign(hashcontent, data.sign, data.pubKey)) + throw Error("Invalid signature") + var key = `${Date.now()}_${data.senderID}` + var value = { + senderID: data.senderID, + receiverID: data.receiverID, + pubKey: data.pubKey, + time: data.time, + message: data.message, + sign: data.sign, + application: data.application, + type: data.type, + comment: data.comment + } + compactIDB.addData(value.application in floGlobals.appList ? value.application : + floGlobals.defaultDisk, value, key, `SN_${closeNode}`).then(result => { + floSupernode.supernodeClientWS.send(`@${uid}#${gid}:${JSON.stringify({vectorClock: key})}`) + floSupernode.supernodeClientWS.send(`#${data.receiverID}:${JSON.stringify({[key]: value})}`) + sendBackupData(key, value, closeNode); + }).catch(error => { + throw Error("Invalid Data") + }) + } + + function processDeleteFromUser(gid, uid, data) { + if (!floCrypto.validateAddr(data.requestorID)) + throw Error("Invalid requestorID") + let closeNode = floSupernode.kBucket.closestNode(data.requestorID) + if (!floGlobals.serveList.includes(closeNode)) + throw Error("Incorrect Supernode") + if (data.requestorID !== floCrypto.getFloID(data.pubKey)) + throw Error("Invalid senderID/pubKey") + let hashcontent = ["time", "application", "delete"] + .map(d => data[d]).join("|") + throw Error("Invalid signature") + let disk = data.application in floGlobals.appList ? data.application : floGlobals.defaultDisk; + const deleteData = v => new Promise((res, rej) => { + compactIDB.readData(disk, vc, `SN_${closeNode}`).then(result => { + if(result.receiverID === data.requestorID) + compactIDB.removeData(disk, vc, `SN_${closeNode}`) + .then(r => res(true)).catch(e => res(false)) + else + res(false) + }).catch(e => res(false)) + }) + Promise.all(data.delete.map(vc => deleteData(vc))).then(result => { + floSupernode.supernodeClientWS.send(`@${uid}#${gid}:${JSON.stringify(result)}`) + let vectorClocks = [] + for(let i in result) + if(result[i]) vectorClocks.push(data.delete[i]) + sendBackupDelete(data.application, vectorClocks, closeNode); + }) + } + From b4f3e83b3cd36437acef17b4c07666470f80676c Mon Sep 17 00:00:00 2001 From: sairajzero Date: Sat, 19 Sep 2020 17:28:19 +0530 Subject: [PATCH 5/8] log alignment fixed logs not aligned propoerly --- app/supernodeWSS.bin | Bin 176368 -> 176392 bytes util/websocket.c | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/supernodeWSS.bin b/app/supernodeWSS.bin index 0300454530d24e1d6a39f403c4a0d5dd1011f9d2..8342223cd1a52267de86cfa528fc801d34b9a59e 100755 GIT binary patch delta 44176 zcmd3Pd0dU@8~3^I7Niu~X;q{wS)vG`II?9YTL{@>YQ}z|G9?iYb7Nv+EZG||^~)Bb z7)F+vF*Al3GgBCo!AxV!dEW2+EXO(L_nY^>_w#xCd^+Fj`d-()uh0EF=RCC!Z89F& z*JdDnNml)mPeQUdRH2Fde3<#K=kGd^C4DAAkzJP~Gm%2hdijn>T2{0liij2R!K@MF#RlAE%BoEe{ZU*=QHc(rLVbY=#%ZF0@_j*J;$Y zsWLTaQj|8UHsjT%sWs!(rl~XI)uz$(Gwr-`)1dvm%?uN(H0Yc6A?-f46jQ>gDW6by(g&(W(a``oyKNUlQYLMp*&EE>2XvSMV zlsjpP8Q)0ZGtBr_3cuBi*99qtJTpUAg)cDUyDNOL8Q)*wE6n(T3SVi)kCJ(vrpnAP zP6<$L#?MmtIx~Kr!fUjqO}Rqh9nAPu3hyfMsDE|%eWVxy%mQpz_+T^sQ-zN(Tj_{C;?jlyS`@v4808UHK%rT&*^W>Bjr-;A$S z5-2v~UnzX48DH;_+(|3V_=XC9SL-YHf3*qx6hpOHfB=QBHRGccUNgY7?FT4)eKUT% z!n+!HIepD!#o%o)$m1_b;e*Zi#R?y4#;;X)tr?%K@G)lmZk=L?Gc)W{_yjZlqQWmW ze9TdL8jBls#cV+(F{=cJQ zs4@%CL*Z-9_&y3>XT}d!`1;C~m)f_~_#I4mwEu7=fVYW3qZzI60cQLRg%363XDfV! z8K0!^F=qT?&h!2^*36Km1V}LBGZa42jAsg;V#a^0@EK5oa!dIE`X>Wx^p6y7Au*=g$H&j`{f@4g_sKcLfq@n-9N0{*|Ud#SkGk$EH z%*UAV-ftvcr-?H&SSh(qFyk92e4-g|r|^r-_{IvKV#Yfue1^oMfz|4DQw%v~0X!6b zs~PX3@OfsuzryF6@vRlUz?ct5`?ggK#l{Bk9TdLQj4x1mVWGE6kBXsk!|}t9cKBES zDvkKADha%6#E%wMc)O5T;RnPs*~mvTOpenET$~YKH%R8|fs>n6F$x`gT-@pperL5X z3gsY7@o^DceQfP`eCnUKuo0{lERRk73l`3UT@1E?N&zE;7hvhx!K_(9+hfr%C})|6s?qv~`OxVylc&?JcZm>EhPM z$XaV;?JFE<>0%dYWYq{ATA}`tPCp|S^$*sym93rDh(-N_)q;&SVp0EK7lR#S#G?Me z=7Sw##G=0kpudBSF=A2gVCx6i+6^~iQSV?w18m(!8nG$L*ykf|lrS;C#cqs|75yGz zwj!o+Ml9;RHR>Jg1S1yp4z@19RyWCr)uukHug$XR(b89=O)pKNVx=~oHIajx^ps-{%Z4|2UJ*9#BG6s zr@9)Aq(^hzi|e)j@EYjDb#Ja$NqRS~`*6Kd(mQgUPdPQEk{-zQ7F;iobYHIP_{>+6 zClT(P@aKAtq&ML@p95=BB;AJVt+<{j>6GhyHmr%0^fx}xTXQ`|(tqbVpBHN)B>f53 zgSZ|n>5ri668IEZ<1G>2afh~CcaZd3T<42&jYiV1a-C0_HMMW0KEQQ;a$i#==_Op} z(`QYkq#x&cN3NGj`a!PqiL|Cb(s%pd3N4{CC-Nj>J9pr7YE6!$Z{m6vuBS-)TCVfm zKuw~gujV?RWozOjeJR(&xE>?vi@46`+nNYTpUw5|To0D?sXjQNN#Ij&jkiR^a)%yV zcaZeqT<4Q;jYiU=x!#NGwSP-}fa?)luafj`T<^{GN=fg?bv_~2luCLa*ZXq4K+=76 zoZxeEO`b%!b3KadIg;Ll>wI>uNs)9LuJ`A9qNG!<^Le@^PSW3aLm$BP7)k$~>wLzp ziIDUsTp!5wU`f|K;sl?(YrG}mJFXArx`U+O;yRzkYc!I6mFq*fUi(Js16=15dQFw2 zmvDU;*DEFcIM?~qUQ;UR2f03i>jhlb`S0cgpXF=vB!}%>AI0??N#DeEej!nlBI#?n z&gcD_L`h%G^|4%!GlZX!16}&-@aurgo0GJekF=?eYhy+Z%C`FMAkk>E?WRf4BPbM; zr06sRk-W0^LPKb?d-O(~j?!jZg#oo;b=u6IlH9N?_zL6O2Q(m@%no_uEv#$rMQgl- zBklbIUm&<9xxF^E$2l)Zqj(ToZB~!(pjF$UnAKW*T(-L6Exc{tvU%o;w^>nd5zAAs zndQ~n(YHWkTJ7@oWsPo-F`et?Ad^|6O10ThmDf*JX zEdBZTPqM0R`9@^hZjfG~_K>T4}h zPT)RcDoLk~8}rrp7yl)SDDHXWoqM0|i_-Zqw>Yhf!`l**MJ6%b$F581*bi&d4USq9o6He{gU z=@ew(0=I@mRV0NVk_v71DAbcwq)cQ#y)@a2$E}@Kf!q{mvj#;~oaM_iFUS1xYL0bE z3h=2=LVuAVr=d+308HsBVq)qTAW#p27fp;#y3fTRnw*)jEa z>kY`P{10CQwAoGvxU)$eWXR5_gFvYca^WG@!6eCIwco?64qksK<*)SVa@0X)Gj0xx zDou((lISdkN{Fcw*>AUqt3?O|o7=?0$30dpb&wJ#N?ody`UAwuw6}>j9r=e)FyAXC zsxGyc1J!)vQDnxTI6LHXhRh*8HdDd(Q5^Su6BMcvirBm ze7_#hcnf@s9*iK?N=&Wc*p55qVCH0rcZj2t8|IptZ5F)RderBpnW~A@vZHs%5R2)} ze3^KM1Vvl#L9NCcH^EP@c=S%r&5+UTs1&7)zrB|F#X@aX`dAD*ZKl;f?%bhAFe0d4 z7ojBG(n-pk?2{eh1S8M#20kdVd(?gk-{GpSlP7#@##1SqzjIS-gXuT|)O3_hcezQU zn(TQGt!cnbSysK>eOZgINf&aK#ePjZtT*!*cd*G{6IXJRee^Z)aayw5yxWX@DY9!{ z6JO^(++@=Id|sk|rhHADbfYn;xESK>hRtk9ed+U!E0>>1pZBHD zO6l{0^m#)1+$Vi*l|I)>pUb7s`O;^c^f_GmjKrr>-4jE6P;;rzQfgLdcovjOFRz}4 zo{jD^4~6FuYr`Zio|8 zdziVmE%+QhbgPZ0$h9v-*6l6{*8SpwWV533FuOUR8&q*MJ7f;J_c^}7t#&~jf{5h= zT1iuF&510l8_vF(|M2{kpi(u4c`;wM-u&OzwNhkJr3Ux+byIi=?WRh}@=@#bn0L9% zFL~CNt%NZXGZ&w|pij98J=^M+JYF)ZqeuhVeOasET9%a?k4PG;SHR2D$E8}-Ok-^} z(yc&Vc}$lvMzX91Ir|1^Q2jq~8>c;|Y2+|`+D3Ibsw+yTTH)s9riHD8r%{^qTv*@n z9{r)QFt<}{dc3hPG{jXn25~`SVI#!fA+~8;=+W7lv}xnutI3Lbb-s>jw`xhH{pT9{ zv!#gqE;)*sMr5ULGAZ6^3{Z;&&9x5i7sZf7b>Q`WE-i1Yrc|(Z#uvD z;$imxBCHPmffiQ_Q^JDjnqLGVOy~L9RtjRDR)yHJtiFbtnVuw#M5}LYh2UkE%tnn0H2PRx)qA&T8ZR z`dFCI{S#{0Q23|2OHkimr2hMu$Ci2d9CkdK>RvxdT&2X7S4aFTise>6*B5$)y9Yl1 zQ53IScS7v zk;jC+J@(NPg@uEAHnyez`9_F~cB4DL5tc=drv1JVZUdfwEjYY(5+Vi;qienrgn^MB z5nrL-y78V+T|e^zQm!?O8~?9_R|B`u^>+%h27T^Gzdk4QozjusI$QX`6bC|c&Iy@Q zS5w<_g?7{YNZX%JqjcF(wI%g=b>SZcFR!iy`(G>i(^JBv_=~Q8oRUHmltdVZs5&Kt zP4}dwr-gCTo6%2C7rs9|f>>?*59){A+(Dd$n={7KQ$<3%nNw-kQ^Ka1Deg|`L{riZ zlQO1|Z8#Iaw6z0JeWqAwli=R}VH(WsW%G2|{2n)F7W4Lqx{FDvK9=;#_r0)5J;9xW zW#>@Yc`SFHP%LC8yhmG=2wP^iqR)$kv$NfV$XUHxtj-{s-|feppJCt5Y9-|N8vd@e zZ&RwxjH-Rw;&*!oyIC1R{;W~-YOx^B>Ofhs&~f$@y17`2XM7Rj`Gl@56eiCZLpu}- z#dF3|mm;Cb+%c|4PfFRVllz}qp<7P~tL6sN_X~x>xkG5nLcwBQXX_JByFZ_9-7o~j;7uHL< zN9=8}L(8(H+G&-T?8Ml@-;=}Z(VAU_!7G9Z{biTnGpAWS42JURy}N|8m3?VSo^WMl zPa1PsXrOCFTU{>fsDtC{U4`)}YY82a}PN5||ChOZ8!b8>~1t3BwoPlO$-V`)LI z@Ym{4+J$T0!id!F)Gb%YNp+>47I1z$AvCytSzVw z6TC9J(~13s37MX>hqiEe<_1C!M+tUW+J*`JumQsU&nmv35SQgeWBLiJvi_vKB88>v z`uYA7saXGlfT3I6JM*vJnb?Bf)S}a8_h=O*JX%*`d1#X;3g_oGEiBBQLul)bd_ERB zZE&QCy@dfA0_fR3!h0KbG^)2zZW>-8U#w@tJ||8>r<|XueuL0#V;B0zdSS%IR&>V( zVfn_6ZcB3brG_geGRG{d#6llUW|S+kxu{Sm-`IgJ$PwObJQg{G$B3Np?wA=9T+$UY z272?KJ&5Loji!&*2=%u5 z(COWTz^$X(vJ5GW(i!C%%9nxSa_RN}q>2)0caZrdF`E_DKy&_uB;+gY%9x!s%_D=#Qboi)|h3OG2eIda$!#pBq9y?IQHc^`;L~g@jxm+OD&ZmFwBG z-5OD}FTz|DRh1QCiEMkM3a4|Mc{T5%bX*q*a>qpvQF?8?E`@*OuBAR*JE+}cE#gXJD>Q%|J$0 z!~|p@YdP&!XcdrLs+P3i2-UfuG zE)g#8^QJdj3)TBPBW^BJBfTq^s&ui`&r?@CrpXO33a$ky#VdzkUc90pq2K<)^iqKE zYQJyO^GnsZs^tt8e86j{;jus^ii3f|(0nI)Kd^8{ejiFdNya4PDtI1sWD~{^JKFhu z;irQU)Hk`X)ge?y>sG?TBThop;Um;PNqBX58U4o}j6nM?Qo{MryNiV0N5km#g~IZq zV`-tE@b%Fpw7y^Auwzq64@+;g(g&j7VT0$EmHrCHmQpQ!;j50()$(Za<`;0-b3}P# z_j$!vs4W;tEfxs&EouMxLdWB+=+3#q_~Y*M);!^Z<0<<`oeGy0DIofBgQ13(;eb!94exez*_7a|)Xld1Tj$zRZI+^U@(oD_pq}5V} zld(KSRq=&uWVmqVq?<)pIAQi)#8r54GJyWzQP}MB`Gl^YC1e$KrRfPmSy4DGnpH@P z=UUKl@j~J$585qW*l=nZ9hD%wIyH$-pDBzy-GO#=6h1s1O0_P+`O`hAcBVkiB+))o zg%8g3rW>XTC1+~s^(n&PvlFP-RKe!lK)QRfFy`D&dVH$z=ecgwIZo(Qs-@TBgcYUD z=&?9qdugQvNhR9WhZGBJ5j^-gTxQJ-NYsknT=tyWF z_<~uJf{&7H*{8p~TKx6zOQA#Ui=uAHqjumFsc&UP-9g9iIZ5iHXse3MZ{<$ErHOF# zd^8<8NqBR9B>iiO5OX1*Q?H3Un&jbGqn>HA2H(XF99I+wKNDM2@ZC~Z+@VQLL7(N> zfZ_RM;m`#i$3w6`bxB?EN=#}DD-5$H2~`)G(}5EO{e{Ui)=rpwaX8)QAQWEQLMKlY zqRYOcLPMeDr2*92URZd^ix$NSn=bXGzT<>%E(LacKbB`Wz4`%G0~-gaLO!)o3;SfD zR9JM*d$3ELb6q2$)8$;ccpR3uFnXkcU|a4%58DYr*CYm=-N|diw>ud^>ul|QLs_~ci zNqkH6lfMvt?F#KPs<7VmL~2O(4q?5z=oGd0cs?HiJ?7R`~kUfI8{M*zO zZ#mg(sPN>*{3hoRWJY;ZCDok6W?WbONsKVN(w`QO5H?o&cts88$=jzdf-QB$36kuR z6;&o};gLYaFyUrpGnzGAcwXsFdyNomZg!;0h6_D!wwU-466XOgpuy1o#aZZFIH<}V zjJ*$U+CLQgZrJ$=mf|uPCt=*|qNJNDn~~_dpnne&&fffro)|1_xz*DnatKPfqFR&W zg054AL|$P9kt6wQkWhQe$z>5r_+LBcwu6P{w`b7{gV-udJpcd6?QrT4FCs7M)@_Wa z5uSc^&L&fj0w70Qw1p?WPNf!_w;G|_x1BAn{jCuee%s8V;BQUghHtG2Jsu^n?{+zR zy^;O%>(G_>bx(Fkqc<91$oH-mum92rv%XKF(fx%7-|J{iUm@<^ev3AB8o}m={uXw? zkv|NyKmVr`fjm+D9VHz7VK3brA&k1;iq44;R^AV{*zt!(ICHdweE=w;Igp98+B;7jBx0MU9Yp?^~kH;fpE#87Q1#=Ucznf zbZ@R_sygHq;lkWUztGLyg;75S)7|01hd=J5?qh^jkK5Cl(Zblr9qEzL!n()7&RL^< z(BP=WOkAV$gFS>E(_OgrxD5>(z3C?}TEDyC^OGls-aiGv4*; zzN_%GCW9_|E+jwcOCN*^B~PO1jb8=pr&_x0S0U!98(saYFz0C${h)K<@uz($z3@o* z^Vd%F@)M!W^G6L$aJl1>g5uKxa^`_F&DtBvdx`-9|l=_h!tY%9!p z89+C;7q+}?Pe-&BF2CGHi`xiOUbUc~wiDL8%AjFEg$}P{E$Nj9!rZ_7>4^t|@R!bI z@7Ee-|N5+lVMb{AKxqADF?If0`1DOHD%=;Yy=hI;?-&00rYoUSekcrn>q_Z@TSBzH z6Ak=c(CIU1!A+roIGN^rS2$bjNi09{!i2GYQOK5uq+AzwSEG448+(bFT$lnOo(p3k zOyR;%mFWi|fipcI%;G|42#H(>g76_1d?93U!41MzE;vB=m<#;cVjl$gf*j{;=x!_O zGFkhNh!tx?NpqiMSkZBc(Q|QiDe!*k^kX01v2e_h9WwJSn?*@G8hMv(qvQnL{tX*y zLAo{^{FM^>k(+Fz1&N|z->|zDWEef`&VnpSFP9bW|6?ksXvXJ&o(+zmiV^w6uUUm9 zX&#u*RW%R4;?hGJOgK8>PPcAL#_EWzh>&@IM4VU)E8-FN!53&I9Q|sl&vulW$qTbm zwY-O{lUKlAh*vJyZy{2o*jtuYA4Se^*^-sacbh1qoxhxX^>5e+fs&ju-C3R$nbV{a zCVz!%~GZD~tKW?@TW>B%c> zvMt#}`?;`}wxkPfT>zKUyirpm#TZr4_NnDv%PMJ_bY4m z*p0N*4Lv1S$H{t9uV0C-hs8dmn?moD>6y4Qbyt(r8CF~al~@0Q$TQC)ZodBCl@F0u z&*%O3qUurx7j4+Zq&5gzr2*iwjt4z#9JGpMx|4hKk!8`$W~7`@HwzZ*fi8RE2HW9* z8$;V~uyY>7*>mX)jb>1m)q4Qk;G8Rg-E@icm~?}^^dNo%BB1sjCMIjelZmK@-Z8lC z+Jaxu6unDgC0jS-Ur#N?D=7_Bx?5y*8w=C^}!eV1+b*Ox5L6Ae*HSf(dg z)YK6Z#(VXo1Fff?eCK9yn>F$xzQdbgX+FzugvxV50^xO^j`4t6z}+T1z%l?^%`A_4 zU-C?+2PlS8$#7{UL>H&8PLOfj~bA9WpHK{GIZoRJMBe0dkvNY zBF1HMKnEpYXUR~8G=dcdcUR=Z(?b=;LBrg<5qSWD4(pBk%!3PtjX>o3(`VN*y-5pKON{9MI_CSWQX?Fg)rv*?5PRB= zvhhCTPg+^RM*5Nv+UXR_@+ASZ$tiY{<8LMG0mm;(Sc4Y8{1Vow1@WV6O4vA#(@KhT zEy#6Z@$p}JHq@VZSuBCD5Q6`hzx1f;E!F>^st_7?_s4E;FrglR}k^Fw0^A@g%xLA*n}Vw>YVl&a+2-z{20&B zWx0*TSL|RAIoI%czB0*O&1Xq%$VkW0a5kNJuD@jW+7KUI;3W;teJ7nq8RDceI5jc$ z<+li`*Syq=PnT#{or!=4{(9-UVk`xwbCr*#=Vj(`+$k_hPl}@tk0Zn+*f|6nZJPF{ z{}rww!to)=^OjmVmJiFZFF~72Zl~g*O+%L7PV|y~gk-*_BF>e2VYT4oT zWGD@P$^L0id>ulOv8R1Ye)r@p6S6jswGSpiH0mgu5KKHQzrK$cY1rz@U(;fNFD`W;%*A9s7{1F!1fpoCD-_)=n zdwG=2??9T-!cSRN2Qr&>e9r#tfQ9(|S~j91nPhLpqgVTE&96nJ9Z4V3{HG^!#q!Ao z%PIa!M&KDWWc=e?=GmFF3#rY6nw2e2dKYk2hnaWsVYQZi$5QxT7j_j-*{aT@iq1d8 zriEbZCp=-Fhme83pKn(;(EnhH#ieQ5QzWQ09PQmbSk$5mSx@MU8dlhq%%m^#S@Tdb zztK8WhIHo?6|*LvZ4Je&@;75=L$RS~md_f65l@=MSi3Orf9z)?!^kc=e?R*(4Ew7Q z`&p}QWUcp|eNy?WbEQq8ecJatX>=ZWzjJCIyV;Gn(g{1+({31j>-RB-?ua#Q9}Dgd ze&IeA-JSHMVf)yI?$}ZMl*?+nli{v=AIY7H?*I%B)71W`7Y^mMWgmuNDZyqU9_PmxkTvg2W)f%@pI2cKQ8g}<{1p>ki(%z z>@Ez!QB`bn1nEF`KV;<**jiLpu>rlw@3gtVdh{XPXh9WQ(uZuMLpL$ozN9(rw5cet zFBwD{E&NU|mZZoLhOA?0k?8V^a@e&0*EXV$bC>`=ec>x3ex<%!il0VQE_8+jQ0ev(>*=LQy zE*rNQv5UzLDc;5&YDupKzig8#Z|Wx2dH^m$E^lLr1Be@)w4o?#0BJ$!lCM}nGZetnC*8b~7Pv0Fvm29a+FJ+Or} z9YTiM#BGrRRZqOl<_;kXY0YN#UIvM9bp8TyD=$)bmn-gLqpmOYf@&InHD zVWS-2$4^(28{z`(BSGT4!G)^aLYWH%m#A`!WiAn1gvuq!Tm(2*m0K#X4Kd{7;4{~e zADlO#H}cU}fGITsb=N;j&VDW~K9B?4Mdyzpg}7(2*kR~Q^|ILFVc5wX`?6^JFhU8< z+gP-7IB_O)TMqkt1TJarE@ux%U5@`zUOYU& zGhTfVHd#t3W#3N1K5OWCz3^bmd^T$`$)&r`v3hZ&z%u6?)=ICl&VIbs=vf{guBKC0 z)M=71CI;8xs-;S*-0TpCH1;}<6xW-8)5>c9Gwj3^^ur#h?7JzXC;ewNbDxSy{g>6O z7cl7NYTl=mX)NolG`OV=ceY(QH)T6rk|3mfHOrn#+!|bv)q9H>?v=PTD3R5Y#q1_m zKaJ%J;!esvp+cB zDTTG2LHc?8BMlX$SWD}qVny(gkR763#Byek5ZxJLmjKyCE_ul3vZ?wee^O1k0Lc#7 ztFWg`SV3X;ny{-CR(jUW5W{;4y9B;?rd7VO$qt#WV=*&{Bb}gQGiH)PTC|ciOCS-{ zwuntkAh+?{KvBn8WB~4-FJ~)eW3O(D$z?WiYyD;!Z=sNkM58vW48&7!XayM3`BT2I zS1)5N=8)bk4|8|5t>?iHr(#*@36hi3CvsdQk*%GBrKlvHiF0t_@4Sq8&Lwg5N+C;` zi*fmU8v9}{`qS=(?8RIx5{n91|9NDfb1L{i89lsXFWADzc{nUkc1V7v&*o+pu+jx&27OV$0usqq`r$El zA(6z|2V=MziZ$|CHuHXLEw7ZHz^w-UJhzC649cmzA@R=7@-(#uM(kdr-_ z#C}+WJ(|}n_Rk{H!u=OyP}&7-;Ypyj6~CEDtlN9UtNDGTk0ht?jd@mjo>YVg>7jrV zhgtG_B-L3w#9PVqvA^zzS)(K}g0B36O-sT7SJ(I0rX=!5%b8cb5slqg3DS#sZ1U;J zz5{x`d=AuRS@pfjjwX`<;q_(Tzpn5g#=c!8U;Ff%a^EdQEF}Sur5T^s&(8;0r}wc? zC#|5*lev6&WHXI5Swccxf6bSwF3}Z1 z_+!7a_4oufbqV$`bLX?YOGvn-^FF=ER;3Y-BD$1JC$w}Ao3)I*b{~NLQR2}`N^US7 z)yMi>F$eSX!aZ#9a%$(tQ73q*|&yU*#>IMs3AL z#adduviGtHD@ii_X;#tgmBbE9>8C}HbYwH3pY39cS7B1zzLOnWMV#og-R$xztj$e# zvVN;cpflTvIHMkCMLotdO}reaQmol*Z&T#N{Ks`WRm7vtXe^iDS|5wd5tCe`;C(b-0XhnaGx| zBmHU7NOo}@E|rE%U@w90?y*MME2z)MQd9=g-9C_Y&&F=4Y*SHsHt9-S`i+;aTu0#h z8OU}xG)!(N?Ctd=mA=SfV>e*-e19X~TLrV*8;GmrgdF3`--bCn)g0D62eDe_u=E_# z#`5!?cs@5LhxpJA8`<3)5@cz)>0KZf%c<*)G@7`H&Duzo(?{9tmyK9FpRQ-^HetZb zTF2rxkyc$}kRZP;9#x0?;i**wH`=G7I8}UbrDgNJfSa+eG}z^2Ta6y0tk@}i*x5~_ zeRod~as?N_^{Eg4;zL&QT^?RKZ)iNwsG_prqQ*?JS%5$W(mmNMS-@rEhIOn&Ap7kL zM)3g3IZfC)mbe+0MajlwT^9RnGZ|{{VN7n%V)k3eM(0#%@Hjn+GK%HpTK4%C(yMiI zZd0S)yH+ofsLCvS`@`(XPF(1CeuU$7zqRbEk4R9X{+WhOvg%nWv!6m-SxZI+jem$n zHQb54!&{hCidCqkTrOv(C!nuB?FD^8Iqrz(*H z%$E+BFJB+==26G-sQY6)#7N^I#7ptq_s;XFk*CKy&$?kOYAc!Ebow~6i^Y-S_{E|t z`)VucYTIF$+N?J-nCmvuA?WB~BpfCsoaB#;oHf#&5IxTk?=frE=-H}a#Kr&g8bh^Z zTa6j3(cn4dv{L)crS_kf%JfH(8sutx|PO0_?QfAcz%e|A+HQ!*FVPYC@_QB=aMDOJz(JZ znfxWX>D7SC4eh0o@DSmKgtaFK2Yv8DD~(Q6GY zn>Da#`zPcfq2cY>ie2OqP1Y7g?k2ww`fPbo+dagI(BvQ%xfj>U!JXNXz1Y861hLQe zVt!l{RrLK{OujBRTB~>5oHz2eOp+e#i)7+H($Ug+35M%~DrcJeLDAs-WF(=Meb}e@ z82Gc=u-o}$6@B?W8+`z$84o`wx_f{SVmUt<>j-9mW?uM0tr*fR*Bjq4Xd``kN}mqW zC(UIphsXlDD~V+sA|sj%O45rnGBLE3>#Xo3_Uj?sntJjc>vb5ldhR_o`!ET|{ce`e z^)>IYH;0Kk`}HtsNGD55G;OMoaYt|g`_&@ZW9JbPNH;8ESC5crJW?f-!AEgv@$EvE zcocCiTgZ+c#i_;kg-m}GlUZX4-PzP*q@6`kqO|7X4%kWJ%u0_TvL1=-*)dGLeu-@3 zapYlW0SRn;bb(ZWpYbTq&uH8m3uN1#0PU^i$xgY_sOA5vjD zdvOZ2gWlbk&QKJ)Q`qy}o6g#vA{S^vJogww#c3@1G?ujR>1@Sm;!eFC*|yWz%2%{w z#ixl^f1g4x7qQ_05YM z&tmk@KjWBhDOz~mR5rI1DZ!diij+O47M&_ZkD#8Di~hbq67ekW1U9dX45mBAv(sg` zkN7B-&AdcLH_-B3RCbS2e|Gs2nQA|e@59v1!0-vI>t)i@zN<0mAIs7%qr*OlWydZP z=jI*9@pFL3@{JF^+9VPUhQp^fOALCYI%ukCu@A2xX=wFS>SaXsXrRV##|?R z0-gBfL1xreLggq_Fn<&vCOGHH#hl(_dXFBE`AzRpz44V&{~@!z`4>jp4q-*# zl4QG_82Iy_65u}`fS*Qn8B6e7G5_;_uYv!2Hkv-ehxU%_+k3>w2IKwXC)#NI+XcR- z=|6nvs2C%1)Q|y^rug(5I($s;;gJm#Rfd!fn!+#eGLYb=KH#M=SrvvdaCx zM5NbWN?c7?MdL(`Ib+$>r)bjmWUYnEQQSI>#(jpz_+LnIy<{PZk1WI@@jm&-(K+Ra zC=SE_6#RFx4ZCzDAubI z#TUSN_eIh70d9!kKmI|{(buG#wH!CdIKK= zBY>?s>%}tvLx6^3S!u6sb@S7g!Fu*lE^&QC~Mzast8R)zW5dhZ!lYl*!>&30Wg}`Fq z9^hS|?vKS36}uf8b-_9$Y48kh$xKZQ{P>~R{S23YSLMh&L$wmJyj5JF2)^}q?hIA8@%rBi^n zasH79^u>94DX?7`A^^?=YMN;@p8~ysO)eokFdY~N+zm_tJ_qIjcV0$#VB2zp2aW}5 znxpgTAb3OA1dIUg1;znS15<#vfqB3uz*1lX+()Pax&k#G=rcfXU<>%< zE&=8N(}1PGzkpT1hF1{Y6XSmv1aGWp9|9wQ8?Wlcc;K=x^kN$DK5!2({2DqRFcbI~ z_zTd|3njUZi3j)!7zwoa65)ZffN8+e8|Zw%bCsAkfFo{W-T-d8gU;)%!w-r=2=IpC zEA(FABwzw?77!aWO%gC4_&2Zu*zE^IzR zgz&(z)d&ytd4lkKr;+z7A^<)EYJoeSBLd(hUMDW z=wK;|`M^1iM6m*R23QUJ4p<+X!*A?GF#wo=hi0_ETwnsQPZNX(UI*p_g{BA(ya%iX zHg^@Y!iX(wHfl0tlEfF3#qZPsf6Y%7~U0^_KQLF>L2-Jz9D-N=Tv=PN{U|~C~ zmcV}PvDyMjXHh&1oYMsXftFzi2rLG=wnm%cndop}cu!HB09@Eh6qf`0^by5e;0Jw0 z@ig#Kq$u74x<`xRTj0EbqUarnUx3pM7sUt&9q=rE954l#0$e&u6!!q5@hyM~;1OUo z&~Xf^JV>L72L=EgCyHVWaQtM{7%*ijN(`)t$76xO_opM{z;QStsRPE&62<0i&{gb5H%%|*_EOXnl!z%t-z;LG=sb71eK$T@J;3PjKrofQ}kOkIiaz;9L|X*Ke`Tx`_!L+LbjrY;y>=KCKyTm|z=(Dj|00Aq5b>Fa0JwMqN)F5hmI3Q>ux;Y{ zMp3kEk1F0IihjV&f+$7;zuAl?17_paMH+D8M+gsm$q*jcdn>{NJ8ct1{?q*ZJ{HAb z;1|G=!5IHOJ4A6Yh@-%*z@a-)b-*OxUEnrg9q=;HwF5E)3yqkxNntASgA z2Y|)Ex;%sjF589hzz3fqd`FF@=^lg!UIdQSK^U?h5ddcZw*ub-76bPK?*g0UBLZOP z0YuOVqu~%D08Rpq1#SQ?27U|N3KTv=$$_u1ZF~&0KZ%;_j7|&m1G*HU=76(-I{uSI zB@i-zx!6nQ1FL}*zz(O;>3}27pwoq589s|n2RwQX!xY%$JjOY2D=-7t<^sY4M*}N> zDZpyrL16tZ_?cc{0C3Aiga@9xi1E*V6R8G72C!)vA^`RPRsb&otAWcdA%dlQA2oH<`YJt1&bVURZ(!WLo zz$@P(0^s295CJgu9wGqd1M7!j)%yX%4H&>e>eKO7mEKs~*p~XVS5Hg|=xdD}*3iCo z*3)e3)rX2@QnMWY!&~V^tl97Equ$wbK@V!h&e~D;8K)uV!nd!n@5*<+_rRZneT}i5 zu96&@gL{jA{{X$X*TkM@us0`YivSkwOx<Z(=Vo*e$6>a}sv+vv=VWC9@R2AM96QPdBzxU5X)sNI10b zh%L5>gJ7`7!@eK(lP0AoFw|5U?BSjD;!YF$HiLZ+>?I-C`6AAmE?t0PL z*gouCUgBZz)14i3K^IAb90K1YV_&+!5Z4~qGhrWPlJ6KpzRO@g8?F}@nS`Hb2>%#% zhaTARo7nqHcI+SW;F%lS&kJtwpKT&{&YB97^~>|llUYYOv3#aahLJ~K_KWa}8nPo}X+ zoKJ*o70*_3IS2Bzc(#+v1(0XOv$I^TggkO4yU*oX$TMa#J(nG z2D>Zf)v&ozwOEHj&YUY%UCXhM_s!MgJ4^_u&LS3JQeX52wHWrYd2Ee6x<)Q!_Yc@X zd(`G>$lrg!u5$Sv0SCW-<5k0?0HM1G+UNIp0X4VL7L?Cq+#4VhW+nlQdiVj+F-H4^*(+L z=v}-PxrTE2!G2%`%8jvPD0dX5;m*>OAk7I{_*_iEHGy&7sJuq;R_DL1*>Dvu&rIy6 zc;;=T2r}SsXqBFAXiwdA`A|Bq){FfxD;ZL>=uNphkF5gs*fhQPnMrJi4J}v=`%~D5 znb-#z?DZRBT}ao9_~yM){B6|Q(P;wU&=$A6{%wlW!fyMaUc6+IK&c@C{?nnK!TxV6 zTL$c!483^SI0L*&PxCymqfMx_^HbJjjW_8x?CmXqnp@$&euZz zb`#Tc*})F=u$g%}LJo!;u$lGbat!1RTi7H=>KUB`dC)e!*xjVDx=4-Xjbd(wedfn{ zv5|RMEv5R_aGzrM49jJg5uf!v$YDF!6Q0gn$Up96HcpV6H^TYZCoGW5;gH2$ESk#` zAdlM3W^;Kt=l967TP|d)z3d?8PeaBR46btd9^_Yh*%L0mg&eV8_Gxaf(FEnQK5B36T38V6!>D9P;D?Y%P~_Aul_?4s!W4~WAi;qqI^?gyC- zmOAU^4v6O<3*>S*3iVNmO;+_LN6XS$;=T$W*)=d?wT~Db(SvJ?cz+ag|Y7> z$yLgjAMBy9|4Y?tB4M8Z`&i@fbc7+Tc-S|?-rvODN42-oq`~179G03m$c@iyW)JL6 z*QI%^e;MQi9C%JP@s)RS-1jl;`(b}-Qj0$uYSGeFqgixA&u#=_MfZcUr4kFKiF2vJ zITH46xAfvblPd3LxaNq5eL4khUtTj$zOM>~JoQi3lglxX zpZv)taXArk!@t-{F6TfF`kU?KaslM%f7n?rS3-XMkGwytg*-~n^qhA<-@~($o-H5; zLp~_7o-MEtjf9+m`?aOU`NY?@(HW(BNP<0p;0J(A8cLp_yb)$I?EC~BKUDm#hw+mU z`N~+s>!KJwHkP6|-MB{&S#ObTxsH9`g4)%qfQ+yK`z0k%?ER>ZbpVvX z4OtgI+EOnO4Z4XInJIw-$7iQ-M;7%i?Da^#9TH#=Qr=YFA3 z3f)9;h)L7~3|)RK><@7(V3f&P5hJY?Jo}4bclKs${Lx!-AxHVJgZ^mE(~xrl7=B9! z+a<_X0@xESzlB^Ez-(H=-`k6yZ?Sfe>+#!0OM_6q?$oUxzjt&dNEBxxfp>K_)zJL> zo{}YQ7sQy@2TFF{w0mIh+=k(Ir+lriK|bAqeb*9+KZE>ZC-#OX?%<6nx(oAch3?iF za%D!;s!c*gt~(N0S=8FV!ea`1%P>_i-D77*`@LSLmNo6jB>*g-@3rqNs=e zyE6U79dvRd!@;p9j*(3q-ZNz4F6?t*|F`v|4)(4Q_>EcPOjwi{TGG`Ad+OeJ2Eru% zD8#Rm2URE>G<`&|JqDQ}hZceLc@8iXG-F}6k49H9S%U@`)}Y0(_ZTdS5hnKT2K!dn zC&q~Xy{~DjjOt=I(kjvqa z{~E`lxjX@~MXW3@hin(i)^a`Q`)<8lP#Rtanfm*XIhN?`MYXiMvLkekh7+c|$2a-%t{BnWw{guGx5 zyAN6S3i2-8L;AOF?C6hh^F(o%N&V&;dRS-JH^2^f*8$|+xzqth!m#<@?E#u3IBbJM zTjK~4r1+%>Hp9Me9%dd}%%~@z6syV|C|9A(alr)k3d&dimuMVY;>iv;XW=r?A94+R zhZ3VaTSOar{Ycnd7f5a0G70js1-Mm3jeRE@CdSRMpIa#Pg?c9--(D!PAfz!J^0YE>OdKj$pAg^1+ zj&iva@)xVwm+h%%|HqKutp4AVJfDs%TjTy;DxTpo&M5uC(3|~Wf0in;hUikdNGPsp z`0We}cpJ_pBMoy?JnX$c#N%2f)#hQSwlvuL;x6fW6MLq?z6bWtaQF1ONoFe4%*@o3 z!C}%`^gWXZCL1Dn4EydZ@n7#NSq7r|*5SsoNdoT{ zyDYcd19=bPJ8u%-X*I)|GT5WC<@g>$9;(QeL0IY(*$?tGS=L2Do|paa`HzSFHtfYF z>AgEtPJ?~U`v1L6;;p?0b`kdNCgHcJ`K_-hgTs;y@~%er7|Qw_j5w1KTi-BZE!$uw zz)kFDWQH`pwS-QT*P-;cL}FlLmX6 zZKC+h#QuxHz6bWY?IL^XOx<<`9Cu09;}u+>;4TFZE4aT*_A60vl!7S=s_(m3-*5l7!mIDE zm)=*8yV)A`{q*Yl=zT8Z5n4~3*DX1I^%dsoE6deal&h~K|FlB(Q{Om#?6Rz@ZxmPG zAg;bK-2SR;S6>LOz5rZ(;djYR$!?)Zy#N1IKGgSuTgutjOs(Rb2^%&A&3W~`;Ji!m zKhNNV6kA}`puRm@4WPa?TYXEm%Bye3R$qp#^6E>l)t6q^ld^}M*F!m@ ztrS%KrFU8*eU17OYt^8>yt=Whu*)Hf@u2K5ET>I;iiUVTxq`eI_0SKk+` zzJFM40`U)M&UiDXBCoCQ1@+wf@BdoqRSWQ5EU$FXqV3n8N3yg5;Yk*Z=ef_Wc zx?h!7U-PTJ)>q*zHF%vbCPe?%>g#*ei9&sCuR4s<-pWm7xp7xh)_fqf^~u22iT~Nj zpuu++B+2dnlyx4^RTXI*pEuDZP0b4>1(75u5NZr9QY0V-h@g)EqR2r+LnI-A1QH~a z7my-?2t*x9qy$)lR9UKk1Z?yLJR;)J4OsyjY$B}6D$9P~eE;$2+c_s^?)`mb=FZHW za_{6_oF|#SkLbpp6#69>-PS|DPx**`%v;qj!HQjtK4ET%O$> zzO1|TH*?6RPf`&r8la=-Y7Y+3^@D;{4If0s_~>O?U5gH)8N>StOE)J10ZKpkPW4%ETkO4Ni?k2l^yI?7ZH)N#}r=!YBVnlLSKDwF8I zL$ih?bN?XS%)i#2#;7=a0odI>JHb=IOX!@#5&H9}7*z=#Y$>f~!4C)UD)3VQ{33C! z?)0mn$RDkRY`880H3OWU)!N{pvLpjl20jk_4)AchoTD`icntVs;IqN^#EYL#EvH_2 z9R$r_P@F6voX$CT!DqLVbGw&~4*7WqXXvZ;!pBOahJ*_5-$8)gyGDn+)!;T&dkbn3 z@u<)h3CR06`o{=p_`o-J5pV!JlekAktE7O>MCXAAe!+Vp{6zK;2XXLILYzj02sHwq z{w{%Fu5}cnY69x)4a*}`74%UPY8`PdiH?26K>_@{1AZOcP|0vO1YS4SH7j z@e3VL2nVl%!Djp9kRGal*Me`GD!}f`p+h$35=spyArJM1oYALV7X5UjuR2Vc8dcop>dfev5s%yI#5fUg4g$F32wivWjrASm4+=NB-1?FSEk zTlgF#d<1+8F6mD2&%lrTP4tg~SA&n(Z@FLHD{}QM1T#gT#=zhjxLY34y4QwK${1gn zTQ1Q<8yn)m-SUby0@%U0=M3xmzp*_1Mb#eju(Ps{iW`L{(Nw^918>I z4UC*ow>+Z_CpPS6?k9m;Fgk<7FmMY<$IpSg1tt0^^#izDU^@LE>dQG{x8N*9!gayp zX`#pc4~<9?c=sb4OdJa|j+^~F*rIl}$){*iN9U&W;*ShR%dio+Bm5!M@hjlD zwZiQ_13Fv-?)zD|-5)@Q{1r&8{CBGHzlxw7j?TcK{!QU_KL8zmO`J!|I1EniuDJ>Q zH%ZC?J+=y~BPB6olyLi6jO2;nw-yoS`;Vtu96CWTt5ghlxaH6ryfjQehF~=Wd~bws zjIt^Z{5G%fROn9!?*+bsIH#}5AlTnP3_gItR`69XA_0liN8n={3*ga>!`I*o?iH?~ z{}Xsja{>09HR;pD+i#0B1x4y6li_s0MV$%`|0)W+Pk??LxGzAzC3prnwOJe56X)Ce zi6x398EHUfOf1EOzVUlT6?f3A!04ic&P#QC=D<8Dh^ z{$#Kc0-qBg;dkI@DNhn9oJLW60L~v1WV{a;#>tazY4D}SfxQ<@$8hsli*KU{b)4;7cc@m8~RT||1t0>!u|9*R2=SX#==;DggU`2|%AfbxbGQM)Xr3=q zNTp$wLmis6+F3F()IhMhi#Ru;U$R8sbx?8O{~Rs+fJCYVczKS1boRsb-vNRjAh2(z ziTl7CVv=XyN)sOmUT?BE!kkh~0N(&^|9gY(3dcbnyr_TLT$Hz61YgBQW$!lbCLfn&v` zI)FEuEeT{ozX$ky@Osomb9jn4SLH%fWvujnw7mfatLBR%*V<=;?*K;)sU_fX3q&8& zeYM^u5UnZ$ZsYCHU$a8=?fY5E&_Ta7h*0N{BUFt#41?JfVsO9BKmA+)_%85t@U!5a z^`ieS8ubP6y_<#eq?W@~aR1%gM9@&U3J;f}?gYLbJQ2KlyXaSgcO%Zv15`$WCC$Nm zL%;Gp(Z@nbjkNmFDq2eDSy~cH$c4d$!(xC(T(*P zUKDfq5j?HJ6q8-zjRcX86tQYW9U|MgH)+GbJDL2kIx6%*5-0FgFEHmv>oooGDwB|{ z(?%ZXCVD%>n>qnEj~^8N0Ni{I`*T>BC4*mte)2T|c1J87UIUN(&>W?7w6Cd&%%Rf0 z+E5bef%FD>CEE>OH$zFDyyPF^$8LtELj!r)8R2szQtOEG_4J=&-p$d&`*z2zJ`i#* z&3L&uj^R4Sg2#g=qTOYqc)QzHbC()no*6^6vv|8H9z)qFoGf;|V4h9poNTY&0^Imo z47c+Jne$`lE^+TA>C<>NoPyoASbEHdzb|0t-f!An@Fcqeo`&M8fIi*b_Of=MFaH`m z+H$9)%*tH4WhZPg+j6O;&OT$RIX+QfBJ*@wyJUEK1o|t0`6!V}zxSr&G#9yUYE}?X? zz(0E592!f0-YPIZQk;G}R;W3!-wnNrr^RqID(^V>zJ(x zW3i;#)6mKCJ*ef^kj{hP2TBDz5B?}|Zd84jiJw=%he5w|fq?$th0vcQ_p0fguM?Bq zD`>i(ZP){)8J~+&XM7BN&0$lXPfhV{=>SzT*o9Hs)=me$yXPZ5J}IRI6rOo ziDFae$6AgPx}m#|r=}UV!eor6?%>8P5v+xL3@LMsJotiH#_~8^bTZ^`Adv~(O*P5& zpaUjy0>!@_Fg+!WvbojO|t#OIo+iJx70D{AH*oy8h$pbG1DY zyyiU<`vTS4h8UCn0`*7J>GNuI?rClgzM!K<4tVR&xe2EEB$q-iI`s+2Wgj#)Ut0lg z<`F^qTsn0yM7z>#2tR9kn$DA`v1INMyQ8>620}i4p>X>js&tqMe5%=ZbL2f zHytuVi*=iJqhA+;A0bb;TMDDJop8JHo@6QD8y`?+Z?SIepPnfS9we9tg#xUFpM}F$ z!B4LfPy+pR;1N3o*u8_KQVH%wDY-l31aZCvzL6J0CTp@z)cd!Y#gnPQ)=U%U#k7v* z(1>P-T%}XX%=yXGA9Th_z!f?FTLH_*ePudNp^@odyTz_ZA~m1nTza=p6)+BbCG7r= zr_ruy?Ep{iVYZP!|CxZ<^A*THzzvH5Qw7~4>3oJ8ZYI*X1H87U0JofuC(ik5b#$I3g`-+173s+q;AQEDd0npFz!^fm^c^I)qK(G zVk192*MouE$@QJc)ei6%hshP(hehF0@XhhUk3qi*37kYbb~W)0^xe)h*S`5bAR-%4 zj+%Zbj_f8ya^`I;IZZ4u#nWg?*BO1_cqGIhV&bB$=qteWjZcGPYnjS~ej)htV+0HZ zp8;O)aRJXrHq@(@Ly5;tzwV)H&zc#2>d4)`xG>1?h?C3?MHfaBGaK$9&Y5iRzNu!r zUoZ|{fIJQIW~l!q;6uPmcbJ|fbiZ6%BDs1TnVSpwlY7j|CAvtTE;D*Mwd12h#KDSs z5;uyM^lxG2wMek)0Df7%Zx-~2gXKqEBM0uOp6z(cHEX5 zn5pxS7k*|6NS@xRSS0(Q5Y|L8UV(yY3LZtATT5G%P^uO|Yw)LzhwGuB&}76qV;=h7O_?;M(|W~ zgtgp&?&1jJ-9%b$r_u=1cN1xq$m0*?;tI2fv_joel;$_-2d8z%kdk02r4kdenX>I3 zzR)%&K6TG-Qp{XFO~s=b$_Sl?y3I2+=_YiG?)dvQsH%SAGuRB+qWzve<}^o@X24e6 T$+O$c-Kx8LCYmE#b+i8gM;YZs delta 43188 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 D{0w`7 diff --git a/util/websocket.c b/util/websocket.c index d501e86..cc0e454 100644 --- a/util/websocket.c +++ b/util/websocket.c @@ -43,7 +43,7 @@ static void sys_broadcast(struct mg_connection *nc, const struct mg_str msg) { //Broadcast incoming message [sn_c => all] static void broadcast(const struct mg_str msg) { - printf("\nBROADCAST\t[%d]", (int)msg.len-1); + printf("BROADCAST\t[%d]\n", (int)msg.len-1); struct mg_connection *c; for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) { if (c == sn_c) continue; @@ -55,7 +55,7 @@ static void broadcast(const struct mg_str msg) { static void groupcast(const struct mg_str msg) { char gid[35]; snprintf(gid, sizeof(gid), "%.*s", 34, &msg.p[1]); - printf("\nGROUPCAST\t[%d]", (int)msg.len - 36); + printf("GROUPCAST\t[%d]\n", (int)msg.len - 36); struct mg_connection *c; for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) { if (c == sn_c) continue; @@ -69,7 +69,7 @@ static void unicast(const struct mg_str msg) { char uid[6], gid[35]; snprintf(uid, sizeof(uid), "%.*s", 5, &msg.p[1]); snprintf(gid, sizeof(gid), "%.*s", 34, &msg.p[7]); - printf("\nUNICAST\t[%d]", (int)msg.len - 42); + printf("UNICAST\t\t[%d]\n", (int)msg.len - 42); struct mg_connection *c; for (c = mg_next(sn_c->mgr, NULL); c != NULL; c = mg_next(sn_c->mgr, c)) { if (c == sn_c) continue; @@ -80,11 +80,11 @@ static void unicast(const struct mg_str msg) { //Forward incoming message [user => sn_c] static void forward(const struct mg_str msg) { - printf("\nFORWARD\t[%d]", (int)msg.len - 41); + printf("FORWARD\t\t[%d]\n", (int)msg.len - 41); if(sn_c != NULL) mg_send_websocket_frame(sn_c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len); else - printf("\nWARNING: No supernode connected"); + printf("WARNING: No supernode connected\n"); } static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) { From 0398de34fde8936f5f0fb74a5585ccb3a6884f41 Mon Sep 17 00:00:00 2001 From: sairajzero Date: Fri, 25 Sep 2020 02:49:11 +0530 Subject: [PATCH 6/8] Migration and bug fixes Fixed: Migration feature issues Fixed: minor bug from previous update --- app/index.html | 1094 +++++++++++++++--------------------------------- 1 file changed, 336 insertions(+), 758 deletions(-) diff --git a/app/index.html b/app/index.html index 20dc343..4594f53 100644 --- a/app/index.html +++ b/app/index.html @@ -25,7 +25,7 @@ FLO: ['https://explorer.mediciland.com/', 'https://livenet.flocha.in/', 'https://flosight.duckdns.org/', 'http://livenet-explorer.floexperiments.com'], FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/'] }, - SNStorageID: "FEzk75EGMPEQMrCuPosGiwuK162hcEu49E", + SNStorageID: "FR876VRTna3DremwQKZ421xPGMTREzctRB", //sendAmt: 0.001, //fee: 0.0005, @@ -41,8 +41,9 @@ /* List of supernode configurations (all blockchain controlled by SNStorageID) backupDepth - (Interger) Number of backup nodes refreshDelay - (Interger) Count of requests for triggering read-blockchain and autodelete - deleteDelay - (Interger) Maximum number of duration (milliseconds) an unauthorised data is stored + deleteDelay - (Interger) Maximum duration (milliseconds) an unauthorised data is stored errorFeedback - (Boolean) Send error (if any) feedback to the requestor + delayDelta - (Interger) Maximum allowed delay from the data-time */ } @@ -52,52 +53,6 @@ -