From 981f198585175572280ddb74dc6d25316a347ea8 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Sun, 21 Jun 2020 11:21:44 +0200 Subject: [PATCH 01/16] fix unreachable and duplicate code --- ZelBack/src/services/explorerService.js | 10 ++++++---- ZelBack/src/services/zelfluxCommunication.js | 3 +-- ZelFront/src/components/Explorer.vue | 3 --- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/ZelBack/src/services/explorerService.js b/ZelBack/src/services/explorerService.js index 614361188..528573eee 100644 --- a/ZelBack/src/services/explorerService.js +++ b/ZelBack/src/services/explorerService.js @@ -518,10 +518,12 @@ async function initiateBlockProcessor(restoreDatabase) { database.collection(zelappsHashesCollection).createIndex({ zelapphash: 1 }, { name: 'query for getting zelapphash' }); } if (scannedBlockHeight !== 0 && restoreDatabase) { - const databaseRestored = await restoreDatabaseToBlockheightState(scannedBlockHeight); - console.log(`Database restore status: ${databaseRestored}`); - if (!databaseRestored) { - throw new Error('Error restoring database!'); + try { + await restoreDatabaseToBlockheightState(scannedBlockHeight); + log.info('Database restored OK'); + } catch (e) { + log.error('Error restoring database!'); + throw e; } } processBlock(scannedBlockHeight + 1); diff --git a/ZelBack/src/services/zelfluxCommunication.js b/ZelBack/src/services/zelfluxCommunication.js index e5b14e5dc..14d81c1cc 100644 --- a/ZelBack/src/services/zelfluxCommunication.js +++ b/ZelBack/src/services/zelfluxCommunication.js @@ -1065,8 +1065,7 @@ async function adjustFirewall() { } function isCommunicationEstablished(req, res) { - let message = serviceHelper.createErrorMessage('Communication to other ZelFluxes is not sufficient'); - + let message; if (outgoingPeers.length < 5) { message = serviceHelper.createErrorMessage('Not enough outgoing connections'); } else if (incomingPeers.length < 2) { diff --git a/ZelFront/src/components/Explorer.vue b/ZelFront/src/components/Explorer.vue index 12e68061f..d169850dd 100644 --- a/ZelFront/src/components/Explorer.vue +++ b/ZelFront/src/components/Explorer.vue @@ -698,9 +698,6 @@ export default { this.transactionDetail = txDetail; this.uniqueKey += 1; } - const txDetail = txContent.data.data; - txDetail.senders = senders; - this.transactionDetail = txDetail; } else { this.transactionDetail = txContent.data.data; this.uniqueKey += 1; From da1ba577110e0a6c2b191ec4a38d6d435550f8b5 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Sun, 21 Jun 2020 11:27:31 +0200 Subject: [PATCH 02/16] fix duplicate css --- ZelFront/src/assets/css/main.css | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ZelFront/src/assets/css/main.css b/ZelFront/src/assets/css/main.css index c43d999c2..a118c3b56 100644 --- a/ZelFront/src/assets/css/main.css +++ b/ZelFront/src/assets/css/main.css @@ -72,6 +72,7 @@ ul { input[type="checkbox"] { margin: 5px; vertical-align: top; + background-color: #000; } input[type="checkbox"]:checked + label, @@ -79,10 +80,6 @@ input[type="checkbox"]:checked + label a { color: #97f597; } -input[type="checkbox"] { - background-color: #000; -} - img { -webkit-app-region: no-drag; transition: 0.1s; @@ -236,10 +233,7 @@ h4 { display: inline-block; } -.el-menu--collapse - .el-submenu.is-opened - > .el-submenu__title - .el-submenu__icon-arrow { +.el-menu--collapse .el-submenu .is-opened > .el-submenu__title .el-submenu__icon-arrow { -webkit-transform: rotateZ(180deg); -ms-transform: rotateZ(180deg); transform: rotateZ(180deg); From 3fa578492ebe57a85b448c1d943e4c3180f8e998 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Sun, 21 Jun 2020 11:32:32 +0200 Subject: [PATCH 03/16] simplif color --- ZelFront/src/assets/css/main.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZelFront/src/assets/css/main.css b/ZelFront/src/assets/css/main.css index a118c3b56..5e49b31e7 100644 --- a/ZelFront/src/assets/css/main.css +++ b/ZelFront/src/assets/css/main.css @@ -276,7 +276,7 @@ h4 { .bar3 { width: 35px; height: 5px; - background-color: #ffffff; + background-color: #fff; margin: 6px 0; transition: 0.4s; } From b667ca3f231a2c1337b57df5d025dca1055aa918 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Sun, 21 Jun 2020 11:42:09 +0200 Subject: [PATCH 04/16] markdown --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 959792f9a..32e2722fc 100644 --- a/README.md +++ b/README.md @@ -20,28 +20,28 @@ This application communicates locally with the ZelCash Daemon (zelcashd), ZelBen ### Backend Solution - zelback -- Provide communication with zelcashd -- Providing private API, and public API, ZelNode team API (limited!) -- Listen and handel frontend requests -- Requests signing and authenticity verifying -- Handel communication with other zelnode daemons (zelflux solution) -- Manage ZelNode applications - smart spawning, distributing workload, termination depending of application subscription. -- and more! +- Provide communication with zelcashd +- Providing private API, and public API, ZelNode team API (limited!) +- Listen and handel frontend requests +- Requests signing and authenticity verifying +- Handel communication with other zelnode daemons (zelflux solution) +- Manage ZelNode applications - smart spawning, distributing workload, termination depending of application subscription. +- and more! ### Frontend Solution - zelfront -- Display ZelNode status information -- Display Zel Network information -- Display ZelCash status information -- Display ZelCash network information -- Display Specific application information -- Provide API access -- Login into private API part (frontend part) -- Login into ZelNode team API part (frontend part) -- Private: Management of ZelNode -- Private: Management of ZelCash -- Private: Update, status information -- and more! +- Display ZelNode status information +- Display Zel Network information +- Display ZelCash status information +- Display ZelCash network information +- Display Specific application information +- Provide API access +- Login into private API part (frontend part) +- Login into ZelNode team API part (frontend part) +- Private: Management of ZelNode +- Private: Management of ZelCash +- Private: Update, status information +- and more! This application is open source and distributed under the GNU AGPLv3 licence From c4e60907536d7cc171f6b8cc0946f6f556e50429 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Sun, 21 Jun 2020 11:51:28 +0200 Subject: [PATCH 05/16] keep range of connections --- ZelBack/src/services/zelfluxCommunication.js | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/ZelBack/src/services/zelfluxCommunication.js b/ZelBack/src/services/zelfluxCommunication.js index 14d81c1cc..f04f9733c 100644 --- a/ZelBack/src/services/zelfluxCommunication.js +++ b/ZelBack/src/services/zelfluxCommunication.js @@ -649,11 +649,15 @@ async function initiateAndHandleConnection(ip) { async function fluxDisovery() { const minPeers = 10; + const maxPeers = 20; const zl = await deterministicZelNodeList(); const numberOfZelNodes = zl.length; const requiredNumberOfConnections = numberOfZelNodes / 100; // 1% - const minCon = Math.max(minPeers, requiredNumberOfConnections); - if (outgoingConnections.length < minCon) { + const maxNumberOfConnections = numberOfZelNodes / 50; // 2% + const minCon = Math.max(minPeers, requiredNumberOfConnections); // awlays maintain at least 10 or 1% of nodes whatever is higher + const maxCon = Math.max(maxPeers, maxNumberOfConnections); // have a maximum of 20 or 2% of nodes whatever is higher + // coonect a peer as maximum connections not yet established + if (outgoingConnections.length < maxCon) { let ip = await getRandomConnection(); const clientExists = outgoingConnections.find((client) => client._socket.remoteAddress === ip); if (clientExists) { @@ -663,16 +667,11 @@ async function fluxDisovery() { log.info(`Adding ZelFlux peer: ${ip}`); initiateAndHandleConnection(ip); } - // connect another peer - setTimeout(() => { - fluxDisovery(); - }, 1000); - } else { - // do new connections every 60 seconds - setTimeout(() => { - fluxDisovery(); - }, 60000); } + // fast connect another peer as we do not have even enough connections to satisfy min or wait 1 min. + setTimeout(() => { + fluxDisovery(); + }, outgoingConnections.length < minCon ? 1000 : 60 * 1000); } function connectedPeers(req, res) { From 7f7ae1d12f328ec55845a9d60a637fdb00697c3d Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Sun, 21 Jun 2020 12:19:10 +0200 Subject: [PATCH 06/16] send messages to incoming connection as well delay service helper --- ZelBack/src/services/serviceHelper.js | 5 + ZelBack/src/services/zelfluxCommunication.js | 135 +++++++++++-------- 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/ZelBack/src/services/serviceHelper.js b/ZelBack/src/services/serviceHelper.js index d0842b483..b750f98ae 100644 --- a/ZelBack/src/services/serviceHelper.js +++ b/ZelBack/src/services/serviceHelper.js @@ -13,6 +13,10 @@ const log = require('../lib/log'); const { MongoClient } = mongodb; const mongoUrl = `mongodb://${config.database.url}:${config.database.port}/`; +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + function createDataMessage(data) { const successMessage = { status: 'success', @@ -420,4 +424,5 @@ module.exports = { errUnauthorizedMessage, axiosGet, verifyZelID, + delay, }; diff --git a/ZelBack/src/services/zelfluxCommunication.js b/ZelBack/src/services/zelfluxCommunication.js index f04f9733c..bb87fae93 100644 --- a/ZelBack/src/services/zelfluxCommunication.js +++ b/ZelBack/src/services/zelfluxCommunication.js @@ -187,68 +187,88 @@ async function verifyTimestampInFluxBroadcast(data, currentTimeStamp) { return false; } -function sendToAllPeers(data) { - let removals = []; - let ipremovals = []; - // console.log(data); - outgoingConnections.forEach((client) => { - try { - client.send(data); - } catch (e) { - log.error(e); - removals.push(client); - const ip = client._socket.remoteAddress; - const foundPeer = outgoingPeers.find((peer) => peer.ip === ip); - ipremovals.push(foundPeer); - } - }); +function sendToAllPeers(data, wsList) { + try { + let removals = []; + let ipremovals = []; + // console.log(data); + // wsList is always a sublist of outgoingConnections + const outConList = wsList || outgoingConnections; + outConList.forEach((client) => { + try { + client.send(data); + } catch (e) { + log.error(e); + removals.push(client); + try { + const ip = client._socket.remoteAddress; + const foundPeer = outgoingPeers.find((peer) => peer.ip === ip); + ipremovals.push(foundPeer); + } catch (err) { + log.error(err); + } + } + }); - for (let i = 0; i < ipremovals.length; i += 1) { - const peerIndex = outgoingPeers.indexOf(ipremovals[i]); - if (peerIndex > -1) { - outgoingPeers.splice(peerIndex, 1); + for (let i = 0; i < ipremovals.length; i += 1) { + const peerIndex = outgoingPeers.indexOf(ipremovals[i]); + if (peerIndex > -1) { + outgoingPeers.splice(peerIndex, 1); + } } - } - for (let i = 0; i < removals.length; i += 1) { - const ocIndex = outgoingConnections.indexOf(removals[i]); - if (ocIndex > -1) { - outgoingConnections.splice(ocIndex, 1); + for (let i = 0; i < removals.length; i += 1) { + const ocIndex = outgoingConnections.indexOf(removals[i]); + if (ocIndex > -1) { + outgoingConnections.splice(ocIndex, 1); + } } + removals = []; + ipremovals = []; + } catch (error) { + log.error(error); } - removals = []; - ipremovals = []; } -function sendToAllIncomingConnections(data) { - let removals = []; - let ipremovals = []; - // console.log(data); - incomingConnections.forEach((client) => { - try { - client.send(data); - } catch (e) { - log.error(e); - removals.push(client); - const ip = client._socket.remoteAddress; - const foundPeer = incomingPeers.find((peer) => peer.ip === ip); - ipremovals.push(foundPeer); - } - }); +function sendToAllIncomingConnections(data, wsList) { + try { + let removals = []; + let ipremovals = []; + // console.log(data); + // wsList is always a sublist of incomingConnections + const incConList = wsList || incomingConnections; + incConList.forEach((client) => { + try { + client.send(data); + } catch (e) { + log.error(e); + removals.push(client); + try { + const ip = client._socket.remoteAddress; + const foundPeer = incomingPeers.find((peer) => peer.ip === ip); + ipremovals.push(foundPeer); + } catch (err) { + log.error(err); + } + } + }); - for (let i = 0; i < ipremovals.length; i += 1) { - const peerIndex = incomingPeers.indexOf(ipremovals[i]); - if (peerIndex > -1) { - incomingPeers.splice(peerIndex, 1); + for (let i = 0; i < ipremovals.length; i += 1) { + const peerIndex = incomingPeers.indexOf(ipremovals[i]); + if (peerIndex > -1) { + incomingPeers.splice(peerIndex, 1); + } } - } - for (let i = 0; i < removals.length; i += 1) { - const ocIndex = incomingConnections.indexOf(removals[i]); - if (ocIndex > -1) { - incomingConnections.splice(ocIndex, 1); + for (let i = 0; i < removals.length; i += 1) { + const ocIndex = incomingConnections.indexOf(removals[i]); + if (ocIndex > -1) { + incomingConnections.splice(ocIndex, 1); + } } + removals = []; + ipremovals = []; + } catch (error) { + log.error(error); } - removals = []; - ipremovals = []; } async function serialiseAndSignZelFluxBroadcast(dataToBroadcast, privatekey) { @@ -273,7 +293,7 @@ async function serialiseAndSignZelFluxBroadcast(dataToBroadcast, privatekey) { return dataString; } -async function handleZelAppRegisterMessage(message) { +async function handleZelAppRegisterMessage(message, fromIP) { try { // check if we have it in database and if not add // if not in database, rebroadcast to outgoing connections only @@ -284,6 +304,9 @@ async function handleZelAppRegisterMessage(message) { if (rebroadcastToPeers === true) { const messageString = serviceHelper.ensureString(message); sendToAllPeers(messageString); + await serviceHelper.delay(2345); + const wsList = incomingConnections.filter((client) => client._socket.remoteAddress !== fromIP); + sendToAllIncomingConnections(messageString, wsList); } } catch (error) { log.error(error); @@ -309,7 +332,7 @@ function handleIncomingConnection(ws, req, expressWS) { try { const msgObj = serviceHelper.ensureObject(msg); if (msgObj.data.type === 'zelappregister') { - handleZelAppRegisterMessage(msgObj); + handleZelAppRegisterMessage(msgObj, peer.ip); } if (msgObj.data.type === 'HeartBeat' && msgObj.data.message === 'ping') { // we know that data exists const newMessage = msgObj.data; @@ -1089,7 +1112,11 @@ async function broadcastTemporaryZelAppMessage(message) { if (typeof message !== 'object' && typeof message.type !== 'string' && typeof message.version !== 'number' && typeof message.zelAppSpecifications !== 'object' && typeof message.signature !== 'string' && typeof message.timestamp !== 'number' && typeof message.hash !== 'string') { return new Error('Invalid ZelApp message for storing'); } + // to all outoing await broadcastMessageToOutgoing(message); + await serviceHelper.delay(2345); + // to all incoming. Delay broadcast in case message is processing + await broadcastMessageToIncoming(message); return 0; } From 1b8d2c912a77d97c503dfddd33b7bde1683d98d9 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Mon, 22 Jun 2020 09:59:47 +0200 Subject: [PATCH 07/16] UI adjustment --- README.md | 30 ++++++++++---------- ZelBack/src/services/zelappsService.js | 4 +-- ZelBack/src/services/zelfluxCommunication.js | 22 +++++++------- ZelBack/src/services/zelnodeService.js | 12 ++++---- ZelFront/public/index.html | 4 +-- ZelFront/src/components/Login.vue | 2 +- ZelFront/src/components/ZelAdmin.vue | 16 +++++------ ZelFront/src/components/ZelCash.vue | 4 +-- ZelFront/src/components/ZelNode.vue | 4 +-- ZelFront/src/components/shared/Footer.vue | 6 ++-- ZelFront/src/components/shared/Header.vue | 12 ++++---- init.js | 4 +-- package.json | 3 +- tests/ZelBack/zelcashService.js | 2 +- tests/ZelBack/zelfluxCommunication.js | 8 +++--- zelflux.sh | 2 +- 16 files changed, 68 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 32e2722fc..d651466a8 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ -# ZelFlux - ZelNode Daemon +# Flux - Node Daemon ![ZelNode.gif](ZelFront/src/assets/img/zelnode.gif) -[![DeepScan grade](https://deepscan.io/api/teams/6436/projects/8442/branches/100920/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=6436&pid=8442&bid=100920) [![CodeFactor](https://www.codefactor.io/repository/github/zelcash/zelflux/badge)](https://www.codefactor.io/repository/github/zelcash/zelflux) +[![DeepScan grade](https://deepscan.io/api/teams/6436/projects/8442/branches/100920/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=6436&pid=8442&bid=100920) [![CodeFactor](https://www.codefactor.io/repository/github/zelcash/Flux/badge)](https://www.codefactor.io/repository/github/zelcash/Flux) ## API Documentation -[API documentation](https://zelcash.github.io/zelfluxdocs/) +[API documentation](https://zelcash.github.io/Fluxdocs/) ## The gateway to the Zel Network -ZelFlux is the frontend UI to the entire Zel Network, it enables ZelNode operators to manage their ZelNode easily via a simple web interface. ZelFlux enables a ZelNode operator to perform all tasks such as updating and maintenance from a simple web interface, instead of having to remotely login to their ZelNode to manage it. +Flux is the frontend UI to the entire Zel Network, it enables ZelNode operators to manage their ZelNode easily via a simple web interface. Flux enables a ZelNode operator to perform all tasks such as updating and maintenance from a simple web interface, instead of having to remotely login to their ZelNode to manage it. -ZelFlux Requires a reasonably new version of Node.js (npm), MongoDB and Docker. It is a MongoDB, Express.js, Vue.js, Node.js (MEVN) application +Flux Requires a reasonably new version of Node.js (npm), MongoDB and Docker. It is a MongoDB, Express.js, Vue.js, Node.js (MEVN) application -This application communicates locally with the ZelCash Daemon (zelcashd), ZelBench Daemon (benchmarkd) and with other ZelNode Daemons (zelflux). +This application communicates locally with the ZelCash Daemon (zelcashd), ZelBench Daemon (benchmarkd) and with other ZelNode Daemons (Flux). ## Application Overview @@ -24,7 +24,7 @@ This application communicates locally with the ZelCash Daemon (zelcashd), ZelBen - Providing private API, and public API, ZelNode team API (limited!) - Listen and handel frontend requests - Requests signing and authenticity verifying -- Handel communication with other zelnode daemons (zelflux solution) +- Handel communication with other zelnode daemons (Flux solution) - Manage ZelNode applications - smart spawning, distributing workload, termination depending of application subscription. - and more! @@ -45,9 +45,9 @@ This application communicates locally with the ZelCash Daemon (zelcashd), ZelBen This application is open source and distributed under the GNU AGPLv3 licence -## Start ZelFlux +## Start Flux -ZelFlux needs Zelcashd to be ruuning, to setup Zelcashd follow [these instructions.](https://github.com/zelcash/ZelNodeInstallv3) +Flux needs Zelcashd to be ruuning, to setup Zelcashd follow [these instructions.](https://github.com/zelcash/ZelNodeInstallv3) Setup Mongodb on Ubuntu 16.04 (LTS): @@ -144,20 +144,20 @@ Install Docker using snap snap install docker ``` -Clone ZelFlux repo (Ubuntu): +Clone Flux repo (Ubuntu): ```bash sudo apt-get install git -git clone https://github.com/zelcash/zelflux +git clone https://github.com/zelcash/Flux ``` -Clone ZelFlux repo (Redhat/CentOS): +Clone Flux repo (Redhat/CentOS): ```bash sudo yum install git -git clone https://github.com/zelcash/zelflux +git clone https://github.com/zelcash/Flux ``` Allow Inbound Connections on UFW firewall (ONLY if ufw enabled): @@ -167,10 +167,10 @@ sudo ufw allow 16126/tcp sudo ufw allow 16127/tcp ``` -Install ZelFlux dependancies (Ubuntu/CentOS/Redhat): +Install Flux dependancies (Ubuntu/CentOS/Redhat): ```bash -cd zelflux +cd Flux npm install ``` diff --git a/ZelBack/src/services/zelappsService.js b/ZelBack/src/services/zelappsService.js index 1186d2e92..d84e9143a 100644 --- a/ZelBack/src/services/zelappsService.js +++ b/ZelBack/src/services/zelappsService.js @@ -1656,7 +1656,7 @@ async function registerZelAppLocally(zelAppSpecifications, res) { // check if zelfluxDockerNetwork exists, if not create const fluxNetworkStatus = { - status: 'Checking ZelFlux network...', + status: 'Checking Flux network...', }; log.info(fluxNetworkStatus); if (res) { @@ -2137,7 +2137,7 @@ async function registerZelAppGlobalyApi(req, res) { // first check if this node is available for application registration - has at least 5 outgoing connections and 2 incoming connections (that is sufficient as it means it is confirmed and works correctly) // TODO reenable in smarter way // if (zelfluxCommunication.outgoingPeers.length < 5 || zelfluxCommunication.incomingPeers.length < 2) { - // throw new Error('Sorry, This ZelFlux does not have enough peers for safe application registration'); + // throw new Error('Sorry, This Flux does not have enough peers for safe application registration'); // } const processedBody = serviceHelper.ensureObject(body); // Note. Actually signature, timestamp is not needed. But we require it only to verify that user indeed has access to the private key of the owner zelid. diff --git a/ZelBack/src/services/zelfluxCommunication.js b/ZelBack/src/services/zelfluxCommunication.js index bb87fae93..73aba955c 100644 --- a/ZelBack/src/services/zelfluxCommunication.js +++ b/ZelBack/src/services/zelfluxCommunication.js @@ -351,7 +351,7 @@ function handleIncomingConnection(ws, req, expressWS) { } } } else { - ws.send(`ZelFlux ${userconfig.initial.ipaddress} says message received!`); + ws.send(`Flux ${userconfig.initial.ipaddress} says message received!`); } } catch (e) { log.error(e); @@ -364,7 +364,7 @@ function handleIncomingConnection(ws, req, expressWS) { // } } else if (messageOK === true) { try { - ws.send(`ZelFlux ${userconfig.initial.ipaddress} says message received but your message is outdated!`); + ws.send(`Flux ${userconfig.initial.ipaddress} says message received but your message is outdated!`); } catch (e) { log.error(e); } @@ -431,7 +431,7 @@ async function broadcastMessageToOutgoingFromUser(req, res) { if (authorized === true) { broadcastMessageToOutgoing(data); - const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to ZelFlux network'); + const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to Flux network'); response = message; } else { response = serviceHelper.errUnauthorizedMessage(); @@ -454,7 +454,7 @@ async function broadcastMessageToOutgoingFromUserPost(req, res) { const authorized = await serviceHelper.verifyPrivilege('zelteam', req); if (authorized === true) { broadcastMessageToOutgoing(processedBody); - const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to ZelFlux network'); + const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to Flux network'); response = message; } else { response = serviceHelper.errUnauthorizedMessage(); @@ -475,7 +475,7 @@ async function broadcastMessageToIncomingFromUser(req, res) { if (authorized === true) { broadcastMessageToIncoming(data); - const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to ZelFlux network'); + const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to Flux network'); response = message; } else { response = serviceHelper.errUnauthorizedMessage(); @@ -498,7 +498,7 @@ async function broadcastMessageToIncomingFromUserPost(req, res) { const authorized = await serviceHelper.verifyPrivilege('zelteam', req); if (authorized === true) { broadcastMessageToIncoming(processedBody); - const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to ZelFlux network'); + const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to Flux network'); response = message; } else { response = serviceHelper.errUnauthorizedMessage(); @@ -520,7 +520,7 @@ async function broadcastMessageFromUser(req, res) { if (authorized === true) { broadcastMessageToOutgoing(data); broadcastMessageToIncoming(data); - const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to ZelFlux network'); + const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to Flux network'); response = message; } else { response = serviceHelper.errUnauthorizedMessage(); @@ -544,7 +544,7 @@ async function broadcastMessageFromUserPost(req, res) { if (authorized === true) { broadcastMessageToOutgoing(processedBody); broadcastMessageToIncoming(processedBody); - const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to ZelFlux network'); + const message = serviceHelper.createSuccessMessage('Message successfully broadcasted to Flux network'); response = message; } else { response = serviceHelper.errUnauthorizedMessage(); @@ -588,7 +588,7 @@ async function initiateAndHandleConnection(ip) { rtt: null, }; outgoingPeers.push(peer); - broadcastMessageToOutgoing('Hello ZelFlux'); + broadcastMessageToOutgoing('Hello Flux'); console.log(`#connectionsOut: ${outgoingConnections.length}`); }); @@ -687,7 +687,7 @@ async function fluxDisovery() { ip = null; } if (ip) { - log.info(`Adding ZelFlux peer: ${ip}`); + log.info(`Adding Flux peer: ${ip}`); initiateAndHandleConnection(ip); } } @@ -1093,7 +1093,7 @@ function isCommunicationEstablished(req, res) { } else if (incomingPeers.length < 2) { message = serviceHelper.createErrorMessage('Not enough incomming connections'); } else { - message = serviceHelper.createSuccessMessage('Communication to ZelFlux network is properly established'); + message = serviceHelper.createSuccessMessage('Communication to Flux network is properly established'); } res.json(message); } diff --git a/ZelBack/src/services/zelnodeService.js b/ZelBack/src/services/zelnodeService.js index 878015eb4..d04f441a8 100644 --- a/ZelBack/src/services/zelnodeService.js +++ b/ZelBack/src/services/zelnodeService.js @@ -18,10 +18,10 @@ async function updateZelFlux(req, res) { const exec = `cd ${zelnodedpath} && npm run updatezelflux`; cmd.get(exec, (err) => { if (err) { - const errMessage = serviceHelper.createErrorMessage(`Error updating ZelFlux: ${err.message}`, err.name, err.code); + const errMessage = serviceHelper.createErrorMessage(`Error updating Flux: ${err.message}`, err.name, err.code); return res.json(errMessage); } - const message = serviceHelper.createSuccessMessage('ZelFlux successfully updaating'); + const message = serviceHelper.createSuccessMessage('Flux successfully updaating'); return res.json(message); }); } else { @@ -38,10 +38,10 @@ async function hardUpdateZelFlux(req, res) { const exec = `cd ${zelnodedpath} && npm run hardupdatezelflux`; cmd.get(exec, (err) => { if (err) { - const errMessage = serviceHelper.createErrorMessage(`Error hardupdating ZelFlux: ${err.message}`, err.name, err.code); + const errMessage = serviceHelper.createErrorMessage(`Error hardupdating Flux: ${err.message}`, err.name, err.code); return res.json(errMessage); } - const message = serviceHelper.createSuccessMessage('ZelFlux successfully updating'); + const message = serviceHelper.createSuccessMessage('Flux successfully updating'); return res.json(message); }); } else { @@ -58,10 +58,10 @@ async function rebuildZelFront(req, res) { const exec = `cd ${zelnodedpath} && npm run zelfrontbuild`; cmd.get(exec, (err) => { if (err) { - const errMessage = serviceHelper.createErrorMessage(`Error rebuilding ZelFlux: ${err.message}`, err.name, err.code); + const errMessage = serviceHelper.createErrorMessage(`Error rebuilding Flux: ${err.message}`, err.name, err.code); return res.json(errMessage); } - const message = serviceHelper.createSuccessMessage('ZelFlux successfully rebuilt'); + const message = serviceHelper.createSuccessMessage('Flux successfully rebuilt'); return res.json(message); }); } else { diff --git a/ZelFront/public/index.html b/ZelFront/public/index.html index 9d36d7439..51a30c76f 100644 --- a/ZelFront/public/index.html +++ b/ZelFront/public/index.html @@ -5,11 +5,11 @@ - ZelFlux + Flux
diff --git a/ZelFront/src/components/Login.vue b/ZelFront/src/components/Login.vue index e2d4074c1..7ecb328cd 100644 --- a/ZelFront/src/components/Login.vue +++ b/ZelFront/src/components/Login.vue @@ -97,7 +97,7 @@ export default { const isChrome = !!window.chrome; if (!isChrome) { vue.$message({ - message: 'Your browser does not support ZelFluxs websockets. Logging in with Zel ID is not possible. For an optimal experience, please use Chrome or Edge', + message: 'Your browser does not support Flux websockets. Logging in with Zel ID is not possible. For an optimal experience, please use Chrome or Edge', type: 'warning', duration: 0, showClose: true, diff --git a/ZelFront/src/components/ZelAdmin.vue b/ZelFront/src/components/ZelAdmin.vue index 239ef20e7..fd557eff4 100644 --- a/ZelFront/src/components/ZelAdmin.vue +++ b/ZelFront/src/components/ZelAdmin.vue @@ -47,12 +47,12 @@ Logout all sessions -
+
- Update ZelFlux + Update Flux { console.log(response); if (response.data.version !== self.zelfluxVersion) { - vue.$message.success('ZelFlux is now updating in the background'); + vue.$message.success('Flux is now updating in the background'); ZelNodeService.updateZelFlux(zelidauth) .then((responseB) => { console.log(responseB); @@ -250,7 +250,7 @@ export default { vue.$message.error(e.toString()); }); } else { - vue.$message.success('ZelFlux is already up to date.'); + vue.$message.success('Flux is already up to date.'); } }) .catch((error) => { @@ -374,9 +374,9 @@ export default { .then((response) => { console.log(response); if (response.data.version !== self.zelfluxVersion) { - vue.$message.warning('ZelFlux requires an update!'); + vue.$message.warning('Flux requires an update!'); } else { - vue.$message.success('ZelFlux is up to date'); + vue.$message.success('Flux is up to date'); } }) .catch((error) => { diff --git a/ZelFront/src/components/ZelCash.vue b/ZelFront/src/components/ZelCash.vue index af8d2991f..84bfa2ebc 100644 --- a/ZelFront/src/components/ZelCash.vue +++ b/ZelFront/src/components/ZelCash.vue @@ -153,9 +153,9 @@ export default { if (this.getZelNodeStatusResponse.data.status === 'CONFIRMED' || this.getZelNodeStatusResponse.data.location === 'CONFIRMED') { this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode is working correctly'; } else if (this.getZelNodeStatusResponse.data.status === 'STARTED' || this.getZelNodeStatusResponse.data.location === 'STARTED') { - this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode has just been started. ZelFlux is running with limited capabilities.'; + this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode has just been started. Flux is running with limited capabilities.'; } else { - this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode is not confirmed. ZelFlux is running with limited capabilities.'; + this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode is not confirmed. Flux is running with limited capabilities.'; } } }, diff --git a/ZelFront/src/components/ZelNode.vue b/ZelFront/src/components/ZelNode.vue index 2c524a1a0..2c4ad5dc2 100644 --- a/ZelFront/src/components/ZelNode.vue +++ b/ZelFront/src/components/ZelNode.vue @@ -302,9 +302,9 @@ export default { if (this.getZelNodeStatusResponse.data.status === 'CONFIRMED' || this.getZelNodeStatusResponse.data.location === 'CONFIRMED') { this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode is working correctly'; } else if (this.getZelNodeStatusResponse.data.status === 'STARTED' || this.getZelNodeStatusResponse.data.location === 'STARTED') { - this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode has just been started. ZelFlux is running with limited capabilities.'; + this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode has just been started. Flux is running with limited capabilities.'; } else { - this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode is not confirmed. ZelFlux is running with limited capabilities.'; + this.getZelNodeStatusResponse.zelnodeStatus = 'ZelNode is not confirmed. Flux is running with limited capabilities.'; } } }, diff --git a/ZelFront/src/components/shared/Footer.vue b/ZelFront/src/components/shared/Footer.vue index d03b07eb3..65222f023 100644 --- a/ZelFront/src/components/shared/Footer.vue +++ b/ZelFront/src/components/shared/Footer.vue @@ -24,7 +24,7 @@ >The gateway to the Zel Network
@@ -81,9 +81,9 @@ export default { .then((response) => { console.log(response); if (response.data.version !== self.zelfluxVersion) { - vue.$message.warning('ZelFlux needs to be updated!'); + vue.$message.warning('Flux needs to be updated!'); } else { - vue.$message.success('ZelFlux is up to date'); + vue.$message.success('Flux is up to date'); } }) .catch((error) => { diff --git a/ZelFront/src/components/shared/Header.vue b/ZelFront/src/components/shared/Header.vue index 3b9017eef..b95485792 100644 --- a/ZelFront/src/components/shared/Header.vue +++ b/ZelFront/src/components/shared/Header.vue @@ -117,10 +117,10 @@ index="2" :popper-append-to-body=true > - - ZelNode Status - ZelFlux Network - ZelFlux Messages + + Node Status + Flux Network + Flux Messages - Manage ZelFlux + Manage Flux { - it('correctly communicates with ZelFlux and obtains data from a ZelCash service', () => { + it('correctly communicates with Flux and obtains data from a ZelCash service', () => { const data = { hexstring: '0400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff20037a4b0700324d696e6572732068747470733a2f2f326d696e6572732e636f6dffffffff04bf258e9e020000001976a91404e2699cec5f44280540fb752c7660aa3ba857cc88aca0118721000000001976a914c85ea3b8348d148b953b5c659f4f94c8998ffe2488ac601de137000000001976a914cb798afa21a56aab837459959e44ee7bc3c5691188ac80461c86000000001976a91404b74cfe29c810a5d49e8a7cdc0785e46fc4a8c588ac00000000000000000000000000000000000000', }; diff --git a/tests/ZelBack/zelfluxCommunication.js b/tests/ZelBack/zelfluxCommunication.js index b599510ac..4b4060a69 100644 --- a/tests/ZelBack/zelfluxCommunication.js +++ b/tests/ZelBack/zelfluxCommunication.js @@ -6,7 +6,7 @@ const qs = require('qs'); const WebSocket = require('ws'); describe('getFluxMessageSignature', () => { - it('correctly signs zelflux message', async () => { + it('correctly signs Flux message', async () => { const message = 'abc'; const privKey = '5JTeg79dTLzzHXoJPALMWuoGDM8QmLj4n5f6MeFjx8dzsirvjAh'; const signature = await communication.getFluxMessageSignature(message, privKey); @@ -15,7 +15,7 @@ describe('getFluxMessageSignature', () => { expect(signature2).to.be.an('error'); }); - it('correctly verifies zelflux broadcast', async () => { + it('correctly verifies Flux broadcast', async () => { const timeStamp = Date.now(); const version = 1; const privKey = '5JTeg79dTLzzHXoJPALMWuoGDM8QmLj4n5f6MeFjx8dzsirvjAh'; @@ -68,7 +68,7 @@ describe('getFluxMessageSignature', () => { }).timeout(5000); it('establishes websocket connection and sends correct data', async () => { - const data = 'Hello ZelFlux testsuite!'; + const data = 'Hello Flux testsuite!'; const privKey = '5JTeg79dTLzzHXoJPALMWuoGDM8QmLj4n5f6MeFjx8dzsirvjAh'; const messageToSend = await communication.serialiseAndSignZelFluxBroadcast(data, privKey); console.log(messageToSend); @@ -81,7 +81,7 @@ describe('getFluxMessageSignature', () => { websocket.on('message', (msg) => { console.log(msg); const msgZelFlux = msg.split(' ')[0]; - expect(msgZelFlux).to.equal('ZelFlux'); + expect(msgZelFlux).to.equal('Flux'); websocket.close(1000); }); }); diff --git a/zelflux.sh b/zelflux.sh index d31d87538..3ee514412 100644 --- a/zelflux.sh +++ b/zelflux.sh @@ -17,7 +17,7 @@ snap install docker curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - sudo apt-get install nodejs -y -# Clone ZelFlux repo and start ZelFlux +# Clone Flux repo and start Flux git clone https://github.com/zelcash/zelflux && cd zelflux npm start From 55618e9582acce9a98b17ca3b9e9834b47c4c935 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Mon, 22 Jun 2020 10:19:13 +0200 Subject: [PATCH 08/16] logo --- README.md | 6 +- ZelFront/src/assets/img/flux_banner.png | Bin 0 -> 55410 bytes ZelFront/src/assets/img/flux_grey_logo.svg | 64 ++++++++++++++++++++ ZelFront/src/assets/img/flux_white_logo.svg | 64 ++++++++++++++++++++ ZelFront/src/components/shared/Header.vue | 2 +- 5 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 ZelFront/src/assets/img/flux_banner.png create mode 100644 ZelFront/src/assets/img/flux_grey_logo.svg create mode 100644 ZelFront/src/assets/img/flux_white_logo.svg diff --git a/README.md b/README.md index d651466a8..993571eaa 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Flux - Node Daemon -![ZelNode.gif](ZelFront/src/assets/img/zelnode.gif) +![Flux.png](ZelFront/src/assets/img/flux_banner.png) -[![DeepScan grade](https://deepscan.io/api/teams/6436/projects/8442/branches/100920/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=6436&pid=8442&bid=100920) [![CodeFactor](https://www.codefactor.io/repository/github/zelcash/Flux/badge)](https://www.codefactor.io/repository/github/zelcash/Flux) +[![DeepScan grade](https://deepscan.io/api/teams/6436/projects/8442/branches/100920/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=6436&pid=8442&bid=100920) [![CodeFactor](https://www.codefactor.io/repository/github/zelcash/Flux/badge)](https://www.codefactor.io/repository/github/zelcash/zelflux)[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/zelcash/zelflux.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/zelcash/zelflux/context:javascript) ## API Documentation -[API documentation](https://zelcash.github.io/Fluxdocs/) +[API documentation](https://zelcash.github.io/zelfluxdocs/) ## The gateway to the Zel Network diff --git a/ZelFront/src/assets/img/flux_banner.png b/ZelFront/src/assets/img/flux_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..af6bcae7afeee4f69f99927ad8628edc7e60da94 GIT binary patch literal 55410 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 YG + + Flux Logo - White text + + + + + image/svg+xml + + Flux Logo - White text + + + + + + + + + + + + + + diff --git a/ZelFront/src/assets/img/flux_white_logo.svg b/ZelFront/src/assets/img/flux_white_logo.svg new file mode 100644 index 000000000..7e08df99f --- /dev/null +++ b/ZelFront/src/assets/img/flux_white_logo.svg @@ -0,0 +1,64 @@ + + + Flux Logo - White text + + + + + image/svg+xml + + Flux Logo - White text + + + + + + + + + + + + + + diff --git a/ZelFront/src/components/shared/Header.vue b/ZelFront/src/components/shared/Header.vue index b95485792..6285ef60f 100644 --- a/ZelFront/src/components/shared/Header.vue +++ b/ZelFront/src/components/shared/Header.vue @@ -14,7 +14,7 @@ Date: Mon, 22 Jun 2020 10:20:41 +0200 Subject: [PATCH 09/16] fix link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 993571eaa..cf9d43779 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Flux.png](ZelFront/src/assets/img/flux_banner.png) -[![DeepScan grade](https://deepscan.io/api/teams/6436/projects/8442/branches/100920/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=6436&pid=8442&bid=100920) [![CodeFactor](https://www.codefactor.io/repository/github/zelcash/Flux/badge)](https://www.codefactor.io/repository/github/zelcash/zelflux)[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/zelcash/zelflux.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/zelcash/zelflux/context:javascript) +[![DeepScan grade](https://deepscan.io/api/teams/6436/projects/8442/branches/100920/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=6436&pid=8442&bid=100920) [![CodeFactor](https://www.codefactor.io/repository/github/zelcash/zelflux/badge)](https://www.codefactor.io/repository/github/zelcash/zelflux)[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/zelcash/zelflux.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/zelcash/zelflux/context:javascript) ## API Documentation From a09df4cd02699ad8f6b6825a58240561088cbc28 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Mon, 22 Jun 2020 17:45:39 +0200 Subject: [PATCH 10/16] checkAndRequestZelApp --- ZelBack/src/services/explorerService.js | 14 ++- ZelBack/src/services/zelappsService.js | 134 +++++++++++++++++++++--- package.json | 2 +- tests/ZelBack/appsService.js | 14 +++ 4 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 tests/ZelBack/appsService.js diff --git a/ZelBack/src/services/explorerService.js b/ZelBack/src/services/explorerService.js index 528573eee..e7fcc3fd0 100644 --- a/ZelBack/src/services/explorerService.js +++ b/ZelBack/src/services/explorerService.js @@ -278,6 +278,8 @@ async function processBlock(blockHeight) { // normal transactions if (tx.version < 5 && tx.version > 0) { let message = ''; + let isZelAppMessageValue = 0; + const addresses = []; tx.senders.forEach((sender) => { addresses.push(sender.address); @@ -285,6 +287,10 @@ async function processBlock(blockHeight) { tx.vout.forEach((receiver) => { if (receiver.scriptPubKey.addresses) { // count for messages addresses.push(receiver.scriptPubKey.addresses[0]); + if (receiver.scriptPubKey.addresses[0] === config.zelapps.address) { + // it is a zelapp message. Get Satoshi amount + isZelAppMessageValue = receiver.valueSat; + } } if (receiver.scriptPubKey.asm) { message = decodeMessage(receiver.scriptPubKey.asm); // TODO adding messages to database so we can then get all messages from blockchain @@ -303,13 +309,15 @@ async function processBlock(blockHeight) { }); })); // MAY contain ZelApp transaction. Store it. - if (addressesOK.indexOf(config.zelapps.address) > -1 && message.length === 64) { // todo sha256 hash length - const zelappTxRecord = { txid: tx.txid, height: tx.height, zelapphash: message }; - zelappsService.checkAndRequestZelApp(message); + if (isZelAppMessageValue > 0 && message.length === 64) { + const zelappTxRecord = { + txid: tx.txid, height: tx.height, zelapphash: message, value: isZelAppMessageValue, + }; await serviceHelper.insertOneToDatabase(database, zelappsHashesCollection, zelappTxRecord).catch((error) => { db.close(); throw error; }); + zelappsService.checkAndRequestZelApp(message, tx.txid, tx.height, isZelAppMessageValue); } } // tx version 5 are zelnode transactions. Put them into zelnode diff --git a/ZelBack/src/services/zelappsService.js b/ZelBack/src/services/zelappsService.js index d84e9143a..3bddce676 100644 --- a/ZelBack/src/services/zelappsService.js +++ b/ZelBack/src/services/zelappsService.js @@ -2496,17 +2496,42 @@ async function installedZelApps(req, res) { } } -async function calculateZelAmountForApp(cpu, ram, hdd) { - const cpuPrice = config.zelapps.price.cpu; - const ramPrice = config.zelapps.price.ram; - const hddPrice = config.zelapps.price.hdd; +async function requestZelAppMessage(hash) { + // some message type request zelapp message, message hash + // peer responds with data from permanent database or temporary database. If does not have it requests further + console.log(hash); + // TODO request a zelapp message from all peers +} - const cpuTotal = serviceHelper.ensureNumber(cpu) * cpuPrice; - const ramTotal = serviceHelper.ensureNumber(ram) * ramPrice; - const hddTotal = serviceHelper.ensureNumber(hdd) * hddPrice; +async function storeZelAppPermanentMessage(message) { + /* message object + * @param type string + * @param version number + * @param zelAppSpecifications object + * @param hash string + * @param timestamp number + * @param signature string + * @param txid string + * @param height number + * @param valueSat number + */ + if (typeof message !== 'object' && typeof message.type !== 'string' && typeof message.version !== 'number' && typeof message.zelAppSpecifications !== 'object' && typeof message.signature !== 'string' + && typeof message.timestamp !== 'number' && typeof message.hash !== 'string' && typeof message.txid !== 'string' && typeof message.height !== 'number' && typeof message.valueSat !== 'number') { + return new Error('Invalid ZelApp message for storing'); + } - const total = cpuTotal + ramTotal + hddTotal; - return total; + const db = await serviceHelper.connectMongoDb(mongoUrl).catch((error) => { + log.error(error); + throw error; + }); + const database = db.db(config.database.zelappsglobal.database); + await serviceHelper.insertOneToDatabase(database, globalZelAppsMessages, message).catch((error) => { + log.error(error); + db.close(); + throw error; + }); + db.close(); + return true; } async function checkZelAppMessageExistence(zelapphash) { @@ -2514,9 +2539,21 @@ async function checkZelAppMessageExistence(zelapphash) { const dbopen = await serviceHelper.connectMongoDb(mongoUrl).catch((error) => { throw error; }); - const zelappsDatabase = dbopen.db(config.database.zelappslocal.database); - const zelappsQuery = { zelapphash }; + const zelappsDatabase = dbopen.db(config.database.zelappsglobal.database); + const zelappsQuery = { hash: zelapphash }; const zelappsProjection = {}; + // a permanent global zelappmessage looks like this: + // const permanentZelAppMessage = { + // type: messageType, + // version: typeVersion, + // zelAppSpecifications: zelAppSpecFormatted, + // hash: messageHASH, + // timestamp, + // signature, + // txid, + // height, + // valueSat, + // }; const zelappResult = await serviceHelper.findOneInDatabase(zelappsDatabase, globalZelAppsMessages, zelappsQuery, zelappsProjection).catch((error) => { dbopen.close(); throw error; @@ -2532,12 +2569,78 @@ async function checkZelAppMessageExistence(zelapphash) { } } -async function checkAndRequestZelApp(zelapphash) { +async function checkZelAppTemporaryMessageExistence(zelapphash) { + try { + const dbopen = await serviceHelper.connectMongoDb(mongoUrl).catch((error) => { + throw error; + }); + const zelappsDatabase = dbopen.db(config.database.zelappsglobal.database); + const zelappsQuery = { hash: zelapphash }; + const zelappsProjection = {}; + // a temporary zelappmessage looks like this: + // const newMessage = { + // zelAppSpecifications: message.zelAppSpecifications, + // type: message.type, + // version: message.version, + // hash: message.hash, + // timestamp: message.timestamp, + // signature: message.signature, + // createdAt: new Date(message.timestamp), + // expireAt: new Date(validTill), + // }; + const zelappResult = await serviceHelper.findOneInDatabase(zelappsDatabase, globalZelAppsTempMessages, zelappsQuery, zelappsProjection).catch((error) => { + dbopen.close(); + throw error; + }); + dbopen.close(); + if (zelappResult) { + return zelappResult; + } + return false; + } catch (error) { + log.error(error); + return error; + } +} + +// hash of zelapp information, txid it was in, height of blockchain containing the txid +async function checkAndRequestZelApp(zelapphash, txid, height, valueSat, i = 0) { try { const appMessageExists = await checkZelAppMessageExistence(zelapphash); - if (!appMessageExists) { - // we surely do not have that message. - // request the message and broadcast the message further to our connected peers. + if (appMessageExists === false) { // otherwise do nothing + // we surely do not have that message in permanent storaage. + // check temporary message storage + // if we have it in temporary storage, get the temporary message + const tempMessage = await checkZelAppTemporaryMessageExistence(zelapphash); + if (tempMessage) { + // check if value is optimal or higher + const appPrice = await appPricePerMonth(tempMessage.zelAppSpecifications); + if (valueSat >= appPrice * 1e8) { + // if all ok. store it as permanent zelapp message + const permanentZelAppMessage = { + type: tempMessage.type, + version: tempMessage.version, + zelAppSpecifications: tempMessage.zelAppSpecifications, + hash: tempMessage.hash, + timestamp: tempMessage.timestamp, + signature: tempMessage.signature, + txid: serviceHelper.ensureString(txid), + height: serviceHelper.ensureNumber(height), + valueSat: serviceHelper.ensureNumber(valueSat), + }; + storeZelAppPermanentMessage(permanentZelAppMessage); + } // else do nothing + } else { + // TODO request the message and broadcast the message further to our connected peers. + requestZelAppMessage(zelapphash); + // rerun this after 1 min delay + // stop this loop after 1 hour, as it might be a scammy message or simply this message is nowhere on the network + if (i < 60) { + await serviceHelper.delay(60 * 1000); + checkAndRequestZelApp(zelapphash, txid, height, valueSat, i + 1); + } + // TODO additional constant requesting of missing zelapp messages + } } } catch (error) { log.error(error); @@ -2624,7 +2727,6 @@ module.exports = { installedZelApps, availableZelApps, zelappsResources, - calculateZelAmountForApp, checkZelAppMessageExistence, checkAndRequestZelApp, checkDockerAccessibility, diff --git a/package.json b/package.json index 67ef7b950..5038b5893 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zelflux", - "version": "0.62.0", + "version": "0.63.0", "description": "Flux - Node Daemon. The entrace to the Flux network.", "repository": { "type": "git", diff --git a/tests/ZelBack/appsService.js b/tests/ZelBack/appsService.js new file mode 100644 index 000000000..f84ae55cc --- /dev/null +++ b/tests/ZelBack/appsService.js @@ -0,0 +1,14 @@ +const appService = require("../../ZelBack/src/services/zelappsService"); +const chai = require('chai'); +const expect = chai.expect; + +// describe('checkAndRequestZelApp', () => { +// it('signs checks and requests app properly', async () => { +// const zelapphash = 'abc'; +// const txid = '5JTeg79dTLzzHXoJPALMWuoGDM8QmLj4n5f6MeFjx8dzsirvjAh'; +// const height = 33; +// valueSat = 33 * 1e8; +// const abc = await appService.checkAndRequestZelApp(zelapphash, txid, height, valueSat); +// expect(abc).to.equal('abc'); +// }); +// }); \ No newline at end of file From c06fa0a65a433beb7f1bd37deeebd26bf81a58a3 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Mon, 22 Jun 2020 17:47:43 +0200 Subject: [PATCH 11/16] remove await --- ZelBack/src/services/zelappsService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZelBack/src/services/zelappsService.js b/ZelBack/src/services/zelappsService.js index 3bddce676..515dd6c9a 100644 --- a/ZelBack/src/services/zelappsService.js +++ b/ZelBack/src/services/zelappsService.js @@ -2614,7 +2614,7 @@ async function checkAndRequestZelApp(zelapphash, txid, height, valueSat, i = 0) const tempMessage = await checkZelAppTemporaryMessageExistence(zelapphash); if (tempMessage) { // check if value is optimal or higher - const appPrice = await appPricePerMonth(tempMessage.zelAppSpecifications); + const appPrice = appPricePerMonth(tempMessage.zelAppSpecifications); if (valueSat >= appPrice * 1e8) { // if all ok. store it as permanent zelapp message const permanentZelAppMessage = { From cc624b880c33f83711f1861dfd8cd4c3d13d1335 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Tue, 23 Jun 2020 20:35:19 +0200 Subject: [PATCH 12/16] correct receivedAt and validTilll in temp messages so it is always when we receive the message and not on first broadcast requestZelAppMessage if we do not have a message, request it from other peers checks of message existance now returns the message proper sending message to WS wsList for sending to outgoing zelapprequest type of message responds to zelappregister of outgoing connection is the some as of inc --- ZelBack/src/services/zelappsService.js | 18 +++-- ZelBack/src/services/zelfluxCommunication.js | 85 +++++++++++++++++--- 2 files changed, 89 insertions(+), 14 deletions(-) diff --git a/ZelBack/src/services/zelappsService.js b/ZelBack/src/services/zelappsService.js index 515dd6c9a..6b311675b 100644 --- a/ZelBack/src/services/zelappsService.js +++ b/ZelBack/src/services/zelappsService.js @@ -2082,7 +2082,8 @@ async function storeZelAppTemporaryMessage(message, furtherVerification = false) await verifyZelAppMessageSignature(message.type, message.version, message.zelAppSpecifications, message.timestamp, message.signature); } - const validTill = message.timestamp + (60 * 60 * 1000); // 60 minutes + const receivedAt = Date.now(); + const validTill = receivedAt + (60 * 60 * 1000); // 60 minutes const db = await serviceHelper.connectMongoDb(mongoUrl).catch((error) => { log.error(error); @@ -2097,7 +2098,7 @@ async function storeZelAppTemporaryMessage(message, furtherVerification = false) hash: message.hash, timestamp: message.timestamp, signature: message.signature, - createdAt: new Date(message.timestamp), + receivedAt: new Date(receivedAt), expireAt: new Date(validTill), }; const value = newMessage; @@ -2323,7 +2324,7 @@ async function registerZelAppGlobalyApi(req, res) { const messageHASH = await messageHash(message); const responseHash = serviceHelper.createDataMessage(messageHASH); // now all is great. Store zelAppSpecFormatted, timestamp, signature and hash in zelappsTemporaryMessages. with 1 hours expiration time. Broadcast this message to all outgoing connections. - const temporaryZelAppMessage = { + const temporaryZelAppMessage = { // specification of temp message type: messageType, version: typeVersion, zelAppSpecifications: zelAppSpecFormatted, @@ -2500,7 +2501,14 @@ async function requestZelAppMessage(hash) { // some message type request zelapp message, message hash // peer responds with data from permanent database or temporary database. If does not have it requests further console.log(hash); - // TODO request a zelapp message from all peers + const message = { + type: 'zelapprequest', + version: 1, + hash, + }; + await zelfluxCommunication.broadcastMessageToOutgoing(message); + await serviceHelper.delay(2345); + await zelfluxCommunication.broadcastMessageToIncoming(message); } async function storeZelAppPermanentMessage(message) { @@ -2560,7 +2568,7 @@ async function checkZelAppMessageExistence(zelapphash) { }); dbopen.close(); if (zelappResult) { - return true; + return zelappResult; } return false; } catch (error) { diff --git a/ZelBack/src/services/zelfluxCommunication.js b/ZelBack/src/services/zelfluxCommunication.js index 73aba955c..2b436f925 100644 --- a/ZelBack/src/services/zelfluxCommunication.js +++ b/ZelBack/src/services/zelfluxCommunication.js @@ -303,7 +303,8 @@ async function handleZelAppRegisterMessage(message, fromIP) { const rebroadcastToPeers = await zelappsService.storeZelAppTemporaryMessage(message.data, true); if (rebroadcastToPeers === true) { const messageString = serviceHelper.ensureString(message); - sendToAllPeers(messageString); + const wsListOut = outgoingConnections.filter((client) => client._socket.remoteAddress !== fromIP); + sendToAllPeers(messageString, wsListOut); await serviceHelper.delay(2345); const wsList = incomingConnections.filter((client) => client._socket.remoteAddress !== fromIP); sendToAllIncomingConnections(messageString, wsList); @@ -313,6 +314,70 @@ async function handleZelAppRegisterMessage(message, fromIP) { } } +async function sendMessageToWS(message, ws) { + try { + const pongResponse = await serialiseAndSignZelFluxBroadcast(message); + ws.send(pongResponse); + } catch (error) { + log.error(error); + } +} + +async function respondWithAppMessage(message, ws) { + // check if we have it database of permanent zelappMessages + // eslint-disable-next-line global-require + const zelappsService = require('./zelappsService'); + const permanentMessage = await zelappsService.checkZelAppMessageExistence(message.data.hash); + if (permanentMessage) { + // message exists in permanent storage. Create a message and broadcast it to the fromIP peer + // const permanentZelAppMessage = { + // type: messageType, + // version: typeVersion, + // zelAppSpecifications: zelAppSpecFormatted, + // hash: messageHASH, + // timestamp, + // signature, + // txid, + // height, + // valueSat, + // }; + const temporaryZelAppMessage = { // specification of temp message + type: permanentMessage.type, + version: permanentMessage.version, + zelAppSpecifications: permanentMessage.zelAppSpecifications, + hash: permanentMessage.hash, + timestamp: permanentMessage.timestamp, + signature: permanentMessage.signature, + }; + sendMessageToWS(temporaryZelAppMessage, ws); + } else { + const existingTemporaryMessage = await zelappsService.checkZelAppTemporaryMessageExistence(message.data.hash); + if (existingTemporaryMessage) { + // a temporary zelappmessage looks like this: + // const newMessage = { + // zelAppSpecifications: message.zelAppSpecifications, + // type: message.type, + // version: message.version, + // hash: message.hash, + // timestamp: message.timestamp, + // signature: message.signature, + // createdAt: new Date(message.timestamp), + // expireAt: new Date(validTill), + // }; + const temporaryZelAppMessage = { // specification of temp message + type: existingTemporaryMessage.type, + version: existingTemporaryMessage.version, + zelAppSpecifications: existingTemporaryMessage.zelAppSpecifications, + hash: existingTemporaryMessage.hash, + timestamp: existingTemporaryMessage.timestamp, + signature: existingTemporaryMessage.signature, + }; + sendMessageToWS(temporaryZelAppMessage, ws); + } + // else do nothing. We do not have this message. And this Flux would be requesting it from other peers soon too. + } +} + // eslint-disable-next-line no-unused-vars function handleIncomingConnection(ws, req, expressWS) { // now we are in connections state. push the websocket to our incomingconnections @@ -333,8 +398,9 @@ function handleIncomingConnection(ws, req, expressWS) { const msgObj = serviceHelper.ensureObject(msg); if (msgObj.data.type === 'zelappregister') { handleZelAppRegisterMessage(msgObj, peer.ip); - } - if (msgObj.data.type === 'HeartBeat' && msgObj.data.message === 'ping') { // we know that data exists + } else if (msgObj.data.type === 'zelapprequest') { + respondWithAppMessage(msgObj, ws); + } else if (msgObj.data.type === 'HeartBeat' && msgObj.data.message === 'ping') { // we know that data exists const newMessage = msgObj.data; newMessage.message = 'pong'; const pongResponse = await serialiseAndSignZelFluxBroadcast(newMessage); @@ -618,16 +684,17 @@ async function initiateAndHandleConnection(ip) { const currentTimeStamp = Date.now(); // ms const messageOK = await verifyOriginalFluxBroadcast(evt.data, undefined, currentTimeStamp); if (messageOK === true) { + const { url } = websocket; + let conIP = url.split('/')[2]; + conIP = conIP.split(`:${config.server.apiport}`).join(''); const msgObj = serviceHelper.ensureObject(evt.data); if (msgObj.data.type === 'zelappregister') { - // do not interact on zelappregister message received from an outgoing connection - } - if (msgObj.data.type === 'HeartBeat' && msgObj.data.message === 'pong') { + handleZelAppRegisterMessage(msgObj.data, conIP); + } else if (msgObj.data.type === 'zelapprequest') { + respondWithAppMessage(msgObj, websocket); + } else if (msgObj.data.type === 'HeartBeat' && msgObj.data.message === 'pong') { const newerTimeStamp = Date.now(); // ms, get a bit newer time that has passed verification of broadcast const rtt = newerTimeStamp - msgObj.data.timestamp; - const { url } = websocket; - let conIP = url.split('/')[2]; - conIP = conIP.split(`:${config.server.apiport}`).join(''); const foundPeer = outgoingPeers.find((peer) => peer.ip === conIP); if (foundPeer) { const peerIndex = outgoingPeers.indexOf(foundPeer); From 0da86b7b8d77c2994cdc3c990f4cd3f8845b3d8e Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Wed, 24 Jun 2020 12:25:54 +0200 Subject: [PATCH 13/16] zelapp service tests --- ZelBack/src/services/zelappsService.js | 10 +- tests/ZelBack/appsService.js | 282 ++++++++++++++++++++++++- 2 files changed, 289 insertions(+), 3 deletions(-) diff --git a/ZelBack/src/services/zelappsService.js b/ZelBack/src/services/zelappsService.js index 6b311675b..ee47ff661 100644 --- a/ZelBack/src/services/zelappsService.js +++ b/ZelBack/src/services/zelappsService.js @@ -1981,7 +1981,7 @@ async function verifyZelAppHash(message) { if (messageHASH !== message.hash) { throw new Error('Invalid ZelApp hash received!'); } - return 0; + return true; } async function verifyZelAppMessageSignature(type, version, zelAppSpec, timestamp, signature) { @@ -2024,6 +2024,7 @@ async function verifyRepository(repotag) { } else { throw new Error('Repository is not in valid format namespace/repository:tag'); } + return true; } async function verifyZelAppSpecifications(zelAppSpecifications) { @@ -2639,7 +2640,7 @@ async function checkAndRequestZelApp(zelapphash, txid, height, valueSat, i = 0) storeZelAppPermanentMessage(permanentZelAppMessage); } // else do nothing } else { - // TODO request the message and broadcast the message further to our connected peers. + // request the message and broadcast the message further to our connected peers. requestZelAppMessage(zelapphash); // rerun this after 1 min delay // stop this loop after 1 hour, as it might be a scammy message or simply this message is nowhere on the network @@ -2742,4 +2743,9 @@ module.exports = { appPricePerMonth, getZelAppsTemporaryMessages, storeZelAppTemporaryMessage, + verifyRepository, + checkHWParameters, + messageHash, + verifyZelAppHash, + verifyZelAppMessageSignature, }; diff --git a/tests/ZelBack/appsService.js b/tests/ZelBack/appsService.js index f84ae55cc..96d86a865 100644 --- a/tests/ZelBack/appsService.js +++ b/tests/ZelBack/appsService.js @@ -1,3 +1,4 @@ +process.env.NODE_CONFIG_DIR = `${process.cwd()}/ZelBack/config/`; const appService = require("../../ZelBack/src/services/zelappsService"); const chai = require('chai'); const expect = chai.expect; @@ -11,4 +12,283 @@ const expect = chai.expect; // const abc = await appService.checkAndRequestZelApp(zelapphash, txid, height, valueSat); // expect(abc).to.equal('abc'); // }); -// }); \ No newline at end of file +// }); + +describe('checkHWParameters', () => { + it('Verifies HW specs are correct', () => { + const zelAppSpecs = { + "version": 1, + "name": "FoldingAtHome", + "description": "Folding @ Home is cool :)", + "repotag": "yurinnick/folding-at-home:latest", + "owner": "1CbErtneaX2QVyUfwU7JGB7VzvPgrgc3uC", + "port": 30001, + "enviromentParameters": [ + "USER=foldingUser", + "TEAM=262156", + "ENABLE_GPU=false", + "ENABLE_SMP=true" + ], + "commands": [ + "--allow", + "0/0", + "--web-allow", + "0/0" + ], + "containerPort": 7396, + "containerData": "/config", + "cpu": 0.5, + "ram": 500, + "hdd": 5, + "tiered": true, + "cpubasic": 0.5, + "cpusuper": 1, + "cpubamf": 2, + "rambasic": 500, + "ramsuper": 1000, + "rambamf": 2000, + "hddbasic": 5, + "hddsuper": 5, + "hddbamf": 5 + }; + expect(appService.checkHWParameters(zelAppSpecs)).to.be.equal(true); + }); + + it('Verifies HW specs are badly asssigned', () => { + const zelAppSpecs = { + "version": 1, + "name": "FoldingAtHome", + "description": "Folding @ Home is cool :)", + "repotag": "yurinnick/folding-at-home:latest", + "owner": "1CbErtneaX2QVyUfwU7JGB7VzvPgrgc3uC", + "port": 30001, + "enviromentParameters": [ + "USER=foldingUser", + "TEAM=262156", + "ENABLE_GPU=false", + "ENABLE_SMP=true" + ], + "commands": [ + "--allow", + "0/0", + "--web-allow", + "0/0" + ], + "containerPort": 7396, + "containerData": "/config", + "cpu": 0.5, + "ram": 'badly assigned', + "hdd": 5, + "tiered": true, + "cpubasic": 0.5, + "cpusuper": 1, + "cpubamf": 2, + "rambasic": 500, + "ramsuper": 1000, + "rambamf": 2000, + "hddbasic": 5, + "hddsuper": 5, + "hddbamf": 5 + }; + expect(appService.checkHWParameters(zelAppSpecs)).to.be.an('error'); + }); + + it('Verifies HW specs are missing', () => { + const zelAppSpecs = { + "version": 1, + "name": "FoldingAtHome", + "description": "Folding @ Home is cool :)", + "repotag": "yurinnick/folding-at-home:latest", + "owner": "1CbErtneaX2QVyUfwU7JGB7VzvPgrgc3uC", + "port": 30001, + "enviromentParameters": [ + "USER=foldingUser", + "TEAM=262156", + "ENABLE_GPU=false", + "ENABLE_SMP=true", + ], + "commands": [ + "--allow", + "0/0", + "--web-allow", + "0/0" + ], + "containerPort": 7396, + "containerData": "/config", + "cpu": null, + "ram": 4000, + "hdd": 5, + "tiered": true, + "cpubasic": 0.5, + "cpusuper": 1, + "cpubamf": 2, + "rambasic": 500, + "ramsuper": 1000, + "rambamf": 2000, + "hddbasic": 5, + "hddsuper": 5, + "hddbamf": 21 + }; + const hwSpecs = appService.checkHWParameters(zelAppSpecs); + expect(hwSpecs).to.be.an('error'); + }); + + it('Verifies repository exists or is not correct', async () => { + const zelAppSpecs = { + "repotag": "yurinnick/folding-at-home:latest", + "repotagB": "yurinnick/folding-at-home:latestaaa", + }; + const type = 'zelappregister'; + const version = 1; + const timestamp = 1592988806887 + const dataToSign = 'zelappregister1{"version":1,"name":"FoldingAtHome","description":"Folding @ Home is cool :)","repotag":"yurinnick/folding-at-home:latest","owner":"1CbErtneaX2QVyUfwU7JGB7VzvPgrgc3uC","port":30001,"enviromentParameters":["USER=foldingUser","TEAM=262156","ENABLE_GPU=false","ENABLE_SMP=true"],"commands":["--allow","0/0","--web-allow","0/0"],"containerPort":7396,"containerData":"/config","cpu":0.5,"ram":500,"hdd":5,"tiered":true,"cpubasic":0.5,"cpusuper":1,"cpubamf":2,"rambasic":500,"ramsuper":1000,"rambamf":2000,"hddbasic":5,"hddsuper":5,"hddbamf":5}1592988806887'; + const signature = 'HxgaYStMPP/Als06OmDJltVyp/7bcV8mEjwXictlgZKnTQJoxf0eIA/np2q6OSrkQx6IB1ksiS+71uYEOIHrFvw='; + const repA = await appService.verifyRepository(zelAppSpecs.repotag); + expect(repA).to.be.equal(true); + const repB = await appService.verifyRepository(zelAppSpecs.repotagB).catch((error) => { + expect(error.message).to.be.equal('Repository is not in valid format namespace/repository:tag') + }); + expect(repB).to.be.equal(undefined) + // expect(appService.verifyZelAppSpecifications(zelAppSpecs)).to.not.throw(); + }); + + it('Message Hash is correctly calculated', async () => { + const zelAppSpecs = { + "version": 1, + "name": "FoldingAtHome", + "description": "Folding @ Home is cool :)", + "repotag": "yurinnick/folding-at-home:latest", + "owner": "1CbErtneaX2QVyUfwU7JGB7VzvPgrgc3uC", + "port": 30001, + "enviromentParameters": [ + "USER=foldingUser", + "TEAM=262156", + "ENABLE_GPU=false", + "ENABLE_SMP=true" + ], + "commands": [ + "--allow", + "0/0", + "--web-allow", + "0/0" + ], + "containerPort": 7396, + "containerData": "/config", + "cpu": 0.5, + "ram": 500, + "hdd": 5, + "tiered": true, + "cpubasic": 0.5, + "cpusuper": 1, + "cpubamf": 2, + "rambasic": 500, + "ramsuper": 1000, + "rambamf": 2000, + "hddbasic": 5, + "hddsuper": 5, + "hddbamf": 5 + }; + const type = 'zelappregister'; + const version = 1; + const timestamp = 1592988806887 + const signature = 'HxgaYStMPP/Als06OmDJltVyp/7bcV8mEjwXictlgZKnTQJoxf0eIA/np2q6OSrkQx6IB1ksiS+71uYEOIHrFvw='; + const messageHash = 'd77a4ac4580391fb1122e43cc32d5899eeb1ca655f6bf11d0ef2639a4cf2cd94' + const message = type + version + JSON.stringify(zelAppSpecs) + timestamp + signature; + expect(await appService.messageHash(message)).to.be.equal(messageHash); + }); + + it('Message Hash is correctly verified', async () => { + const zelAppSpecs = { + "version": 1, + "name": "FoldingAtHome", + "description": "Folding @ Home is cool :)", + "repotag": "yurinnick/folding-at-home:latest", + "owner": "1CbErtneaX2QVyUfwU7JGB7VzvPgrgc3uC", + "port": 30001, + "enviromentParameters": [ + "USER=foldingUser", + "TEAM=262156", + "ENABLE_GPU=false", + "ENABLE_SMP=true" + ], + "commands": [ + "--allow", + "0/0", + "--web-allow", + "0/0" + ], + "containerPort": 7396, + "containerData": "/config", + "cpu": 0.5, + "ram": 500, + "hdd": 5, + "tiered": true, + "cpubasic": 0.5, + "cpusuper": 1, + "cpubamf": 2, + "rambasic": 500, + "ramsuper": 1000, + "rambamf": 2000, + "hddbasic": 5, + "hddsuper": 5, + "hddbamf": 5 + }; + const type = 'zelappregister'; + const version = 1; + const timestamp = 1592988806887 + const signature = 'HxgaYStMPP/Als06OmDJltVyp/7bcV8mEjwXictlgZKnTQJoxf0eIA/np2q6OSrkQx6IB1ksiS+71uYEOIHrFvw='; + const messageHash = 'd77a4ac4580391fb1122e43cc32d5899eeb1ca655f6bf11d0ef2639a4cf2cd94'; + const message = { + type, + version, + hash: messageHash, + zelAppSpecifications: zelAppSpecs, + timestamp, + signature, + } + expect(await appService.verifyZelAppHash(message)).to.be.equal(true); + }); + + it('Message is correctly signed', async () => { + const zelAppSpecs = { + "version": 1, + "name": "FoldingAtHome", + "description": "Folding @ Home is cool :)", + "repotag": "yurinnick/folding-at-home:latest", + "owner": "1CbErtneaX2QVyUfwU7JGB7VzvPgrgc3uC", + "port": 30001, + "enviromentParameters": [ + "USER=foldingUser", + "TEAM=262156", + "ENABLE_GPU=false", + "ENABLE_SMP=true" + ], + "commands": [ + "--allow", + "0/0", + "--web-allow", + "0/0" + ], + "containerPort": 7396, + "containerData": "/config", + "cpu": 0.5, + "ram": 500, + "hdd": 5, + "tiered": true, + "cpubasic": 0.5, + "cpusuper": 1, + "cpubamf": 2, + "rambasic": 500, + "ramsuper": 1000, + "rambamf": 2000, + "hddbasic": 5, + "hddsuper": 5, + "hddbamf": 5 + }; + const type = 'zelappregister'; + const version = 1; + const timestamp = 1592988806887 + const signature = 'HxgaYStMPP/Als06OmDJltVyp/7bcV8mEjwXictlgZKnTQJoxf0eIA/np2q6OSrkQx6IB1ksiS+71uYEOIHrFvw='; + expect(await appService.verifyZelAppMessageSignature(type, version, zelAppSpecs, timestamp, signature)).to.be.equal(true); + }); +}); From 3942d91818afadaffe85ea54a5a8cfb6e5ba3b8c Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Wed, 24 Jun 2020 13:55:51 +0200 Subject: [PATCH 14/16] do deeper restoring of database on errors, initial load as of reorg possibility --- ZelBack/src/services/explorerService.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ZelBack/src/services/explorerService.js b/ZelBack/src/services/explorerService.js index e7fcc3fd0..839d177b3 100644 --- a/ZelBack/src/services/explorerService.js +++ b/ZelBack/src/services/explorerService.js @@ -527,8 +527,24 @@ async function initiateBlockProcessor(restoreDatabase) { } if (scannedBlockHeight !== 0 && restoreDatabase) { try { - await restoreDatabaseToBlockheightState(scannedBlockHeight); - log.info('Database restored OK'); + // adjust for initial reorg + if (zelcashHeight < scannedBlockHeight + 100) { + // we are less than 100 blocks from zelcash height. Do deep restoring + scannedBlockHeight = Math.max(scannedBlockHeight - 100, 0); + await restoreDatabaseToBlockheightState(scannedBlockHeight); + const queryHeight = { generalScannedHeight: { $gte: 0 } }; + const update = { $set: { generalScannedHeight: scannedBlockHeight } }; + const options = { upsert: true }; + await serviceHelper.findOneAndUpdateInDatabase(database, scannedHeightCollection, queryHeight, update, options).catch((error) => { + db.close(); + throw error; + }); + log.info('Database restored OK'); + } else { + // we are more than 100 blocks from zelcash. No need for deep restoring + await restoreDatabaseToBlockheightState(scannedBlockHeight); + log.info('Database restored OK'); + } } catch (e) { log.error('Error restoring database!'); throw e; From 0c509bda6ae307b6c757b2369eec972c1014ea68 Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Tue, 30 Jun 2020 16:44:43 +0200 Subject: [PATCH 15/16] do not initiate block processing --- ZelBack/src/services/zelfluxCommunication.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ZelBack/src/services/zelfluxCommunication.js b/ZelBack/src/services/zelfluxCommunication.js index 2b436f925..2d8fa7601 100644 --- a/ZelBack/src/services/zelfluxCommunication.js +++ b/ZelBack/src/services/zelfluxCommunication.js @@ -1198,8 +1198,8 @@ function startFluxFunctions() { checkDeterministicNodesCollisions(); }, 60000); log.info('Flux checks operational'); - explorerService.initiateBlockProcessor(true); - log.info('Flux Block Explorer Service started'); + // explorerService.initiateBlockProcessor(true); + // log.info('Flux Block Explorer Service started'); } module.exports = { From 2eb748b52a3a8037ed3c39031f43599e5bede50d Mon Sep 17 00:00:00 2001 From: TheTrunk Date: Tue, 30 Jun 2020 16:55:36 +0200 Subject: [PATCH 16/16] comment explorer service --- ZelBack/src/services/zelfluxCommunication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ZelBack/src/services/zelfluxCommunication.js b/ZelBack/src/services/zelfluxCommunication.js index 2d8fa7601..25e49767a 100644 --- a/ZelBack/src/services/zelfluxCommunication.js +++ b/ZelBack/src/services/zelfluxCommunication.js @@ -9,7 +9,7 @@ const log = require('../lib/log'); const serviceHelper = require('./serviceHelper'); const zelcashService = require('./zelcashService'); const userconfig = require('../../../config/userconfig'); -const explorerService = require('./explorerService'); +// const explorerService = require('./explorerService'); const outgoingConnections = []; // websocket list const outgoingPeers = []; // array of objects containing ip and rtt latency