Merge pull request #4 from ranchimall/master

This commit is contained in:
Sai Raj 2023-02-11 19:58:26 +05:30 committed by GitHub
commit b5d123dfed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 13346 additions and 33014 deletions

View File

@ -1,3 +0,0 @@
SERVER_PWD=<server_password>
BROWSER=firefox
PORT=7130

6
.gitattributes vendored
View File

@ -1,6 +0,0 @@
*.gitattributes linguist-vendored
util/mongoose.c linguist-vendored
util/mongoose.h linguist-vendored
.gitattributes export-ignore
util/* export-ignore

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/node_modules/
/package-lock.json
/args/config.json
/args/param.json
/args/keys.json
/log.txt
/bash_start*
*test*
*.tmp*

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 RanchiMall
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,23 +1,62 @@
# SuperNodeStorage
FLO Supernode Storage is a Cloud Storage progam for FLO Dapps
# SuperNode Storage
## Installation
1. Download or clone the [repo](https://github.com/ranchimall/SuperNodeStorage):
git clone https://github.com/ranchimall/SuperNodeStorage
2. Add a strong <server_password> in `.config` file
3. Change other configurations (if needed)
4. Host and publish the domain name or IP with port
### Pre-requisite
- [X] Nodejs `version >= 12.9` (`--lts` recommended)
- [X] MySQL Server `version > 8.0`
## Usage
1. Start the app using the following command in terminal. The server WSS will be started and the supernode html-js will be opened in the browser.
### Download
Download the repository using git:
```
git clone https://github.com/ranchimall/SuperNodeStorage.git
```
./start_supernode.sh
2. (Only for first time login) Enter the <server_password> and <private_key> when prompted
### Install
Install using npm:
```
cd SuperNodeStorage
npm install
```
Finish the configuration when prompted
The Supernode storage will automatically start
### Configuration
NOTE: The <server_password> and <private_key> will be stored securedly in IndexedDB of the browser
#### General Configuration
If not finished during installation, or to re-configure use:
```
npm run configure
```
- **port**: Port of the server to run on
- **MySQL host**: Host of the MySQL server (default: ***localhost***)
- **Database name**: Database in which the data should be stored (`<database-name>`) (default: ***supernode***)
- **MySQL username**: Username for MySQL (`<sql-username>`)
- **MySQL password**: Password for MySQL (`<sql-password>`)
NOTE: Users may add `start_supernode` to bootup process to automatically start the supernode during boot up
***Recommended*** *(optional)* Create and use a MySQL user instead of root. Remember to give access to the database to the user.
#### Set/Reset Node key password
If not set during installation, or to reset password, use:
```
npm run reset-password
```
- **private key**: Private key of the node
- **password**: Password to set for the node (`<password>`)
**Note**: Private key of the node is encrypted using the `<password>`. Thus use a ***strong*** password.
### More
For help or list of all commands, use
```
npm run help
```
## Starting the Server
After successful installation and configuration using the above steps, SuperNodeStorage can be started using:
```
npm start -- -PASSWORD=<password>
```
##
For more information and detailed installation, check the wiki [here](https://github.com/ranchimall/SuperNodeStorage/wiki).

File diff suppressed because it is too large Load Diff

Binary file not shown.

144
args/gen-param.html Normal file
View File

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RanchiMall Blockchain Data Cloud</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
font-family: 'Inter', sans-serif;
}
body {
background-color: #FAF3DD;
}
#main_header{
display: flex;
padding: 1rem 1.5rem;
align-items: center;
justify-content: start;
}
#main_header__logo{
height: 2rem;
width: 2rem;
}
section{
display: grid;
place-content: center;
padding: 3rem 1.5rem;
text-align: center;
}
h1{
margin-top: 3rem;
margin-bottom: 0.5rem;
color: #065C6F;
}
p{
margin-bottom: 3rem;
}
.download-icon{
height: 2.6rem;
width: 2.6rem;
padding: 1rem;
border-radius: 50%;
margin-bottom: 0.5rem;
fill: teal;
background-color: rgba(0,0,0, 0.1);
}
h3{
margin-bottom: 1rem;
}
strong{
display: flex;
padding: 0.6rem 0.8rem;
border-radius: 0.5rem;
background-color: #FFD369;
margin: 1.5rem 0;
}
a{
padding: 0.3rem 0.5rem;
text-decoration: none;
font-weight: 500;
background-color: rgba(0,0,0, 0.1);
border-radius: 0.3rem;
}
</style>
</head>
<body>
<header id="main_header">
<svg id="main_header__logo" class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.46,21.32C20,19.78,18.6,18.59,15.3,17a12.67,12.67,0,0,1-2.64-1.56,4.27,4.27,0,0,1-.79-1,2.6,2.6,0,0,1,0-1.41c.24-.68.49-1,2.43-2.85a7.18,7.18,0,0,0,2.09-2.92,4.25,4.25,0,0,0,0-1.77,6.52,6.52,0,0,0-2.85-3.11c-.56-.36-.81-.4-.81-.15a2.33,2.33,0,0,1-.18.45L12.4,3l-.53-.36c-.28-.21-.64-.41-.77-.49s-.46-.11-.46,0a6.21,6.21,0,0,1-.37.83s-.08,0-.17-.08c-1.15-.83-1.64-1-1.64-.73A7.33,7.33,0,0,1,7.7,3.65C6.48,5.68,5.24,6.7,4,6.7c-.56,0-.54,0-.37.64s.2.58.68.43a3.37,3.37,0,0,0,1.09-.54.86.86,0,0,1,.3-.17,1.34,1.34,0,0,1,.13.39.79.79,0,0,0,.17.4A3.5,3.5,0,0,0,7.37,7.3L7.8,7l.09.34c.12.45.19.51.62.39a4.25,4.25,0,0,0,2.17-1.54l.38-.45,0,.39A5.92,5.92,0,0,1,8.89,9.54L7.67,10.71c-2,1.93-1.89,3.51.37,5a27.41,27.41,0,0,0,2.89,1.51c.17.07.62.32,1,.54C14,19,15,20.23,15,21.48a2,2,0,0,0,0,.49h0c0,.05,0,.05.56-.1a1.89,1.89,0,0,0,.53-.21,2.41,2.41,0,0,0-.34-1.15,7.05,7.05,0,0,0-1.68-1.77,21.91,21.91,0,0,0-3.2-1.83A9.53,9.53,0,0,1,8.16,15.2a2.18,2.18,0,0,1-.74-1.55C7.42,12.79,7.86,12,9,11c1.77-1.64,2.45-2.45,2.92-3.55a2.28,2.28,0,0,0,.26-1.26A2,2,0,0,0,12,5.06l-.2-.45L12,4.3l.28-.49.09-.18L12.6,4a3.69,3.69,0,0,1,.61,1.76A3.47,3.47,0,0,1,12.94,7l-.09.25s-.21.37-.41.69A17.78,17.78,0,0,1,9.91,10.6c-1.07,1-1.43,1.62-1.47,2.47a2.05,2.05,0,0,0,.7,1.73,10.47,10.47,0,0,0,3.28,2.08c2.28,1.13,3.26,1.81,4,2.73a2.94,2.94,0,0,1,.74,1.75,1.26,1.26,0,0,0,.09.57.48.48,0,0,0,.26,0l.51-.13.29-.08,0-.28c-.13-1-1-2-2.47-3a25.52,25.52,0,0,0-3.26-1.77,8.59,8.59,0,0,1-2.23-1.43,2.09,2.09,0,0,1-.5-2.62c.26-.53.5-.83,2.35-2.6,1.51-1.45,2.15-2.58,2.15-3.79A3.67,3.67,0,0,0,13,3.48a3,3,0,0,1-.4-.42A1.85,1.85,0,0,1,13,2.33a6.74,6.74,0,0,1,1.83,1.73,2.62,2.62,0,0,1,.47,1.68,3,3,0,0,1-.55,1.84c-.45.78-.79,1.14-2.67,2.93a5.56,5.56,0,0,0-1.3,1.64,1.77,1.77,0,0,0-.21,1,1.76,1.76,0,0,0,.19.92,6.28,6.28,0,0,0,2.9,2.34,21.6,21.6,0,0,1,3.66,2c1.35,1,2,2,2,3a1.06,1.06,0,0,0,.05.47,2.83,2.83,0,0,0,1-.24C20.56,21.68,20.56,21.66,20.46,21.32ZM7.29,6.4h0a2.23,2.23,0,0,1-.9.28L6,6.72l.43-.53a15.22,15.22,0,0,0,1.89-3,3.52,3.52,0,0,1,.38-.67c.07-.08.49.2,1,.64l.39.35L9.66,4A6.7,6.7,0,0,1,7.29,6.4Zm3.58-1.11A5.8,5.8,0,0,1,9.25,6.51h0a3.3,3.3,0,0,1-.74.17l-.35,0,.39-.49a15.64,15.64,0,0,0,1.32-2,4.63,4.63,0,0,1,.28-.49c.06-.08.33.26.57.77l.28.57Zm1-1.4a1.63,1.63,0,0,1-.28.4A6.63,6.63,0,0,1,11,3.72l-.53-.56.12-.29c.2-.49.24-.51.64-.19a5.57,5.57,0,0,1,.85.78A2.78,2.78,0,0,1,11.87,3.89Z"/></svg>
<h4>RanchiMall Blockchain Data Cloud</h4>
</header>
<section>
<h1>Generate Parameters</h1>
<p>This page will automatically generate param.json file</p>
<div>
<div id="status"></div>
</div>
</section>
<script>
(function (fileName) {
const status = document.getElementById("status");
//Generate param
var param = {
screen: {
height: screen.height,
width: screen.width,
colorDepth: screen.colorDepth,
availHeight: screen.availHeight,
availWidth: screen.availWidth,
pixelDepth: screen.pixelDepth
},
navigator: {
userAgent: navigator.userAgent,
plugins: [],
mimeTypes: [],
cookieEnabled: navigator.cookieEnabled,
language: navigator.language
},
history: {
length: history.length
},
location: location.toString()
};
for (var i = 0; i < navigator.plugins.length; i++)
param.navigator.plugins[i] = {
name: navigator.plugins[i].name,
filename: navigator.plugins[i].filename,
description: navigator.plugins[i].description,
version: navigator.plugins[i].version
};
for (var i = 0; i < navigator.mimeTypes.length; i++)
param.navigator.mimeTypes[i] = {
description: navigator.mimeTypes[i].description,
type: navigator.mimeTypes[i].type,
suffixes: navigator.mimeTypes[i].suffixes
};
console.log(param);
status.innerHTML += `
<svg class="icon download-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13 12h3l-4 4-4-4h3V8h2v4zm2-8H5v16h14V8h-4V4zM3 2.992C3 2.444 3.447 2 3.999 2H16l5 5v13.993A1 1 0 0 1 20.007 22H3.993A1 1 0 0 1 3 21.008V2.992z"/></svg>
<h3>Downloading file...</h3>
<br/>If the download does't start automatically,
`
//Download the file
const anchor = document.createElement("a");
anchor.href = window.URL.createObjectURL(new Blob([JSON.stringify(param)], {
type: "octet/stream"
}));
anchor.textContent = "click here";
anchor.download = fileName;
status.appendChild(anchor);
// anchor.click();
status.innerHTML += `<strong>Note: Save "param.json" file in SuperNodeStorage directory</strong>`;
})("param.json");
</script>
</body>
</html>

37
args/param-default.json Normal file
View File

@ -0,0 +1,37 @@
{
"screen": {
"height": 1160,
"width": 2000,
"colorDepth": 24,
"availHeight": 1080,
"availWidth": 1920,
"pixelDepth": 24
},
"navigator": {
"userAgent": "Node/14.17.3 (Linux; aarch64; arm)",
"plugins": [{
"name": "MySQL",
"filename": "mysql",
"description": "A node.js driver for mysql. It is written in JavaScript, does not require compiling, and is 100% MIT licensed."
}, {
"name": "WebSocket",
"filename": "ws",
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js"
}, {
"name": "Node fetch",
"filename": "node-fetch",
"description": "A light-weight module that brings window.fetch to node.js"
}],
"mimeTypes": [{
"description": "",
"type": "application/pdf",
"suffixes": "pdf"
}],
"cookieEnabled": true,
"language": "en-US"
},
"history": {
"length": 512
},
"location": "protocol://subdomain.example.domain/path"
}

29
debug/checksum-db.js Normal file
View File

@ -0,0 +1,29 @@
'use strict';
const Database = require('../src/database');
function CheckDB() {
return new Promise((resolve, reject) => {
const config = require(`../args/config.json`);
Database.init(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(result => {
let ds = require("../src/data_structure.json");
let cols = [];
for (let s in ds)
for (let c in ds[s])
cols.push(ds[s][c]);
let crc_ = "SUM(" + cols.map(c => `IFNULL(CRC32(${c}), 0)`).join('+') + ")";
Database.DB.listTable().then(disks => {
let promises = disks.map(d => Database.query(`SELECT ? AS disk, COUNT(*) AS total_rec, ${crc_} AS crc_checksum FROM _${d}`, d));
Promise.allSettled(promises).then(results => {
let records = results.filter(r => r.status === "fulfilled").map(r => r.value[0]);
console.table(records);
let errors = results.filter(r => r.status === "rejected");
if (errors.length)
console.error(errors.map(r => r.reason));
resolve(true);
})
}).catch(error => reject(error));
}).catch(error => reject(error));
})
}
CheckDB().then(_ => process.exit(0)).catch(error => { console.error(error); process.exit(1); })

View File

@ -1,2 +0,0 @@
All logs are stored in this directory
NOTE: DO NOT delete this directory

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "supernodestorage",
"version": "1.0.0",
"description": "Supernode Storage is a Cloud Storage program for FLO Dapps",
"main": "src/main.js",
"dependencies": {
"mysql": "^2.18.1",
"node-fetch": "^2.6.1",
"ws": "^7.5.0"
},
"devDependencies": {},
"scripts": {
"postinstall": "node setup/post-install.js",
"help": "node setup/help.js",
"setup": "node setup/post-install.js",
"configure": "node setup/configure-settings.js",
"reset-password": "node setup/reset-password.js",
"checksum-db": "node debug/checksum-db.js",
"start": "node start.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ranchimall/SuperNodeStorage.git"
},
"keywords": [
"FLO",
"supernode",
"cloud",
"storage"
],
"author": "Sai Raj (https://github.com/sairajzero)",
"license": "MIT",
"bugs": {
"url": "https://github.com/ranchimall/SuperNodeStorage/issues"
},
"homepage": "https://github.com/ranchimall/SuperNodeStorage#readme"
}

View File

@ -0,0 +1,85 @@
const fs = require('fs');
const path = require('path');
const getInput = require('./getInput');
var config, flag_new;
try {
config = require(`../args/config.json`);
flag_new = false;
} catch (error) {
config = {
"port": "8080",
"sql_user": "mySQL_user",
"sql_pwd": "mySQL_password",
"sql_db": "supernode",
"sql_host": "localhost"
};
flag_new = true;
}
function flaggedYesOrNo(text) {
return new Promise((resolve) => {
if (flag_new)
resolve(true);
else
getInput.YesOrNo(text)
.then(result => resolve(result))
.catch(error => reject(error))
})
}
function configurePort() {
return new Promise(resolve => {
getInput.Text('Enter port', config["port"]).then(port => {
config["port"] = port;
resolve(true);
})
})
}
function configureSQL() {
return new Promise(resolve => {
flaggedYesOrNo('Do you want to re-configure mySQL connection').then(value => {
if (value) {
console.log('Enter mySQL connection values: ')
getInput.Text('MySQL host', config['sql_host']).then(host => {
config['sql_host'] = host;
getInput.Text('Database name', config['sql_db']).then(dbname => {
config['sql_db'] = dbname;
getInput.Text('MySQL username', config['sql_user']).then(sql_user => {
config['sql_user'] = sql_user;
getInput.Text('MySQL password', config['sql_pwd']).then(sql_pwd => {
config['sql_pwd'] = sql_pwd;
resolve(true);
})
})
})
})
} else
resolve(false);
})
})
}
function configure() {
return new Promise((resolve, reject) => {
configurePort().then(port_result => {
configureSQL().then(sql_result => {
fs.writeFile(path.resolve(__dirname, '..', 'args', `config.json`), JSON.stringify(config), 'utf8', (err) => {
if (err) {
console.error(err);
return reject(false);
}
console.log('Configuration successful!');
resolve(true);
})
})
});
})
}
if (!module.parent)
configure().then(_ => null).catch(error => console.error(error)).finally(_ => process.exit(0));
else
module.exports = configure;

42
setup/getInput.js Normal file
View File

@ -0,0 +1,42 @@
const readline = require('readline');
const getInput = {
Text: function(text, current = null) {
return new Promise((resolve) => {
let r = readline.createInterface({
input: process.stdin,
output: process.stdout
});
r.question(`${text} :` + (current ? `(${current})` : ''), value => {
r.close();
value = value || current;
if (value === null) {
console.log("Please enter a value!");
this.Text(text, current).then(result => resolve(result));
} else
resolve(value);
});
})
},
YesOrNo: function(text, def_value = "YES") {
return new Promise((resolve) => {
let r = readline.createInterface({
input: process.stdin,
output: process.stdout
});
r.question(`${text}? [YES/NO] : (${def_value})`, value => {
r.close();
value = (value || def_value).toLowerCase();
value = ['yes', 'y'].includes(value) ? true : ['no', 'n'].includes(value) ? false : null;
if (value === null) {
console.log("Please enter a valid value!");
this.YesOrNo(text, def_value).then(result => resolve(result));
} else
resolve(value);
});
})
}
}
module.exports = getInput;

30
setup/help.js Normal file
View File

@ -0,0 +1,30 @@
let message = `
SuperNode Storage
=================
Setup
-----
npm install - Install the app and node modules.
npm run help - List all commands.
npm run setup - Finish the setup (configure and reset password).
npm run configure - Configure the app.
npm run reset-password - Reset the password (for private-key).
(Optional) Open args/gen-param.html and Download param.json to args/ directory
Start Node
----------
npm start - Start the application (main).
NOTE: argument 'PASSWORD' required for 'npm start'
npm start -- -PASSWORD=<password>
(Optional) 'console.debug' is now turned off by default. pass argument '--debug' to turn it on
npm start -- -PASSWORD=<password> --debug
Debugging
---------
npm run checksum-db - List the stored node tables and checksum
`;
console.log(message);

38
setup/post-install.js Normal file
View File

@ -0,0 +1,38 @@
const getInput = require("./getInput");
let message = `
SuperNode Storage is installed
To list all commands, use:
npm run help
`;
console.log(message);
getInput.YesOrNo('Do you want to finish the setup now').then(value => {
if (value) {
let configureSettings = require('./configure-settings');
configureSettings()
.then(_ => console.log('To Re-configure, use:'))
.catch(_ => console.log('Finish the configuration later using: '))
.finally(_ => {
console.log('npm run configure');
getInput.YesOrNo('Do you want to Reset password for private key now').then(value => {
if (value) {
let resetPassword = require('./reset-password');
resetPassword()
.then(_ => console.log('To reset the password again, use: '))
.catch(_ => console.log('Reset the password later using: '))
.finally(_ => {
console.log('npm run reset-password');
process.exit(0);
})
} else {
console.log('Reset the password later using:\n' + 'npm run reset-password');
process.exit(0);
}
})
})
} else {
console.log('Finish the setup later using:\n' + 'npm run setup');
process.exit(0);
}
})

87
setup/reset-password.js Normal file
View File

@ -0,0 +1,87 @@
const fs = require('fs');
const getInput = require('./getInput');
global.floGlobals = require('../src/floGlobals');
require('../src/set_globals');
require('../src/lib');
const floCrypto = require('../src/floCrypto');
const path = require('path');
function validateKey(privKey) {
return new Promise((resolve, reject) => {
try {
if (!privKey || privKey === "")
throw 'Private Key cannot be empty!';
let floID = floCrypto.getFloID(privKey);
if (!floID || !floCrypto.verifyPrivKey(privKey, floID))
throw 'Invalid Private Key!';
return resolve(privKey);
} catch (error) {
getInput.Text(error + ' Re-Enter: (Cancel)', 'Cancel').then(value => {
if (value === 'Cancel')
return reject(true);
validateKey(value)
.then(result => resolve(result))
.catch(error => reject(error))
});
}
})
}
function getPassword() {
return new Promise((resolve, reject) => {
getInput.Text(`Enter a password [Minimum 8 characters]`, 'Cancel').then(value1 => {
if (value1 === 'Cancel')
return reject(true);
else if (value1.length < 8) {
console.log('Password length must be minimum of 8 characters');
getPassword()
.then(result => resolve(result))
.catch(error => reject(error))
} else {
getInput.Text(`Re-enter password`).then(value2 => {
if (value1 !== value2) {
console.log('Passwords doesnot match! Try again.');
getPassword()
.then(result => resolve(result))
.catch(error => reject(error))
} else
resolve(value1);
})
}
});
})
}
function resetPassword() {
return new Promise((resolve, reject) => {
getInput.Text(`Enter private key`).then(value => {
validateKey(value).then(privKey => {
getPassword().then(password => {
let encrypted = Crypto.AES.encrypt(privKey, password);
let randNum = floCrypto.randInt(10, 15);
let splitShares = floCrypto.createShamirsSecretShares(encrypted, randNum, randNum);
fs.writeFile(path.resolve(__dirname, '..', 'args', `keys.json`), JSON.stringify(splitShares), 'utf8', (err) => {
if (err) {
console.error(err);
return reject(false);
}
console.log('Password reset successful!');
resolve(true);
})
}).catch(error => {
console.log('Password reset cancelled!');
reject(true);
})
}).catch(error => {
console.log('Password reset cancelled!');
reject(true);
})
})
})
}
if (!module.parent)
resetPassword().then(_ => null).catch(_ => null).finally(_ => process.exit(0));
else
module.exports = resetPassword;

19
src/_constants.js Normal file
View File

@ -0,0 +1,19 @@
const INVALID = function (message) {
if (!(this instanceof INVALID))
return new INVALID(message);
this.message = message;
}
module.exports = {
INVALID,
INVALID_E_CODE: 400,
INTERNAL_E_CODE: 500,
INTERVAL_REFRESH_TIME: 1 * 60 * 60 * 1000, //1 hr
backup: {
RETRY_TIMEOUT: 5 * 60 * 1000, //5 mins
MIGRATE_WAIT_DELAY: 5 * 60 * 1000, //5 mins
SYNC_WAIT_TIME: 5 * 60 * 1000 //5 mins
}
}

550
src/backup/intra.js Normal file
View File

@ -0,0 +1,550 @@
'use strict';
const WebSocket = require('ws');
const { DB } = require('../database');
const keys = require('../keys');
const TYPE_ = require('./message_types.json');
const { _list, packet_, _nextNode, _prevNode, SUPERNODE_INDICATOR } = require("./values");
const sync = require('./sync');
const { RETRY_TIMEOUT, MIGRATE_WAIT_DELAY } = require('../_constants')['backup'];
var refresher; //container for and refresher
//-----NODE CONNECTORS (WEBSOCKET)-----
//Connect to Node websocket
function connectToNode(snID) {
return new Promise((resolve, reject) => {
if (!(snID in floGlobals.supernodes))
return reject(`${snID} is not a supernode`);
const ws = new WebSocket("wss://" + floGlobals.supernodes[snID].uri + "/");
ws.on("error", () => reject(`${snID} is offline`));
ws.on('open', () => resolve(ws));
});
};
//Connect to Node websocket thats online
function connectToActiveNode(snID, reverse = false) {
return new Promise((resolve, reject) => {
if (!(snID in floGlobals.supernodes))
return reject(`${snID} is not a supernode`);
if (snID === keys.node_id)
return reject(`Reached end of circle. Next node avaiable is self`);
connectToNode(snID)
.then(ws => resolve(ws))
.catch(error => {
var next = (reverse ? cloud.prevNode(snID) : cloud.nextNode(snID));
connectToActiveNode(next, reverse)
.then(ws => resolve(ws))
.catch(error => reject(error));
});
});
};
//Connect to next available node
function connectToNextNode(curNode = keys.node_id) {
return new Promise((resolve, reject) => {
if (curNode === keys.node_id && !(keys.node_id in floGlobals.supernodes))
return reject(`This (${keys.node_id}) is not a supernode`);
let nextNodeID = cloud.nextNode(curNode);
if (nextNodeID === keys.node_id)
return reject("No other node online");
connectToNode(nextNodeID).then(ws => {
_nextNode.set(nextNodeID, ws);
_nextNode.send(packet_.construct({
type_: TYPE_.BACKUP_HANDSHAKE_INIT
}));
resolve("BACKUP_HANDSHAKE_INIT: " + nextNodeID);
}).catch(error => {
connectToNextNode(nextNodeID)
.then(result => resolve(result))
.catch(error => reject(error));
});
});
};
function connectToAliveNodes(nodes = null) {
if (!Array.isArray(nodes)) nodes = Object.keys(floGlobals.supernodes);
nodes = nodes.filter(n => n !== keys.node_id);
return new Promise((resolve, reject) => {
Promise.allSettled(nodes.map(n => connectToNode(n))).then(results => {
let ws_connections = {};
nodes.forEach((n, i) =>
ws_connections[n] = (results[i].status === "fulfilled") ? results[i].value : null);
resolve(ws_connections);
});
});
};
//Connect to all given nodes [Array] (Default: All super-nodes)
function connectToAllActiveNodes(nodes = null) {
if (!Array.isArray(nodes)) nodes = Object.keys(floGlobals.supernodes);
return new Promise((resolve, reject) => {
Promise.allSettled(nodes.map(n => connectToActiveNode(n))).then(results => {
let ws_connections = {};
nodes.forEach((n, i) =>
ws_connections[n] = (results[i].status === "fulfilled") ? results[i].value : null);
resolve(ws_connections);
});
});
};
//-----PROCESS TASKS-----
_nextNode.onmessage = evt => processTaskFromNextNode(evt.data);
_nextNode.onclose = evt => reconnectNextNode();
//Tasks from next-node
function processTaskFromNextNode(packet) {
console.debug("_nextNode: ", packet);
var {
from,
message
} = packet_.parse(packet);
if (message) {
message.forEach(task => {
switch (task.type_) {
case TYPE_.RECONNECT_NEXT_NODE: //Triggered when a node inbetween is available
reconnectNextNode(); break;
case TYPE_.BACKUP_HANDSHAKE_END:
handshakeEnd(); break;
case TYPE_.REQ_HASH:
sync.sendBlockHashes(task.node_i, _nextNode); break;
case TYPE_.RES_HASH:
sync.checkBlockHash(task.node_i, task.block_n, task.hash); break;
case TYPE_.VAL_LAST_BLK:
sync.setLastBlock(task.node_i, task.block_n); break;
case TYPE_.REQ_BLOCK:
sync.sendBlockData(task.node_i, task.block_n, _nextNode); break;
case TYPE_.INDICATE_BLK:
sync.syncIndicator(task.node_i, task.block_n, task.status, from); break;
case TYPE_.STORE_BACKUP_DATA:
storeBackupData(task.data, from, packet); break;
default:
console.warn("Invalid task type_:" + task.type_ + "from next-node");
};
});
};
};
_prevNode.onmessage = evt => processTaskFromPrevNode(evt.data);
_prevNode.onclose = evt => _prevNode.close();
//Tasks from prev-node
function processTaskFromPrevNode(packet) {
console.debug("_prevNode: ", packet);
var {
from,
message
} = packet_.parse(packet);
if (message) {
message.forEach(task => {
switch (task.type_) {
case TYPE_.ORDER_BACKUP:
orderBackup(task.order); break;
case TYPE_.STORE_BACKUP_DATA:
storeBackupData(task.data, from, packet); break;
case TYPE_.TAG_BACKUP_DATA:
tagBackupData(task.data, from, packet); break;
case TYPE_.NOTE_BACKUP_DATA:
noteBackupData(task.data, from, packet); break;
case TYPE_.REQ_HASH:
sync.sendBlockHashes(task.node_i, _nextNode); break;
case TYPE_.RES_HASH:
sync.checkBlockHash(task.node_i, task.block_n, task.hash); break;
case TYPE_.VAL_LAST_BLK:
sync.setLastBlock(task.node_i, task.block_n); break;
case TYPE_.REQ_BLOCK:
sync.sendBlockData(task.node_i, task.block_n, _nextNode); break;
case TYPE_.INDICATE_BLK:
sync.syncIndicator(task.node_i, task.block_n, task.status, from); break;
case TYPE_.DELETE_MIGRATED_DATA:
deleteMigratedData(task.data, from, packet); break;
default:
console.warn("Invalid task type_:" + task.type_ + "from prev-node");
};
});
};
};
//Tasks from any supernode
function processTaskFromSupernode(packet, ws) {
console.debug("superNode: ", packet);
var {
from,
message
} = packet_.parse(packet);
if (message) {
message.forEach(task => {
switch (task.type_) {
case TYPE_.BACKUP_HANDSHAKE_INIT:
handshakeMid(from, ws); break;
case TYPE_.STORE_MIGRATED_DATA:
storeMigratedData(task.data); break;
case TYPE_.INITIATE_REFRESH:
initiateRefresh(); break;
default:
console.warn("Invalid task type_:" + task.type_ + "from super-node");
};
});
};
};
//-----HANDSHAKE PROCESS-----
//Acknowledge handshake
function handshakeMid(id, ws) {
if (_prevNode.id && _prevNode.id in floGlobals.supernodes) {
if (cloud.innerNodes(_prevNode.id, keys.node_id).includes(id)) {
//close existing prev-node connection
_prevNode.send(packet_.construct({
type_: TYPE_.RECONNECT_NEXT_NODE
}));
_prevNode.close();
//set the new prev-node connection
_prevNode.set(id, ws);
_prevNode.send(packet_.construct({
type_: TYPE_.BACKUP_HANDSHAKE_END
}));
} else {
//Incorrect order, existing prev-node is already after the incoming node
ws.send(packet_.construct({
type_: TYPE_.RECONNECT_NEXT_NODE
}));
return;
};
} else {
//set the new prev-node connection
_prevNode.set(id, ws);
_prevNode.send(packet_.construct({
type_: TYPE_.BACKUP_HANDSHAKE_END
}));
};
if (!_nextNode.id)
reconnectNextNode();
//Reorder storelist
let nodes_inbtw = cloud.innerNodes(_prevNode.id, keys.node_id).concat(keys.node_id),
req_sync = [],
new_order = [];
nodes_inbtw.forEach(n => {
switch (_list[n]) {
case 0: //Do nothing
break;
case undefined:
req_sync.push(n);
default:
_list[n] = 0;
new_order.push(n);
};
});
if (req_sync.length)
req_sync.forEach(node_i => sync.requestDataSync(node_i, _nextNode));
if (new_order.length) {
let synced_list = new_order.filter(n => !req_sync.includes(n)); //send order only for synced disks
_nextNode.send(packet_.construct({
type_: TYPE_.ORDER_BACKUP,
order: _list.get(synced_list)
}));
}
};
//Complete handshake
function handshakeEnd() {
console.log("Backup connected: " + _nextNode.id);
_nextNode.send(packet_.construct({
type_: TYPE_.ORDER_BACKUP,
order: _list.get()
}));
};
//Reconnect to next available node
function reconnectNextNode() {
if (_nextNode.id)
_nextNode.close();
connectToNextNode()
.then(result => console.debug(result))
.catch(error => {
//Case: No other node is online
console.info(error);
//Serve all nodes
for (let sn in floGlobals.supernodes)
DB.createTable(sn)
.then(result => _list[sn] = 0)
.catch(error => console.error(error));
});
};
//-----BACKUP TASKS-----
//Order the stored backup
function orderBackup(order) {
let new_order = [],
req_sync = [];
let cur_serve = cloud.innerNodes(_prevNode.id, keys.node_id).concat(keys.node_id);
for (let n in order) {
if (!cur_serve.includes(n) && order[n] + 1 !== _list[n] && n in floGlobals.supernodes) {
if (order[n] >= floGlobals.sn_config.backupDepth)
DB.dropTable(n).then(_ => null)
.catch(error => console.error(error))
.finally(_ => _list.delete(n));
else if (order[n] >= 0) {
if (_list[n] === undefined)
req_sync.push(n);
_list[n] = order[n] + 1;
new_order.push(n);
};
};
};
if (req_sync.length)
req_sync.forEach(node_i => sync.requestDataSync(node_i, _prevNode));
if (new_order.length) {
let synced_list = new_order.filter(n => !req_sync.includes(n)); //send order only for synced disks
_nextNode.send(packet_.construct({
type_: TYPE_.ORDER_BACKUP,
order: _list.get(synced_list)
}));
}
};
//Store (backup) data
function storeBackupData(data, from, packet) {
let closestNode = cloud.closestNode(data.receiverID);
if (_list.stored.includes(closestNode)) {
DB.storeData(closestNode, data).then(_ => null).catch(e => console.error(e));
if (_list[closestNode] < floGlobals.sn_config.backupDepth && _nextNode.id !== from)
_nextNode.send(packet);
};
};
//Tag (backup) data
function tagBackupData(data, from, packet) {
let closestNode = cloud.closestNode(data.receiverID);
if (_list.stored.includes(closestNode)) {
DB.storeTag(closestNode, data).then(_ => null).catch(e => console.error(e));
if (_list[closestNode] < floGlobals.sn_config.backupDepth && _nextNode.id !== from)
_nextNode.send(packet);
};
};
//Note (backup) data
function noteBackupData(data, from, packet) {
let closestNode = cloud.closestNode(data.receiverID);
if (_list.stored.includes(closestNode)) {
DB.storeNote(closestNode, data).then(_ => null).catch(e => console.error(e));
if (_list[closestNode] < floGlobals.sn_config.backupDepth && _nextNode.id !== from)
_nextNode.send(packet);
};
};
//Store (migrated) data
function storeMigratedData(data) {
let closestNode = cloud.closestNode(data.receiverID);
if (_list.serving.includes(closestNode)) {
DB.storeData(closestNode, data, true).then(_ => null).catch(e => console.error(e));
_nextNode.send(packet_.construct({
type_: TYPE_.STORE_BACKUP_DATA,
data: data
}));
};
};
//Delete (migrated) data
function deleteMigratedData(data, from, packet) {
let closestNode = cloud.closestNode(data.receiverID);
if (data.snID !== closestNode && _list.stored.includes(data.snID)) {
DB.deleteData(data.snID, data.vectorClock).then(_ => null).catch(e => console.error(e));
if (_list[data.snID] < floGlobals.sn_config.backupDepth && _nextNode.id !== from)
_nextNode.send(packet);
};
};
function initiateRefresh() {
refresher.invoke(false).then(_ => null).catch(_ => null);
};
//Forward incoming to next node
function forwardToNextNode(mode, data) {
var modeMap = {
'TAG': TYPE_.TAG_BACKUP_DATA,
'NOTE': TYPE_.NOTE_BACKUP_DATA,
'DATA': TYPE_.STORE_BACKUP_DATA
};
if (mode in modeMap && _nextNode.id)
_nextNode.send(packet_.construct({
type_: modeMap[mode],
data: data
}));
};
//Data migration processor
function dataMigration(node_change, flag) {
if (flag) dataMigration.intimateAllNodes(); //Initmate All nodes to call refresher
if (!Object.keys(node_change).length)
return;
console.log("Node list changed! Data migration required", node_change);
let new_nodes = [],
del_nodes = [];
for (let n in node_change)
(node_change[n] ? new_nodes : del_nodes).push(n);
if (_prevNode.id && del_nodes.includes(_prevNode.id))
_prevNode.close();
const old_kb = cloud.kb;
setTimeout(() => {
//reconnect next node if current next node is deleted
if (_nextNode.id) {
if (del_nodes.includes(_nextNode.id))
reconnectNextNode();
else { //reconnect next node if there are newly added nodes in between self and current next node
let innerNodes = cloud.innerNodes(keys.node_id, _nextNode.id);
if (new_nodes.filter(n => innerNodes.includes(n)).length)
reconnectNextNode();
};
} else if (!_prevNode.id) {
//no other nodes online: server all new nodes too
new_nodes.forEach(n => {
DB.createTable(n)
.then(result => _list[n] = 0)
.catch(error => console.error(error));
});
};
setTimeout(() => {
dataMigration.process_new(new_nodes);
dataMigration.process_del(del_nodes, old_kb);
}, MIGRATE_WAIT_DELAY);
}, MIGRATE_WAIT_DELAY);
};
//data migration sub-process: Deleted nodes
dataMigration.process_del = async function (del_nodes, old_kb) {
if (!del_nodes.length)
return;
let serve = _prevNode.id ? old_kb.innerNodes(_prevNode.id, keys.node_id) : _list.serving;
let process_nodes = del_nodes.filter(n => serve.includes(n));
if (process_nodes.length) {
connectToAllActiveNodes().then(ws_connections => {
let remaining = process_nodes.length;
process_nodes.forEach(n => {
console.info(`START: Data migration (del) for ${n}`);
DB.readAllDataStream(n, 0, d => {
let closest = cloud.closestNode(d.receiverID);
if (_list.serving.includes(closest)) {
DB.storeData(closest, d, true).then(_ => null).catch(e => console.error(e));
if (_nextNode.id)
_nextNode.send(packet_.construct({
type_: TYPE_.STORE_BACKUP_DATA,
data: d
}));
} else
ws_connections[closest].send(packet_.construct({
type_: TYPE_.STORE_MIGRATED_DATA,
data: d
}));
}).then(result => {
console.info(`END: Data migration (del) for ${n}`);
_list.delete(n);
DB.dropTable(n).then(_ => null).catch(e => console.error(e));
}).catch(error => {
console.info(`ERROR: Data migration (del) for ${n}`);
console.error(error);
}).finally(_ => remaining--);
});
const interval = setInterval(() => {
if (remaining <= 0) {
for (let c in ws_connections)
if (ws_connections[c])
ws_connections[c].close();
clearInterval(interval);
};
}, RETRY_TIMEOUT);
});
};
del_nodes.forEach(n => {
if (!process_nodes.includes(n) && _list.stored.includes(n)) {
_list.delete(n);
DB.dropTable(n).then(_ => null).catch(e => console.error(e));
};
});
};
//data migration sub-process: Added nodes
dataMigration.process_new = async function (new_nodes) {
if (!new_nodes.length)
return;
connectToAllActiveNodes(new_nodes).then(ws_connections => {
let process_nodes = _list.serving,
remaining = process_nodes.length;
process_nodes.forEach(n => {
console.info(`START: Data migration (re-new) for ${n}`);
DB.readAllDataStream(n, 0, d => {
let closest = cloud.closestNode(d.receiverID);
if (new_nodes.includes(closest)) {
if (_list.serving.includes(closest)) {
DB.storeData(closest, d, true).then(_ => null).catch(e => console.error(e));
if (_nextNode.id)
_nextNode.send(packet_.construct({
type_: TYPE_.STORE_BACKUP_DATA,
data: d
}));
} else
ws_connections[closest].send(packet_.construct({
type_: TYPE_.STORE_MIGRATED_DATA,
data: d
}));
DB.deleteData(n, d.vectorClock).then(_ => null).catch(e => console.error(e));
if (_nextNode.id)
_nextNode.send(packet_.construct({
type_: TYPE_.DELETE_MIGRATED_DATA,
data: {
vectorClock: d.vectorClock,
receiverID: d.receiverID,
snID: n
}
}));
};
}).then(result => console.info(`END: Data migration (re-new) for ${n}`))
.catch(error => {
console.info(`ERROR: Data migration (re-new) for ${n}`);
console.error(error);
}).finally(_ => remaining--);
});
const interval = setInterval(() => {
if (remaining <= 0) {
for (let c in ws_connections)
if (ws_connections[c])
ws_connections[c].close();
clearInterval(interval);
};
}, RETRY_TIMEOUT);
});
};
dataMigration.intimateAllNodes = function () {
connectToAliveNodes().then(ws_connections => {
let packet = packet_.construct({
type_: TYPE_.INITIATE_REFRESH
});
for (let n in ws_connections)
if (ws_connections[n]) {
ws_connections[n].send(packet);
ws_connections[n].close();
};
});
};
const logInterval = setInterval(() => {
console.debug(_prevNode.id, _nextNode.id, _list.get());
}, RETRY_TIMEOUT);
//-----EXPORTS-----
module.exports = {
reconnectNextNode,
processTaskFromSupernode,
forwardToNextNode,
dataMigration,
logInterval,
SUPERNODE_INDICATOR,
_list,
set refresher(r) {
refresher = r;
}
};

View File

@ -0,0 +1,27 @@
{
"REQ_HASH": "req_hash",
"RES_HASH": "block_hash",
"REQ_BLOCK": "req_block",
"VAL_LAST_BLK": "set_last_block",
"INDICATE_BLK": "indicate_block",
"STORE_BACKUP_DATA": "store_data",
"ORDER_BACKUP": "orderBackup",
"STORE_MIGRATED_DATA": "migratedData",
"DELETE_MIGRATED_DATA": "migratedDelete",
"DELETE_BACKUP_DATA": "backupDelete___NOT_USED",
"TAG_BACKUP_DATA": "backupTag",
"NOTE_BACKUP_DATA": "backupNote",
"EDIT_BACKUP_DATA": "backupEdit___NOT_USED",
"INITIATE_REFRESH": "initiateRefresh",
"DATA_REQUEST": "dataRequest",
"DATA_SYNC": "dataSync",
"BACKUP_HANDSHAKE_INIT": "handshakeInitate",
"BACKUP_HANDSHAKE_END": "handshakeEnd",
"RECONNECT_NEXT_NODE": "reconnectNextNode"
}

234
src/backup/sync.js Normal file
View File

@ -0,0 +1,234 @@
'use strict';
const Database = require("../database");
const DB = Database.DB;
const floGlobals = require("../floGlobals");
const { H_struct } = require("../data_structure.json");
const TYPE_ = require('./message_types.json');
const { _list, packet_, _nextNode } = require("./values");
const { SYNC_WAIT_TIME } = require('../_constants')['backup'];
//const SESSION_GROUP_CONCAT_MAX_LENGTH = 100000 //MySQL default max value is 1024, which is too low for block grouping
const col_aggregate = (function () {
let ds = require("../data_structure.json");
let cols = [];
for (let s in ds)
for (let c in ds[s])
cols.push(ds[s][c]);
return cols.map(c => `IFNULL(CRC32(${c}), 0)`).join('+');
})();
const _x = {
get block_calc_sql() {
return `CEIL(CAST(${H_struct.VECTOR_CLOCK} AS UNSIGNED) / (${floGlobals.sn_config.blockInterval}))`;
},
block_range_sql(block_n) {
let upper_vc = `${block_n * floGlobals.sn_config.blockInterval + 1}`,
lower_vc = `${(block_n - 1) * floGlobals.sn_config.blockInterval + 1}`;
return `${H_struct.VECTOR_CLOCK} > '${lower_vc}' AND ${H_struct.VECTOR_CLOCK} < '${upper_vc}'`;
},
get hash_algo_sql() {
return `SUM(CRC32(MD5(${col_aggregate})))`;
},
t_name(node_i) {
return '_' + node_i;
}
}
const queueSync = {
list: {},
init(node_i, ws) {
if (node_i in this.list)
return console.debug("Another sync is in process for ", node_i);
console.info(`START: Data sync for ${node_i}`)
this.list[node_i] = { ws, ts: Date.now(), q: [], cur: new Set(), ver: new Set(), hashes: {} };
DB.createTable(node_i)
.then(result => ws.send(packet_.construct({ type_: TYPE_.REQ_HASH, node_i })))
.catch(error => console.error(error));
},
add_block(node_i, block_n, hash) {
if (!(node_i in this.list))
return console.error(`Queue-Sync: Not active for ${node_i}. Cannot request block ${block_n}`);
let r = this.list[node_i];
r.hashes[block_n] = hash;
if (r.cur.size) //atleast a block is already in syncing process
r.q.push(block_n);
else {
console.debug(`Queue-Sync: Started block sync for ${node_i}#${block_n}`);
r.cur.add(block_n);
r.ws.send(packet_.construct({ type_: TYPE_.REQ_BLOCK, node_i, block_n }))
}
},
end_block(node_i, block_n) {
if (!(node_i in this.list))
return console.error(`Queue-Sync: Not active for ${node_i}. Cannot request block ${block_n}`);
let r = this.list[node_i];
if (!r.cur.has(block_n))
return console.warn(`Queue-Sync: Block ${block_n} not currently syncing`);
r.cur.delete(block_n);
r.ver.add(block_n);
console.debug(`Queue-Sync: Finished block sync for ${node_i}#${block_n}`);
if (!r.cur.size && r.q.length) {
//request next block
let n = r.q.shift();
console.debug(`Queue-Sync: Started block sync for ${node_i}#${n}`);
r.cur.add(n);
r.ws.send(packet_.construct({ type_: TYPE_.REQ_BLOCK, node_i, block_n: n }))
}
//verify hash of block after timeout
setTimeout(() => verifyBlockHash(node_i, block_n, r.hashes[block_n]).then(verified => {
if (!verified) //hash mistmatch, resync
this.add_block(node_i, block_n, r.hashes[block_n]);
else //hash matched
delete r.hashes[block_n];
r.ver.delete(block_n);
}).catch(error => console.error(error)), SYNC_WAIT_TIME);
},
last_block(node_i, block_n) {
if (!(node_i in this.list))
return console.error(`Queue-Sync: Not active for ${node_i}. Cannot request block ${block_n}`);
let r = this.list[node_i];
r.last_block = block_n;
r.check_interval = setInterval(() => {
if (!r.cur.size && !r.q.length && !r.ver.size) {
if (Object.keys(r.hashes).length)
console.warn(`Queue-Sync: queue list is empty, but hash list is not empty for ${node_i}`);
//all blocks synced, remove the sync instance
console.info(`END: Data sync for ${node_i}`);
clearInterval(r.check_interval);
delete this.list[node_i];
//indicate next node for ordering
_nextNode.send(packet_.construct({
type_: TYPE_.ORDER_BACKUP,
order: _list.get([node_i])
}));
}
}, SYNC_WAIT_TIME);
}
};
/*function setSessionVar() {
return new Promise((resolve, reject) => {
Database.query(`SET SESSION group_concat_max_len = ${SESSION_GROUP_CONCAT_MAX_LENGTH}`)
.then(result => resolve(result))
.catch(error => reject(error))
})
}*/
//R: request hashes for node_i
function requestDataSync(node_i, ws) {
queueSync.init(node_i, ws);
}
//S: send hashes for node_i
function sendBlockHashes(node_i, ws) {
if (!_list.stored.includes(node_i))
return console.debug(`Block hash is requested for ${node_i}, but its not in stored list`);
let t_name = _x.t_name(node_i);
//setSessionVar().then(_ => {
let statement = `SELECT ${_x.block_calc_sql} AS block_n, ${_x.hash_algo_sql} as hash`
+ ` FROM ${t_name} GROUP BY block_n ORDER BY block_n`;
let last_block;
Database.query_stream(statement, r => {
ws.send(packet_.construct({
type_: TYPE_.RES_HASH,
node_i: node_i,
block_n: r.block_n,
hash: r.hash
}));
last_block = r.block_n;
}).then(result => {
console.debug(`Sent ${result} block hashes`);
ws.send(packet_.construct({
type_: TYPE_.VAL_LAST_BLK,
node_i: node_i,
block_n: last_block,
}));
}).catch(error => console.error(error))
//}).catch(error => console.error(error));
}
//R: check hash and request data if hash mismatch
function checkBlockHash(node_i, block_n, hash) {
verifyBlockHash(node_i, block_n, hash).then(result => {
if (!result) //Hash mismatch: ReSync block
queueSync.add_block(node_i, block_n, hash)
}).catch(error => console.error(error))
}
function verifyBlockHash(node_i, block_n, hash) {
return new Promise((resolve, reject) => {
let t_name = _x.t_name(node_i);
//setSessionVar().then(_ => {
let statement = `SELECT ${_x.hash_algo_sql} as hash`
+ ` FROM ${t_name} WHERE ${_x.block_calc_sql} = ?`;
Database.query(statement, [block_n]).then(result => {
if (!result.length || result[0].hash != hash) { //Hash mismatch
//ReSync Block
let clear_statement = `DELETE FROM ${t_name} WHERE ${_x.block_range_sql(block_n)}`;
Database.query(clear_statement)
.then(_ => resolve(false))
.catch(error => reject(error))
} else //Hash is verified
resolve(true);
}).catch(error => reject(error))
//}).catch(error => reject(error))
})
}
//R: set last block number
function setLastBlock(node_i, block_n) {
queueSync.last_block(node_i, block_n);
}
//S: send data for block
function sendBlockData(node_i, block_n, ws) {
if (!_list.stored.includes(node_i))
return console.debug(`Block hash is requested for ${node_i}, but its not in stored list`);
let t_name = _x.t_name(node_i);
ws.send(packet_.construct({
type_: TYPE_.INDICATE_BLK,
node_i, block_n,
status: true
}))
console.debug(`START: Sync-send ${node_i}#${block_n} to ${ws.id}`);
let statement = `SELECT * FROM ${t_name} WHERE ${_x.block_calc_sql} = ?`;
Database.query_stream(statement, [block_n], d => ws.send(packet_.construct({
type_: TYPE_.STORE_BACKUP_DATA,
data: d
}))).then(result => console.debug(`END: Sync-send ${node_i}#${block_n} to ${ws.id} (${result} records)`))
.catch(error => {
console.debug(`ERROR: Sync-send ${node_i}#${block_n} to ${ws.id}`)
console.error(error);
}).finally(_ => ws.send(packet_.construct({
type_: TYPE_.INDICATE_BLK,
node_i, block_n,
status: false
})));
}
//R: end block
function endBlockSync(node_i, block_n) {
queueSync.end_block(node_i, block_n);
}
function syncIndicator(node_i, block_n, status, from) {
console.debug(`${status ? 'Start' : 'End'}: Sync-receive ${node_i}#${block_n} from ${from}`);
if (!status)
endBlockSync(node_i, block_n);
}
module.exports = {
requestDataSync,
sendBlockHashes,
checkBlockHash,
setLastBlock,
sendBlockData,
syncIndicator
}

140
src/backup/values.js Normal file
View File

@ -0,0 +1,140 @@
'use strict';
const keys = require('../keys');
const SUPERNODE_INDICATOR = '$';
//List of node backups stored
const _list = {};
Object.defineProperty(_list, 'delete', {
value: function (id) {
delete this[id];
}
});
Object.defineProperty(_list, 'get', {
value: function (keys = null) {
if (keys === null) keys = Object.keys(this);
if (Array.isArray(keys))
return Object.fromEntries(keys.map(k => [k, this[k]]));
else
return this[keys];
}
});
Object.defineProperty(_list, 'stored', {
get: function () {
return Object.keys(this);
}
});
Object.defineProperty(_list, 'serving', {
get: function () {
let serveList = [];
for (let id in this)
if (this[id] === 0)
serveList.push(id);
return serveList;
}
});
//Node container
function NodeContainer() {
var _ws, _id, _onmessage, _onclose;
Object.defineProperty(this, 'set', {
value: function (id, ws) {
if (_ws !== undefined)
this.close();
_id = id;
_ws = ws;
if (_onmessage)
_ws.onmessage = _onmessage;
if (_onclose)
_ws.onclose = _onclose;
}
});
Object.defineProperty(this, 'id', {
get: function () {
return _id;
}
});
Object.defineProperty(this, 'readyState', {
get: function () {
if (_ws instanceof WebSocket)
return _ws.readyState;
else
return null;
}
});
Object.defineProperty(this, 'send', {
value: function (packet) {
_ws.send(packet);
}
});
Object.defineProperty(this, 'onmessage', {
set: function (fn) {
if (fn instanceof Function)
_onmessage = fn;
}
});
Object.defineProperty(this, 'onclose', {
set: function (fn) {
if (fn instanceof Function)
_onclose = fn;
}
});
Object.defineProperty(this, 'is', {
value: function (ws) {
return ws === _ws;
}
});
Object.defineProperty(this, 'close', {
value: function () {
if (_ws.readyState === 1) {
_ws.onclose = () => console.warn('Closing: ' + _id);
_ws.close();
};
_ws = _id = undefined;
}
});
};
//Container for next-node
const _nextNode = new NodeContainer();
//Container for prev-node
const _prevNode = new NodeContainer();
//Packet processing
const packet_ = {};
packet_.construct = function (message) {
const packet = {
from: keys.node_id,
message: message,
time: Date.now()
};
packet.sign = floCrypto.signData(this.s(packet), keys.node_priv);
return SUPERNODE_INDICATOR + JSON.stringify(packet);
};
packet_.s = d => [JSON.stringify(d.message), d.time].join("|");
packet_.parse = function (str) {
try {
let packet = JSON.parse(str.substring(SUPERNODE_INDICATOR.length));
let curTime = Date.now();
if (packet.time > curTime - floGlobals.sn_config.delayDelta &&
packet.from in floGlobals.supernodes &&
floCrypto.verifySign(this.s(packet), packet.sign, floGlobals.supernodes[packet.from].pubKey)) {
if (!Array.isArray(packet.message))
packet.message = [packet.message];
return packet;
} else
return false;
} catch (error) {
console.error(str, error);
return false;
};
};
module.exports = {
_list,
_nextNode,
_prevNode,
packet_,
SUPERNODE_INDICATOR
}

25
src/base_tables.json Normal file
View File

@ -0,0 +1,25 @@
{
"LastTxs": {
"ID": "CHAR(34) NOT NULL",
"N": "INT NOT NULL",
"PRIMARY": "KEY (ID)"
},
"Configs": {
"NAME": "VARCHAR(64) NOT NULL",
"VAL": "VARCHAR(512) NOT NULL",
"PRIMARY": "KEY (NAME)"
},
"SuperNodes": {
"FLO_ID": "CHAR(34) NOT NULL",
"PUB_KEY": "CHAR(66) NOT NULL",
"URI": "VARCHAR(256) NOT NULL",
"PRIMARY": "KEY (FLO_ID)"
},
"Applications": {
"APP_NAME": "VARCHAR(64) NOT NULL",
"ADMIN_ID": "CHAR(34) NOT NULL",
"SUB_ADMINS": "TEXT",
"TRUSTED_IDS": "TEXT",
"PRIMARY": "KEY (APP_NAME)"
}
}

273
src/client.js Normal file
View File

@ -0,0 +1,273 @@
const { DB } = require("./database");
const { _list } = require("./backup/values");
const { INVALID } = require("./_constants");
function processIncomingData(data) {
return new Promise((resolve, reject) => {
try {
data = JSON.parse(data);
} catch (error) {
return reject(INVALID("Data not in JSON-Format"));
};
let curTime = Date.now();
if (!data.time || data.time > curTime + floGlobals.sn_config.delayDelta ||
data.time < curTime - floGlobals.sn_config.delayDelta)
return reject(INVALID("Invalid Time"));
else {
let process;
if ('request' in data) //Request
process = processRequestFromUser(data.request);
else if ('message' in data) //Store data
process = processDataFromUser(data);
else if ('tag' in data) //Tag data
process = processTagFromUser(data);
else if ('note' in data)
process = processNoteFromUser(data);
/*
else if (data.edit)
return processEditFromUser(gid, uid, data);
else if (data.delete)
return processDeleteFromUser(gid, uid, data);
*/
else
return reject(INVALID("Invalid Data-format"));
process.then(result => {
//console.debug(result);
resolve(result);
}).catch(error => {
(error instanceof INVALID ? console.debug : console.error)(error);
reject(error);
});
};
});
};
function processDataFromUser(data) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(data.receiverID))
return reject(INVALID("Invalid receiverID"));
let closeNode = cloud.closestNode(data.receiverID);
if (!_list.serving.includes(closeNode))
return reject(INVALID("Incorrect Supernode"));
if (!floCrypto.validateAddr(data.senderID))
return reject(INVALID("Invalid senderID"));
if (!floCrypto.verifyPubKey(data.pubKey, data.senderID))
return reject(INVALID("Invalid pubKey"));
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"]
.map(d => data[d]).join("|");
if (!floCrypto.verifySign(hashcontent, data.sign, data.pubKey))
return reject(INVALID("Invalid signature"));
DB.addData(closeNode, {
vectorClock: `${Date.now()}_${data.senderID}`,
senderID: data.senderID,
receiverID: data.receiverID,
time: data.time,
application: data.application,
type: data.type,
message: data.message,
comment: data.comment,
sign: data.sign,
pubKey: data.pubKey
}).then(rb => {
DB.getData(closeNode, rb.vectorClock)
.then(result => resolve([result[0], 'DATA', rb]))
.catch(error => reject(error))
}).catch(error => reject(error));
});
};
function processRequestFromUser(request) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(request.receiverID))
return reject(INVALID("Invalid receiverID"));
let closeNode = cloud.closestNode(request.receiverID);
if (!_list.serving.includes(closeNode))
return reject(INVALID("Incorrect Supernode"));
DB.searchData(closeNode, request)
.then(result => resolve([result]))
.catch(error => reject(error));
});
};
function processTagFromUser(data) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(data.receiverID))
return reject(INVALID("Invalid receiverID"));
let closeNode = cloud.closestNode(data.receiverID);
if (!_list.serving.includes(closeNode))
return reject(INVALID("Incorrect Supernode"));
DB.getData(closeNode, data.vectorClock).then(result => {
if (!result.length)
return reject(INVALID("Invalid vectorClock"));
result = result[0];
if (!(result.application in floGlobals.appList))
return reject(INVALID("Application not authorised"));
if (!floCrypto.validateAddr(data.requestorID) ||
(!floGlobals.appSubAdmins[result.application].includes(data.requestorID)
&& !floGlobals.appTrustedIDs[result.application].includes(data.requestorID)))
return reject(INVALID("Invalid requestorID"));
if (!floCrypto.verifyPubKey(data.pubKey, data.requestorID))
return reject(INVALID("Invalid pubKey"));
let hashcontent = ["time", "vectorClock", "tag"].map(d => data[d]).join("|");
if (!floCrypto.verifySign(hashcontent, data.sign, data.pubKey))
return reject(INVALID("Invalid signature"));
let tag = ([null, undefined, ""].includes(data.tag) ? null : data.tag.toString());
DB.tagData(closeNode, data.vectorClock, tag, data.time, data.pubKey, data.sign).then(rb => {
DB.getData(closeNode, data.vectorClock)
.then(result => resolve([result[0], 'TAG', rb]))
.catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
});
};
function processNoteFromUser(data) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateAddr(data.receiverID))
return reject(INVALID("Invalid receiverID"));
let closeNode = cloud.closestNode(data.receiverID);
if (!_list.serving.includes(closeNode))
return reject(INVALID("Incorrect Supernode"));
DB.getData(closeNode, data.vectorClock).then(result => {
if (!result.length)
return reject(INVALID("Invalid vectorClock"));
result = result[0];
if (result.application in floGlobals.appList && floGlobals.appList[result.application] === result.receiverID) {
if (!floGlobals.appSubAdmins[result.application].includes(data.requestorID) && result.receiverID !== data.requestorID)
return reject(INVALID("Invalid requestorID"));
} else if (result.receiverID !== data.requestorID)
return reject(INVALID("Invalid requestorID"));
if (!floCrypto.verifyPubKey(data.pubKey, data.requestorID))
return reject(INVALID("Invalid pubKey"));
let hashcontent = ["time", "vectorClock", "note"].map(d => data[d]).join("|");
if (!floCrypto.verifySign(hashcontent, data.sign, data.pubKey))
return reject(INVALID("Invalid signature"));
let note = ([null, undefined, ""].includes(data.note) ? null : data.note.toString());
DB.noteData(closeNode, data.vectorClock, note, data.time, data.pubKey, data.sign).then(rb => {
DB.getData(closeNode, data.vectorClock)
.then(result => resolve([result[0], 'NOTE', rb]))
.catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
function checkIfRequestSatisfy(request, data) {
if (!request || request.mostRecent || request.receiverID !== (data.proxyID || data.receiverID))
return false;
if (request.atVectorClock && request.atVectorClock !== data.vectorClock)
return false;
if (request.lowerVectorClock && request.lowerVectorClock > data.vectorClock)
return false;
if (request.upperVectorClock && request.upperVectorClock < data.vectorClock)
return false;
if (request.afterTime && request.afterTime > data.log_time)
return false;
if (request.application !== data.application)
return false;
if (request.comment && request.comment !== data.comment)
return false;
if (request.type && request.type !== data.type)
return false;
if (request.senderID) {
if (Array.isArray(request.senderID)) {
if (!request.senderID.includes(data.senderID))
return false;
} else if (request.senderID !== data.senderID)
return false;
};
return true;
};
function processStatusFromUser(request, ws) {
if (request.status) {
//Set user-online status
if (!request.floID || !request.application || !request.sign || !request.pubKey || !request.time)
return ws.send("Invalid request parameters");
if (!floCrypto.verifyPubKey(request.pubKey, request.floID))
return ws.send("Invalid pubKey");
let hashcontent = ["time", "application", "floID"].map(d => request[d]).join("|");
if (!floCrypto.verifySign(hashcontent, request.sign, request.pubKey))
return ws.send("Invalid signature");
clientOnline(ws, request.application, request.floID);
} else {
//Track online status
if (!request.application || !Array.isArray(request.trackList))
return ws.send("Invalid request parameters");
addRequestClient(ws, request.application, request.trackList);
}
}
let onlineClients = {},
requestClients = {};
const ONLINE = 1,
OFFLINE = 0;
function clientOnline(ws, application, floID) {
if (!(application in onlineClients))
onlineClients[application] = {};
if (floID in onlineClients[application])
onlineClients[application][floID] += 1;
else {
onlineClients[application][floID] = 1;
informRequestClients(application, floID, ONLINE);
}
ws.on('close', () => clientOffline(application, floID));
}
function clientOffline(application, floID) {
if (application in onlineClients)
if (floID in onlineClients[application]) {
if (onlineClients[application][floID] > 1)
onlineClients[application][floID] -= 1;
else {
delete onlineClients[application][floID];
informRequestClients(application, floID, OFFLINE);
if (!Object.keys(onlineClients[application]).length)
delete onlineClients[application];
}
}
}
function addRequestClient(ws, application, trackList) {
let id = Date.now() + floCrypto.randString(8);
if (!(application in requestClients))
requestClients[application] = {};
requestClients[application][id] = {
ws: ws,
trackList
};
let status = {};
if (application in onlineClients)
trackList.forEach(floID => status[floID] = (onlineClients[application][floID] ? ONLINE : OFFLINE));
else
trackList.forEach(floID => status[floID] = OFFLINE);
ws.send(JSON.stringify(status));
ws.on('close', () => rmRequestClient(application, id));
}
function rmRequestClient(application, id) {
if ((application in requestClients) && (id in requestClients[application])) {
delete requestClients[application][id];
if (!Object.keys(requestClients[application]).length)
delete requestClients[application];
}
}
function informRequestClients(application, floID, status) {
if (application in requestClients)
for (let r in requestClients[application])
if (requestClients[application][r].trackList.includes(floID))
requestClients[application][r].ws.send(JSON.stringify({
[floID]: status
}));
}
module.exports = {
checkIfRequestSatisfy,
processRequestFromUser,
processIncomingData,
processStatusFromUser
};

160
src/cloud.js Normal file
View File

@ -0,0 +1,160 @@
'use strict';
function K_Bucket(masterID, nodeList) {
const decodeID = function(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;
};
const _KB = new BuildKBucket({
localNodeId: decodeID(masterID)
});
nodeList.forEach(id => _KB.add({
id: decodeID(id),
floID: id
}));
const _CO = nodeList.map(sn => [_KB.distance(decodeID(masterID), decodeID(sn)), sn])
.sort((a, b) => a[0] - b[0])
.map(a => a[1]);
const self = this;
Object.defineProperty(self, 'order', {
get: () => Array.from(_CO)
});
self.innerNodes = function(id1, id2) {
if (!_CO.includes(id1) || !_CO.includes(id2))
throw Error('Given nodes are not in KB');
let iNodes = [];
for (let i = _CO.indexOf(id1) + 1; _CO[i] != id2; i++) {
if (i < _CO.length)
iNodes.push(_CO[i]);
else i = -1;
};
return iNodes;
};
self.outterNodes = function(id1, id2) {
if (!_CO.includes(id1) || !_CO.includes(id2))
throw Error('Given nodes are not in KB');
let oNodes = [];
for (let i = _CO.indexOf(id2) + 1; _CO[i] != id1; i++) {
if (i < _CO.length)
oNodes.push(_CO[i]);
else i = -1;
};
return oNodes;
};
self.prevNode = function(id, N = 1) {
let n = N || _CO.length;
if (!_CO.includes(id))
throw Error('Given node is not KB');
let pNodes = [];
for (let i = 0, j = _CO.indexOf(id) - 1; i < n; j--) {
if (j == _CO.indexOf(id))
break;
else if (j > -1)
pNodes[i++] = _CO[j];
else j = _CO.length;
};
return (N == 1 ? pNodes[0] : pNodes);
};
self.nextNode = function(id, N = 1) {
let n = N || _CO.length;
if (!_CO.includes(id))
throw Error('Given node is not KB');
let nNodes = [];
for (let i = 0, j = _CO.indexOf(id) + 1; i < n; j++) {
if (j == _CO.indexOf(id))
break;
else if (j < _CO.length)
nNodes[i++] = _CO[j];
else j = -1;
};
return (N == 1 ? nNodes[0] : nNodes);
};
self.closestNode = function(id, N = 1) {
let decodedId = decodeID(id);
let n = N || _CO.length;
let cNodes = _KB.closest(decodedId, n)
.map(k => k.floID);
return (N == 1 ? cNodes[0] : cNodes);
};
}
const blockchainPrefix = 0x23;
function proxyID(address) {
if (!address)
return;
var bytes;
if (address.length == 34) { //legacy encoding
let decode = bitjs.Base58.decode(address);
bytes = decode.slice(0, decode.length - 4);
let checksum = decode.slice(decode.length - 4),
hash = Crypto.SHA256(Crypto.SHA256(bytes, {
asBytes: true
}), {
asBytes: true
});
hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ?
bytes = undefined : bytes.shift();
} else if (address.length == 42 || address.length == 62) { //bech encoding
if (typeof coinjs !== 'function')
throw "library missing (lib_btc.js)";
let decode = coinjs.bech32_decode(address);
if (decode) {
bytes = decode.data;
bytes.shift();
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
if (address.length == 62) //for long bech, aggregate once more to get 160 bit
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
}
} else if (address.length == 66) { //public key hex
bytes = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address), {
asBytes: true
}));
}
if (!bytes)
throw "Invalid address: " + address;
else {
bytes.unshift(blockchainPrefix);
let hash = Crypto.SHA256(Crypto.SHA256(bytes, {
asBytes: true
}), {
asBytes: true
});
return bitjs.Base58.encode(bytes.concat(hash.slice(0, 4)));
}
}
var kBucket;
const cloud = module.exports = function Cloud(masterID, nodeList) {
kBucket = new K_Bucket(masterID, nodeList);
}
cloud.proxyID = (a) => proxyID(a);
cloud.closestNode = (id, N = 1) => kBucket.closestNode(proxyID(id), N);
cloud.prevNode = (id, N = 1) => kBucket.prevNode(id, N);
cloud.nextNode = (id, N = 1) => kBucket.nextNode(id, N);
cloud.innerNodes = (id1, id2) => kBucket.innerNodes(id1, id2);
cloud.outterNodes = (id1, id2) => kBucket.outterNodes(id1, id2);
Object.defineProperties(cloud, {
kb: {
get: () => kBucket
},
order: {
get: () => kBucket.order
}
});

33
src/data_structure.json Normal file
View File

@ -0,0 +1,33 @@
{
"H_struct": {
"VECTOR_CLOCK": "vectorClock",
"SENDER_ID": "senderID",
"RECEIVER_ID": "receiverID",
"TYPE": "type",
"APPLICATION": "application",
"TIME": "time",
"PUB_KEY": "pubKey"
},
"B_struct": {
"MESSAGE": "message",
"SIGNATURE": "sign",
"COMMENT": "comment"
},
"L_struct": {
"PROXY_ID": "proxyID",
"STATUS": "status_n",
"LOG_TIME": "log_time"
},
"T_struct": {
"TAG": "tag",
"TAG_TIME": "tag_time",
"TAG_KEY": "tag_key",
"TAG_SIGN": "tag_sign"
},
"F_struct": {
"NOTE": "note",
"NOTE_TIME": "note_time",
"NOTE_KEY": "note_key",
"NOTE_SIGN": "note_sign"
}
}

496
src/database.js Normal file
View File

@ -0,0 +1,496 @@
'use strict';
var mysql = require('mysql');
const Base_Tables = require("./base_tables.json");
const { H_struct, B_struct, L_struct, T_struct, F_struct } = require("./data_structure.json");
var pool; //container for pool
function initConnection(user, password, dbname, host = 'localhost') {
return new Promise((resolve, reject) => {
pool = mysql.createPool({
host: host,
user: user,
password: password,
database: dbname
});
getConnection().then(conn => {
conn.release();
resolve(DB);
}).catch(error => reject(error));
});
};
const getConnection = () => new Promise((resolve, reject) => {
pool.getConnection((error, conn) => {
if (error)
reject(error);
else
resolve(conn);
});
});
function queryResolve(sql, values) {
return new Promise((resolve, reject) => {
getConnection().then(conn => {
const fn = (err, res) => {
conn.release();
(err ? reject(err) : resolve(res));
};
if (values)
conn.query(sql, values, fn);
else
conn.query(sql, fn);
}).catch(error => reject(error));
})
}
function queryStream(sql, values, callback) {
return new Promise((resolve, reject) => {
getConnection().then(conn => {
if (!callback && values instanceof Function) {
callback = values;
values = undefined;
}
var err_flag, row_count = 0;
(values ? conn.query(sql, values) : conn.query(sql))
.on('error', err => err_flag = err)
.on('result', row => {
row_count++;
conn.pause();
callback(row);
conn.resume();
}).on('end', _ => {
conn.release();
err_flag ? reject(err_flag) : resolve(row_count);
});
})
})
}
const DB = {};
/* Base Tables */
DB.createBase = function () {
return new Promise((resolve, reject) => {
let statements = [];
for (let t in Base_Tables)
statements.push("CREATE TABLE IF NOT EXISTS " + t + "( " +
Object.keys(Base_Tables[t]).map(a => a + " " + Base_Tables[t][a]).join(", ") + " )");
Promise.all(statements.map(s => queryResolve(s)))
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.setLastTx = function (id, n) {
return new Promise((resolve, reject) => {
let statement = "INSERT INTO LastTxs (ID, N) VALUES (?, ?)" +
" ON DUPLICATE KEY UPDATE N=?";
queryResolve(statement, [id, n, n])
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.setConfig = function (name, value) {
return new Promise((resolve, reject) => {
value = JSON.stringify(value);
let statement = "INSERT INTO Configs (NAME, VAL) VALUES (?, ?)" +
" ON DUPLICATE KEY UPDATE VAL=?";
queryResolve(statement, [name, value, value])
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.addSuperNode = function (id, pubKey, uri) {
return new Promise((resolve, reject) => {
let statement = "INSERT INTO SuperNodes (FLO_ID, PUB_KEY, URI) VALUES (?, ?, ?)" +
" ON DUPLICATE KEY UPDATE URI=?";
queryResolve(statement, [id, pubKey, uri, uri])
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.rmSuperNode = function (id) {
return new Promise((resolve, reject) => {
let statement = "DELETE FROM SuperNodes" +
" WHERE FLO_ID=?";
queryResolve(statement, id)
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.updateSuperNode = function (id, uri) {
return new Promise((resolve, reject) => {
let statement = "UPDATE SuperNodes SET URI=? WHERE FLO_ID=?";
queryResolve(statement, [uri, id])
.then(result => resolve(result))
.catch(error => reject(error));
})
}
DB.setSubAdmin = function (appName, subAdmins) {
return new Promise((resolve, reject) => {
let statement = "UPDATE Applications" +
" SET SUB_ADMINS=?" +
" WHERE APP_NAME=?";
queryResolve(statement, [subAdmins.join(","), appName])
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.setTrustedIDs = function (appName, trustedIDs) {
return new Promise((resolve, reject) => {
let statement = "UPDATE Applications" +
" SET TRUSTED_IDS=?" +
" WHERE APP_NAME=?";
queryResolve(statement, [trustedIDs.join(","), appName])
.then(result => resolve(result))
.catch(error => reject(error));
});
}
DB.addApp = function (appName, adminID) {
return new Promise((resolve, reject) => {
let statement = "INSERT INTO Applications (APP_NAME, ADMIN_ID) VALUES (?, ?)" +
" ON DUPLICATE KEY UPDATE ADMIN_ID=?";
queryResolve(statement, [appName, adminID, adminID])
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.rmApp = function (appName) {
return new Promise((resolve, reject) => {
let statement = "DELETE FROM Applications" +
" WHERE APP_NAME=" + appName;
queryResolve(statement)
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.getBase = function () {
return new Promise((resolve, reject) => {
let tables = Object.keys(Base_Tables);
Promise.all(tables.map(t => queryResolve("SELECT * FROM " + t))).then(result => {
let tmp = Object.fromEntries(tables.map((t, i) => [t, result[i]]));
result = {};
result.lastTx = Object.fromEntries(tmp.LastTxs.map(a => [a.ID, a.N]));
result.sn_config = Object.fromEntries(tmp.Configs.map(a => [a.NAME, JSON.parse(a.VAL)]));
result.appList = Object.fromEntries(tmp.Applications.map(a => [a.APP_NAME, a.ADMIN_ID]));
result.appSubAdmins = Object.fromEntries(tmp.Applications.map(a => [a.APP_NAME, a.SUB_ADMINS ? a.SUB_ADMINS.split(",") : []]));
result.appTrustedIDs = Object.fromEntries(tmp.Applications.map(a => [a.APP_NAME, a.TRUSTED_IDS ? a.TRUSTED_IDS.split(",") : []]));
result.supernodes = Object.fromEntries(tmp.SuperNodes.map(a => [a.FLO_ID, {
pubKey: a.PUB_KEY,
uri: a.URI
}]));
resolve(result);
}).catch(error => reject(error));
});
};
/* Supernode Tables */
DB.createTable = function (snID) {
return new Promise((resolve, reject) => {
let statement = "CREATE TABLE IF NOT EXISTS _" + snID + " ( " +
H_struct.VECTOR_CLOCK + " VARCHAR(88) NOT NULL, " +
H_struct.SENDER_ID + " VARCHAR(72) NOT NULL, " +
H_struct.RECEIVER_ID + " VARCHAR(72) NOT NULL, " +
H_struct.APPLICATION + " TINYTEXT NOT NULL, " +
H_struct.TYPE + " TINYTEXT, " +
B_struct.MESSAGE + " LONGTEXT NOT NULL, " +
H_struct.TIME + " BIGINT NOT NULL, " +
B_struct.SIGNATURE + " VARCHAR(160) NOT NULL, " +
H_struct.PUB_KEY + " CHAR(66) NOT NULL, " +
B_struct.COMMENT + " TINYTEXT, " +
L_struct.PROXY_ID + " CHAR(34), " +
L_struct.STATUS + " INT NOT NULL, " +
L_struct.LOG_TIME + " BIGINT NOT NULL, " +
T_struct.TAG + " TINYTEXT, " +
T_struct.TAG_TIME + " BIGINT, " +
T_struct.TAG_KEY + " CHAR(66), " +
T_struct.TAG_SIGN + " VARCHAR(160), " +
F_struct.NOTE + " TINYTEXT, " +
F_struct.NOTE_TIME + " BIGINT, " +
F_struct.NOTE_KEY + " CHAR(66), " +
F_struct.NOTE_SIGN + " VARCHAR(160), " +
"PRIMARY KEY (" + H_struct.VECTOR_CLOCK + ")" +
" )";
queryResolve(statement)
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.dropTable = function (snID) {
return new Promise((resolve, reject) => {
let statement = "DROP TABLE _" + snID;
queryResolve(statement)
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.listTable = function () {
return new Promise((resolve, reject) => {
queryResolve("SHOW TABLES").then(result => {
const disks = [];
for (let i in result)
for (let j in result[i])
if (result[i][j].startsWith("_"))
disks.push(result[i][j].split("_")[1]);
resolve(disks);
}).catch(error => reject(error))
})
}
/* Data Service (by client)*/
DB.addData = function (snID, data) {
return new Promise((resolve, reject) => {
data[L_struct.STATUS] = 1;
data[L_struct.LOG_TIME] = Date.now();
let proxyID = cloud.proxyID(data[H_struct.RECEIVER_ID]);
data[L_struct.PROXY_ID] = proxyID !== data[H_struct.RECEIVER_ID] ? proxyID : null;
let attr = Object.keys(H_struct).map(a => H_struct[a])
.concat(Object.keys(B_struct).map(a => B_struct[a]))
.concat(Object.keys(L_struct).map(a => L_struct[a]));
let values = attr.map(a => data[a]);
let statement = "INSERT INTO _" + snID +
" (" + attr.join(", ") + ") " +
"VALUES (" + attr.map(a => '?').join(", ") + ")";
data = Object.fromEntries(attr.map((a, i) => [a, values[i]]));
queryResolve(statement, values)
.then(result => resolve(data))
.catch(error => reject(error));
});
};
DB.getData = function (snID, vectorClock) {
return new Promise((resolve, reject) => {
let statement = "SELECT * FROM _" + snID +
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
queryResolve(statement, [vectorClock])
.then(result => resolve(result))
.catch(error => reject(error))
})
};
DB.tagData = function (snID, vectorClock, tag, tagTime, tagKey, tagSign) {
return new Promise((resolve, reject) => {
let data = {
[T_struct.TAG]: tag,
[T_struct.TAG_TIME]: tagTime,
[T_struct.TAG_KEY]: tagKey,
[T_struct.TAG_SIGN]: tagSign,
[L_struct.LOG_TIME]: Date.now()
};
let attr = Object.keys(data);
let values = attr.map(a => data[a]).concat(vectorClock);
data[H_struct.VECTOR_CLOCK] = vectorClock; //also add vectorClock to resolve data
let statement = "UPDATE _" + snID +
" SET " + attr.map(a => a + "=?").join(", ") +
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
queryResolve(statement, values)
.then(result => resolve(data))
.catch(error => reject(error));
});
};
DB.noteData = function (snID, vectorClock, note, noteTime, noteKey, noteSign) {
return new Promise((resolve, reject) => {
let data = {
[F_struct.NOTE]: note,
[F_struct.NOTE_TIME]: noteTime,
[F_struct.NOTE_KEY]: noteKey,
[F_struct.NOTE_SIGN]: noteSign,
[L_struct.LOG_TIME]: Date.now()
};
let attr = Object.keys(data);
let values = attr.map(a => data[a]).concat(vectorClock);
data[H_struct.VECTOR_CLOCK] = vectorClock; //also add vectorClock to resolve data
let statement = "UPDATE _" + snID +
" SET " + attr.map(a => a + "=?").join(", ") +
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
queryResolve(statement, values)
.then(result => resolve(data))
.catch(error => reject(error));
});
}
DB.searchData = function (snID, request) {
return new Promise((resolve, reject) => {
let conditionArr = [];
if (request.lowerVectorClock || request.upperVectorClock || request.atVectorClock) {
if (request.atVectorClock)
conditionArr.push(`${H_struct.VECTOR_CLOCK} = '${request.atVectorClock}'`);
else if (request.lowerVectorClock && request.upperVectorClock)
conditionArr.push(`${H_struct.VECTOR_CLOCK} BETWEEN '${request.lowerVectorClock}' AND '${request.upperVectorClock}'`);
else if (request.lowerVectorClock)
conditionArr.push(`${H_struct.VECTOR_CLOCK} >= '${request.lowerVectorClock}'`);
else if (request.upperVectorClock)
conditionArr.push(`${H_struct.VECTOR_CLOCK} <= '${request.upperVectorClock}'`);
}
if (request.afterTime)
conditionArr.push(`${L_struct.LOG_TIME} > ${request.afterTime}`);
conditionArr.push(`${H_struct.APPLICATION} = '${request.application}'`);
conditionArr.push(`IFNULL(${L_struct.PROXY_ID}, ${H_struct.RECEIVER_ID}) = '${cloud.proxyID(request.receiverID)}'`);
if (request.comment)
conditionArr.push(`${B_struct.COMMENT} = '${request.comment}'`);
if (request.type)
conditionArr.push(`${H_struct.TYPE} = '${request.type}'`);
if (request.senderID) {
if (typeof request.senderID === "string" && request.senderID.includes(','))
request.senderID = request.senderID.split(',');
if (Array.isArray(request.senderID))
conditionArr.push(`${H_struct.SENDER_ID} IN ('${request.senderID.join("', '")}')`);
else
conditionArr.push(`${H_struct.SENDER_ID} = '${request.senderID}'`);
};
//console.log(conditionArr);
//let attr = Object.keys(H_struct).map(a => H_struct[a]).concat(Object.keys(B_struct).map(a => B_struct[a]));
//let statement = "SELECT " + attr.join(", ") + " FROM _" + snID +
let statement = "SELECT * FROM _" + snID +
" WHERE " + conditionArr.join(" AND ") +
" ORDER BY " + (request.afterTime ? L_struct.LOG_TIME : H_struct.VECTOR_CLOCK) +
(request.mostRecent ? " DESC LIMIT 1" : "");
queryResolve(statement)
.then(result => resolve(result))
.catch(error => reject(error));
});
};
/* Backup service */
DB.lastLogTime = function (snID) {
return new Promise((resolve, reject) => {
let attr = "MAX(" + L_struct.LOG_TIME + ")";
let statement = "SELECT " + attr + " FROM _" + snID;
queryResolve(statement)
.then(result => resolve(result[0][attr] || 0))
.catch(error => reject(error));
});
};
DB.createGetLastLog = function (snID) {
return new Promise((resolve, reject) => {
DB.createTable(snID).then(result => {
DB.lastLogTime(snID)
.then(result => resolve(result))
.catch(error => reject(error));
}).catch(error => reject(error));
});
};
DB.readAllDataStream = function (snID, logtime, callback) {
return new Promise((resolve, reject) => {
let statement = "SELECT * FROM _" + snID +
" WHERE " + L_struct.LOG_TIME + ">" + logtime +
" ORDER BY " + L_struct.LOG_TIME;
queryStream(statement, callback)
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.storeData = function (snID, data, updateLogTime = false) {
return new Promise((resolve, reject) => {
if (updateLogTime)
data[L_struct.LOG_TIME] = Date.now();
let u_attr = Object.keys(B_struct).map(a => B_struct[a])
.concat(Object.keys(L_struct).map(a => L_struct[a]))
.concat(Object.keys(T_struct).map(a => T_struct[a]))
.concat(Object.keys(F_struct).map(a => F_struct[a]));
let attr = Object.keys(H_struct).map(a => H_struct[a]).concat(u_attr);
let values = attr.map(a => data[a]);
let u_values = u_attr.map(a => data[a]);
let statement = "INSERT INTO _" + snID +
" (" + attr.join(", ") + ") " +
"VALUES (" + attr.map(a => '?').join(", ") + ") " +
"ON DUPLICATE KEY UPDATE " + u_attr.map(a => a + "=?").join(", ");
queryResolve(statement, values.concat(u_values))
.then(result => resolve(data))
.catch(error => reject(error));
});
};
DB.storeTag = function (snID, data) {
return new Promise((resolve, reject) => {
let attr = Object.keys(T_struct).map(a => T_struct[a]).concat(L_struct.LOG_TIME);
let values = attr.map(a => data[a]).concat(data[H_struct.VECTOR_CLOCK]);
let statement = "UPDATE _" + snID +
" SET " + attr.map(a => a + "=?").join(", ") +
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
queryResolve(statement, values)
.then(result => resolve(data))
.catch(error => reject(error));
});
};
DB.storeNote = function (snID, data) {
let attr = Object.keys(F_struct).map(a => F_struct[a]).concat(L_struct.LOG_TIME);
let values = attr.map(a => data[a]).concat(data[H_struct.VECTOR_CLOCK]);
let statement = "UPDATE _" + snID +
" SET " + attr.map(a => a + "=?").join(", ") +
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
queryResolve(statement, values)
.then(result => resolve(data))
.catch(error => reject(error));
}
DB.deleteData = function (snID, vectorClock) {
return new Promise((resolve, reject) => {
let statement = "DELETE FROM _" + snID +
" WHERE " + H_struct.VECTOR_CLOCK + "=?";
queryResolve(statement, [vectorClock])
.then(result => resolve(result))
.catch(error => reject(error));
});
};
/* Data clearing */
DB.clearAuthorisedAppData = function (snID, app, adminID, subAdmins, timestamp) {
return new Promise((resolve, reject) => {
let statement = "DELETE FROM _" + snID +
" WHERE ( " + H_struct.TIME + "<? AND " +
H_struct.APPLICATION + "=? AND " +
T_struct.TAG + " IS NULL )" +
(subAdmins.length ? " AND ( " +
H_struct.RECEIVER_ID + " != ? OR " +
H_struct.SENDER_ID + " NOT IN (" + subAdmins.map(a => "?").join(", ") + ") )" :
"");
queryResolve(statement, [timestamp, app, adminID].concat(subAdmins))
.then(result => resolve(result))
.catch(error => reject(error));
});
};
DB.clearUnauthorisedAppData = function (snID, appList, timestamp) {
return new Promise((resolve, reject) => {
let statement = "DELETE FROM _" + snID +
" WHERE " + H_struct.TIME + "<?" +
(appList.length ? " AND " +
H_struct.APPLICATION + " NOT IN (" + appList.map(a => "?").join(", ") + ")" :
"");
queryResolve(statement, [timestamp].concat(appList))
.then(result => resolve(result))
.catch(error => reject(error));
});
};
module.exports = {
init: initConnection,
query: queryResolve,
query_stream: queryStream,
DB
};

580
src/floBlockchainAPI.js Normal file
View File

@ -0,0 +1,580 @@
(function (EXPORTS) { //floBlockchainAPI v2.3.3e
/* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
'use strict';
const floBlockchainAPI = EXPORTS;
const DEFAULT = {
blockchain: floGlobals.blockchain,
apiURL: {
FLO: ['https://flosight.duckdns.org/', 'https://flosight.ranchimall.net/'],
FLO_TEST: ['https://testnet-flosight.duckdns.org', 'https://testnet.flocha.in/']
},
sendAmt: 0.001,
fee: 0.0005,
minChangeAmt: 0.0005,
receiverID: floGlobals.adminID
};
Object.defineProperties(floBlockchainAPI, {
sendAmt: {
get: () => DEFAULT.sendAmt,
set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null
},
fee: {
get: () => DEFAULT.fee,
set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null
},
defaultReceiver: {
get: () => DEFAULT.receiverID,
set: floID => DEFAULT.receiverID = floID
},
blockchain: {
get: () => DEFAULT.blockchain
}
});
if (floGlobals.sendAmt) floBlockchainAPI.sendAmt = floGlobals.sendAmt;
if (floGlobals.fee) floBlockchainAPI.fee = floGlobals.fee;
Object.defineProperties(floGlobals, {
sendAmt: {
get: () => DEFAULT.sendAmt,
set: amt => !isNaN(amt) ? DEFAULT.sendAmt = amt : null
},
fee: {
get: () => DEFAULT.fee,
set: fee => !isNaN(fee) ? DEFAULT.fee = fee : null
}
});
const allServerList = new Set(floGlobals.apiURL && floGlobals.apiURL[DEFAULT.blockchain] ? floGlobals.apiURL[DEFAULT.blockchain] : DEFAULT.apiURL[DEFAULT.blockchain]);
var serverList = Array.from(allServerList);
var curPos = floCrypto.randInt(0, serverList.length - 1);
function fetch_retry(apicall, rm_flosight) {
return new Promise((resolve, reject) => {
let i = serverList.indexOf(rm_flosight)
if (i != -1) serverList.splice(i, 1);
curPos = floCrypto.randInt(0, serverList.length - 1);
fetch_api(apicall, false)
.then(result => resolve(result))
.catch(error => reject(error));
})
}
function fetch_api(apicall, ic = true) {
return new Promise((resolve, reject) => {
if (serverList.length === 0) {
if (ic) {
serverList = Array.from(allServerList);
curPos = floCrypto.randInt(0, serverList.length - 1);
fetch_api(apicall, false)
.then(result => resolve(result))
.catch(error => reject(error));
} else
reject("No floSight server working");
} else {
let flosight = serverList[curPos];
fetch(flosight + apicall).then(response => {
if (response.ok)
response.json().then(data => resolve(data));
else {
fetch_retry(apicall, flosight)
.then(result => resolve(result))
.catch(error => reject(error));
}
}).catch(error => {
fetch_retry(apicall, flosight)
.then(result => resolve(result))
.catch(error => reject(error));
})
}
})
}
Object.defineProperties(floBlockchainAPI, {
serverList: {
get: () => Array.from(serverList)
},
current_server: {
get: () => serverList[curPos]
}
});
//Promised function to get data from API
const promisedAPI = floBlockchainAPI.promisedAPI = floBlockchainAPI.fetch = function (apicall) {
return new Promise((resolve, reject) => {
//console.log(apicall);
fetch_api(apicall)
.then(result => resolve(result))
.catch(error => reject(error));
});
}
//Get balance for the given Address
const getBalance = floBlockchainAPI.getBalance = function (addr) {
return new Promise((resolve, reject) => {
promisedAPI(`api/addr/${addr}/balance`)
.then(balance => resolve(parseFloat(balance)))
.catch(error => reject(error));
});
}
//Send Tx to blockchain
const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
else if (!floCrypto.validateFloID(senderAddr))
return reject(`Invalid address : ${senderAddr}`);
else if (!floCrypto.validateFloID(receiverAddr))
return reject(`Invalid address : ${receiverAddr}`);
else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr))
return reject("Invalid Private key!");
else if (typeof sendAmt !== 'number' || sendAmt <= 0)
return reject(`Invalid sendAmt : ${sendAmt}`);
getBalance(senderAddr).then(balance => {
var fee = DEFAULT.fee;
if (balance < sendAmt + fee)
return reject("Insufficient FLO balance!");
//get unconfirmed tx list
promisedAPI(`api/addr/${senderAddr}`).then(result => {
readTxs(senderAddr, 0, result.unconfirmedTxApperances).then(result => {
let unconfirmedSpent = {};
for (let tx of result.items)
if (tx.confirmations == 0)
for (let vin of tx.vin)
if (vin.addr === senderAddr) {
if (Array.isArray(unconfirmedSpent[vin.txid]))
unconfirmedSpent[vin.txid].push(vin.vout);
else
unconfirmedSpent[vin.txid] = [vin.vout];
}
//get utxos list
promisedAPI(`api/addr/${senderAddr}/utxo`).then(utxos => {
//form/construct the transaction data
var trx = bitjs.transaction();
var utxoAmt = 0.0;
for (var i = utxos.length - 1;
(i >= 0) && (utxoAmt < sendAmt + fee); i--) {
//use only utxos with confirmations (strict_utxo mode)
if (utxos[i].confirmations || !strict_utxo) {
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
continue; //A transaction has already used the utxo, but is unconfirmed.
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
utxoAmt += utxos[i].amount;
};
}
if (utxoAmt < sendAmt + fee)
reject("Insufficient FLO: Some UTXOs are unconfirmed");
else {
trx.addoutput(receiverAddr, sendAmt);
var change = utxoAmt - sendAmt - fee;
if (change > DEFAULT.minChangeAmt)
trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
var signedTxHash = trx.sign(privKey, 1);
broadcastTx(signedTxHash)
.then(txid => resolve(txid))
.catch(error => reject(error))
}
}).catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
});
}
//Write Data into blockchain
floBlockchainAPI.writeData = function (senderAddr, data, privKey, receiverAddr = DEFAULT.receiverID, options = {}) {
let strict_utxo = options.strict_utxo === false ? false : true,
sendAmt = isNaN(options.sendAmt) ? DEFAULT.sendAmt : options.sendAmt;
return new Promise((resolve, reject) => {
if (typeof data != "string")
data = JSON.stringify(data);
sendTx(senderAddr, receiverAddr, sendAmt, privKey, data, strict_utxo)
.then(txid => resolve(txid))
.catch(error => reject(error));
});
}
//merge all UTXOs of a given floID into a single UTXO
floBlockchainAPI.mergeUTXOs = function (floID, privKey, floData = '') {
return new Promise((resolve, reject) => {
if (!floCrypto.validateFloID(floID))
return reject(`Invalid floID`);
if (!floCrypto.verifyPrivKey(privKey, floID))
return reject("Invalid Private Key");
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
var trx = bitjs.transaction();
var utxoAmt = 0.0;
var fee = DEFAULT.fee;
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);
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}
*/
floBlockchainAPI.writeDataMultiple = function (senderPrivKeys, data, receivers = [DEFAULT.receiverID], 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 = (DEFAULT.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 = DEFAULT.sendAmt;
receivers.forEach(floID => tmp[floID] = amount);
receivers = tmp
}
if (typeof data != "string")
data = JSON.stringify(data);
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}
*/
const sendTxMultiple = floBlockchainAPI.sendTxMultiple = function (senderPrivKeys, receivers, floData = '') {
return new Promise((resolve, reject) => {
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
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.validateFloID(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(getBalance(floID));
Promise.all(promises).then(results => {
let totalBalance = 0,
totalFee = DEFAULT.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 (let floID in receivers)
totalSendAmt += receivers[floID];
if (totalBalance < totalSendAmt)
return reject("Insufficient total Balance");
//Get the UTXOs of the senders
let promises = [];
for (let floID in senders)
promises.push(promisedAPI(`api/addr/${floID}/utxo`));
Promise.all(promises).then(results => {
let wifSeq = [];
var trx = bitjs.transaction();
for (let 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 (let 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();
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
const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) {
return new Promise((resolve, reject) => {
if (signedTxHash.length < 1)
return reject("Empty Signature");
var url = serverList[curPos] + 'api/tx/send';
fetch(url, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: `{"rawtx":"${signedTxHash}"}`
}).then(response => {
if (response.ok)
response.json().then(data => resolve(data.txid.result));
else
response.text().then(data => resolve(data));
}).catch(error => reject(error));
})
}
floBlockchainAPI.getTx = function (txid) {
return new Promise((resolve, reject) => {
promisedAPI(`api/tx/${txid}`)
.then(response => resolve(response))
.catch(error => reject(error))
})
}
//Read Txs of Address between from and to
const readTxs = floBlockchainAPI.readTxs = function (addr, from, to) {
return new Promise((resolve, reject) => {
promisedAPI(`api/addrs/${addr}/txs?from=${from}&to=${to}`)
.then(response => resolve(response))
.catch(error => reject(error))
});
}
//Read All Txs of Address (newest first)
floBlockchainAPI.readAllTxs = function (addr) {
return new Promise((resolve, reject) => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=${response.totalItems}0`)
.then(response => resolve(response.items))
.catch(error => reject(error));
}).catch(error => reject(error))
});
}
/*Read flo Data from txs of given Address
options can be used to filter data
limit : maximum number of filtered data (default = 1000, negative = no limit)
ignoreOld : ignore old txs (default = 0)
sentOnly : filters only sent data
receivedOnly: filters only received data
pattern : filters data that with JSON pattern
filter : custom filter funtion for floData (eg . filter: d => {return d[0] == '$'})
tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details)
sender : flo-id(s) of sender
receiver : flo-id(s) of receiver
*/
floBlockchainAPI.readData = function (addr, options = {}) {
options.limit = options.limit || 0;
options.ignoreOld = options.ignoreOld || 0;
if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receivers === "string") options.receivers = [options.receivers];
return new Promise((resolve, reject) => {
promisedAPI(`api/addrs/${addr}/txs?from=0&to=1`).then(response => {
var newItems = response.totalItems - options.ignoreOld;
promisedAPI(`api/addrs/${addr}/txs?from=0&to=${newItems * 2}`).then(response => {
if (options.limit <= 0)
options.limit = response.items.length;
var filteredData = [];
let numToRead = response.totalItems - options.ignoreOld,
unconfirmedCount = 0;
for (let i = 0; i < numToRead && filteredData.length < options.limit; i++) {
if (!response.items[i].confirmations) { //unconfirmed transactions
unconfirmedCount++;
if (numToRead < response.items[i].length)
numToRead++;
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.sentOnly) {
let flag = false;
for (let vin of response.items[i].vin)
if (vin.addr === addr) {
flag = true;
break;
}
if (!flag) continue;
}
if (Array.isArray(options.senders)) {
let flag = false;
for (let vin of response.items[i].vin)
if (options.senders.includes(vin.addr)) {
flag = true;
break;
}
if (!flag) continue;
}
if (options.receivedOnly) {
let flag = false;
for (let vout of response.items[i].vout)
if (vout.scriptPubKey.addresses[0] === addr) {
flag = true;
break;
}
if (!flag) continue;
}
if (Array.isArray(options.receivers)) {
let flag = false;
for (let vout of response.items[i].vout)
if (options.receivers.includes(vout.scriptPubKey.addresses[0])) {
flag = true;
break;
}
if (!flag) continue;
}
if (options.filter && !options.filter(response.items[i].floData))
continue;
if (options.tx) {
let d = {}
d.txid = response.items[i].txid;
d.time = response.items[i].time;
d.blockheight = response.items[i].blockheight;
d.senders = new Set(response.items[i].vin.map(v => v.addr));
d.receivers = new Set(response.items[i].vout.map(v => v.scriptPubKey.addresses[0]));
d.data = response.items[i].floData;
filteredData.push(d);
} else
filteredData.push(response.items[i].floData);
}
resolve({
totalTxs: response.totalItems - unconfirmedCount,
data: filteredData
});
}).catch(error => {
reject(error);
});
}).catch(error => {
reject(error);
});
});
}
})('object' === typeof module ? module.exports : window.floBlockchainAPI = {});

370
src/floCrypto.js Normal file
View File

@ -0,0 +1,370 @@
(function(EXPORTS) { //floCrypto v2.3.3
/* FLO Crypto Operators */
'use strict';
const floCrypto = EXPORTS;
const p = BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16);
const ecparams = EllipticCurve.getSECCurveByName("secp256k1");
const ascii_alternatives = ` '\n '\n“ "\n” "\n --\n— ---\n≥ >=\n≤ <=\n≠ !=\n× *\n÷ /\n← <-\n→ ->\n↔ <->\n⇒ =>\n⇐ <=\n⇔ <=>`;
const exponent1 = () => p.add(BigInteger.ONE).divide(BigInteger("4"));
function calculateY(x) {
let exp = exponent1();
// x is x value of public key in BigInteger format without 02 or 03 or 04 prefix
return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p)
}
function getUncompressedPublicKey(compressedPublicKey) {
// Fetch x from compressedPublicKey
let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey);
const prefix = pubKeyBytes.shift() // remove prefix
let prefix_modulus = prefix % 2;
pubKeyBytes.unshift(0) // add prefix 0
let x = new BigInteger(pubKeyBytes)
let xDecimalValue = x.toString()
// Fetch y
let y = calculateY(x);
let yDecimalValue = y.toString();
// verify y value
let resultBigInt = y.mod(BigInteger("2"));
let check = resultBigInt.toString() % 2;
if (prefix_modulus !== check)
yDecimalValue = y.negate().mod(p).toString();
return {
x: xDecimalValue,
y: yDecimalValue
};
}
function getSenderPublicKeyString() {
let privateKey = ellipticCurveEncryption.senderRandom();
var senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey);
return {
privateKey: privateKey,
senderPublicKeyString: senderPublicKeyString
}
}
function deriveSharedKeySender(receiverPublicKeyHex, senderPrivateKey) {
let receiverPublicKeyString = getUncompressedPublicKey(receiverPublicKeyHex);
var senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation(
receiverPublicKeyString.x, receiverPublicKeyString.y, senderPrivateKey);
return senderDerivedKey;
}
function deriveSharedKeyReceiver(senderPublicKeyString, receiverPrivateKey) {
return ellipticCurveEncryption.receiverSharedKeyDerivation(
senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString, receiverPrivateKey);
}
function getReceiverPublicKeyString(privateKey) {
return ellipticCurveEncryption.receiverPublicString(privateKey);
}
function wifToDecimal(pk_wif, isPubKeyCompressed = false) {
let pk = Bitcoin.Base58.decode(pk_wif)
pk.shift()
pk.splice(-4, 4)
//If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01).
if (isPubKeyCompressed == true) pk.pop()
pk.unshift(0)
let privateKeyDecimal = BigInteger(pk).toString()
let privateKeyHex = Crypto.util.bytesToHex(pk)
return {
privateKeyDecimal: privateKeyDecimal,
privateKeyHex: privateKeyHex
}
}
//generate a random Interger within range
floCrypto.randInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
//generate a random String within length (options : alphaNumeric chars only)
floCrypto.randString = function(length, alphaNumeric = true) {
var result = '';
var characters = alphaNumeric ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' :
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_+-./*?@#&$<>=[]{}():';
for (var i = 0; i < length; i++)
result += characters.charAt(Math.floor(Math.random() * characters.length));
return result;
}
//Encrypt Data using public-key
floCrypto.encryptData = function(data, receiverPublicKeyHex) {
var senderECKeyData = getSenderPublicKeyString();
var senderDerivedKey = deriveSharedKeySender(receiverPublicKeyHex, senderECKeyData.privateKey);
let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue;
let secret = Crypto.AES.encrypt(data, senderKey);
return {
secret: secret,
senderPublicKeyString: senderECKeyData.senderPublicKeyString
};
}
//Decrypt Data using private-key
floCrypto.decryptData = function(data, privateKeyHex) {
var receiverECKeyData = {};
if (typeof privateKeyHex !== "string") throw new Error("No private key found.");
let privateKey = wifToDecimal(privateKeyHex, true);
if (typeof privateKey.privateKeyDecimal !== "string") throw new Error("Failed to detremine your private key.");
receiverECKeyData.privateKey = privateKey.privateKeyDecimal;
var receiverDerivedKey = deriveSharedKeyReceiver(data.senderPublicKeyString, receiverECKeyData.privateKey);
let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue;
let decryptMsg = Crypto.AES.decrypt(data.secret, receiverKey);
return decryptMsg;
}
//Sign data using private-key
floCrypto.signData = function(data, privateKeyHex) {
var key = new Bitcoin.ECKey(privateKeyHex);
var messageHash = Crypto.SHA256(data);
var messageSign = Bitcoin.ECDSA.sign(messageHash, key.priv);
var sighex = Crypto.util.bytesToHex(messageSign);
return sighex;
}
//Verify signatue of the data using public-key
floCrypto.verifySign = function(data, signatureHex, publicKeyHex) {
var msgHash = Crypto.SHA256(data);
var sigBytes = Crypto.util.hexToBytes(signatureHex);
var publicKeyPoint = ecparams.getCurve().decodePointHex(publicKeyHex);
var verify = Bitcoin.ECDSA.verify(msgHash, sigBytes, publicKeyPoint);
return verify;
}
//Generates a new flo ID and returns private-key, public-key and floID
const generateNewID = floCrypto.generateNewID = function() {
var key = new Bitcoin.ECKey(false);
key.setCompressed(true);
return {
floID: key.getBitcoinAddress(),
pubKey: key.getPubKeyHex(),
privKey: key.getBitcoinWalletImportFormat()
}
}
Object.defineProperty(floCrypto, 'newID', {
get: () => generateNewID()
});
//Returns public-key from private-key
floCrypto.getPubKeyHex = function(privateKeyHex) {
if (!privateKeyHex)
return null;
var key = new Bitcoin.ECKey(privateKeyHex);
if (key.priv == null)
return null;
key.setCompressed(true);
return key.getPubKeyHex();
}
//Returns flo-ID from public-key or private-key
floCrypto.getFloID = function(keyHex) {
if (!keyHex)
return null;
try {
var key = new Bitcoin.ECKey(keyHex);
if (key.priv == null)
key.setPub(keyHex);
return key.getBitcoinAddress();
} catch {
return null;
}
}
//Verify the private-key for the given public-key or flo-ID
floCrypto.verifyPrivKey = function(privateKeyHex, pubKey_floID, isfloID = true) {
if (!privateKeyHex || !pubKey_floID)
return false;
try {
var key = new Bitcoin.ECKey(privateKeyHex);
if (key.priv == null)
return false;
key.setCompressed(true);
if (isfloID && pubKey_floID == key.getBitcoinAddress())
return true;
else if (!isfloID && pubKey_floID == key.getPubKeyHex())
return true;
else
return false;
} catch {
return null;
}
}
//Check if the given flo-id is valid or not
floCrypto.validateFloID = function(floID) {
if (!floID)
return false;
try {
let addr = new Bitcoin.Address(floID);
return true;
} catch {
return false;
}
}
//Check if the given address (any blockchain) is valid or not
floCrypto.validateAddr = function(address, std = true, bech = true) {
if (address.length == 34) { //legacy or segwit encoding
if (std === false)
return false;
let decode = bitjs.Base58.decode(address);
var raw = decode.slice(0, decode.length - 4),
checksum = decode.slice(decode.length - 4);
var hash = Crypto.SHA256(Crypto.SHA256(raw, {
asBytes: true
}), {
asBytes: true
});
if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3])
return false;
else if (std === true || (!Array.isArray(std) && std === raw[0]) || (Array.isArray(std) && std.includes(raw[0])))
return true;
else
return false;
} else if (address.length == 42 || address.length == 62) { //bech encoding
if (bech === false)
return false;
let decode = coinjs.bech32_decode(address);
if (!decode)
return false;
var raw = decode.data;
if (bech === true || (!Array.isArray(bech) && bech === raw[0]) || (Array.isArray(bech) && bech.includes(raw[0])))
return true;
else
return false;
} else //unknown length
return false;
}
floCrypto.verifyPubKey = function(pubKeyHex, address) {
let pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), {
asBytes: true
})));
if (address.length == 34) { //legacy encoding
let decode = bitjs.Base58.decode(address);
var raw = decode.slice(0, decode.length - 4),
checksum = decode.slice(decode.length - 4);
var hash = Crypto.SHA256(Crypto.SHA256(raw, {
asBytes: true
}), {
asBytes: true
});
if (hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3])
return false;
raw.shift();
return pub_hash === Crypto.util.bytesToHex(raw);
} else if (address.length == 42 || address.length == 62) { //bech encoding
let decode = coinjs.bech32_decode(address);
if (!decode)
return false;
var raw = decode.data;
raw.shift();
raw = coinjs.bech32_convert(raw, 5, 8, false);
return pub_hash === Crypto.util.bytesToHex(raw);
} else //unknown length
return false;
}
//Split the str using shamir's Secret and Returns the shares
floCrypto.createShamirsSecretShares = function(str, total_shares, threshold_limit) {
try {
if (str.length > 0) {
var strHex = shamirSecretShare.str2hex(str);
var shares = shamirSecretShare.share(strHex, total_shares, threshold_limit);
return shares;
}
return false;
} catch {
return false
}
}
//Returns the retrived secret by combining the shamirs shares
const retrieveShamirSecret = floCrypto.retrieveShamirSecret = function(sharesArray) {
try {
if (sharesArray.length > 0) {
var comb = shamirSecretShare.combine(sharesArray.slice(0, sharesArray.length));
comb = shamirSecretShare.hex2str(comb);
return comb;
}
return false;
} catch {
return false;
}
}
//Verifies the shares and str
floCrypto.verifyShamirsSecret = function(sharesArray, str) {
if (!str)
return null;
else if (retrieveShamirSecret(sharesArray) === str)
return true;
else
return false;
}
const validateASCII = floCrypto.validateASCII = function(string, bool = true) {
if (typeof string !== "string")
return null;
if (bool) {
let x;
for (let i = 0; i < string.length; i++) {
x = string.charCodeAt(i);
if (x < 32 || x > 127)
return false;
}
return true;
} else {
let x, invalids = {};
for (let i = 0; i < string.length; i++) {
x = string.charCodeAt(i);
if (x < 32 || x > 127)
if (x in invalids)
invalids[string[i]].push(i)
else
invalids[string[i]] = [i];
}
if (Object.keys(invalids).length)
return invalids;
else
return true;
}
}
floCrypto.convertToASCII = function(string, mode = 'soft-remove') {
let chars = validateASCII(string, false);
if (chars === true)
return string;
else if (chars === null)
return null;
let convertor, result = string,
refAlt = {};
ascii_alternatives.split('\n').forEach(a => refAlt[a[0]] = a.slice(2));
mode = mode.toLowerCase();
if (mode === "hard-unicode")
convertor = (c) => `\\u${('000'+c.charCodeAt().toString(16)).slice(-4)}`;
else if (mode === "soft-unicode")
convertor = (c) => refAlt[c] || `\\u${('000'+c.charCodeAt().toString(16)).slice(-4)}`;
else if (mode === "hard-remove")
convertor = c => "";
else if (mode === "soft-remove")
convertor = c => refAlt[c] || "";
else
return null;
for (let c in chars)
result = result.replaceAll(c, convertor(c));
return result;
}
floCrypto.revertUnicode = function(string) {
return string.replace(/\\u[\dA-F]{4}/gi,
m => String.fromCharCode(parseInt(m.replace(/\\u/g, ''), 16)));
}
})('object' === typeof module ? module.exports : window.floCrypto = {});

24
src/floGlobals.js Normal file
View File

@ -0,0 +1,24 @@
const floGlobals = {
//Required for all
blockchain: "FLO",
SNStorageID: "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",
//Required for Supernode operations
supernodes: {}, //each supnernode must be stored as floID : {uri:<uri>,pubKey:<publicKey>}
appList: {},
appSubAdmins: {},
sn_config: {}
/* 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 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
blockInterval - (Interger) Duration (milliseconds) of a single block (for backup sync)
*/
};
(typeof global !== "undefined" ? global : window).cryptocoin = floGlobals.blockchain;
('object' === typeof module) ? module.exports = floGlobals : null;

37
src/keys.js Normal file
View File

@ -0,0 +1,37 @@
'use strict';
const PRIV_EKEY_MIN = 32,
PRIV_EKEY_MAX = 48;
var node_priv, e_key, node_id, node_pub; //containers for node-key wrapper
const _x = {
get node_priv() {
if (!node_priv || !e_key)
throw Error("keys not set");
return Crypto.AES.decrypt(node_priv, e_key);
},
set node_priv(key) {
node_pub = floCrypto.getPubKeyHex(key);
node_id = floCrypto.getFloID(node_pub);
if (!key || !node_pub || !node_id)
throw Error("Invalid Keys");
let n = floCrypto.randInt(PRIV_EKEY_MIN, PRIV_EKEY_MAX)
e_key = floCrypto.randString(n);
node_priv = Crypto.AES.encrypt(key, e_key);
}
}
module.exports = {
set node_priv(key) {
_x.node_priv = key;
},
get node_priv() {
return _x.node_priv;
},
get node_id() {
return node_id;
},
get node_pub() {
return node_pub;
}
}

9345
src/lib.js Normal file

File diff suppressed because it is too large Load Diff

288
src/main.js Normal file
View File

@ -0,0 +1,288 @@
global.floGlobals = require("./floGlobals");
require('./set_globals');
require('./lib');
global.cloud = require('./cloud');
global.floCrypto = require('./floCrypto');
global.floBlockchainAPI = require('./floBlockchainAPI');
const Database = require("./database");
const intra = require('./backup/intra');
const Server = require('./server');
const keys = require("./keys");
const { INTERVAL_REFRESH_TIME } = require("./_constants");
const DB = Database.DB;
function startNode() {
const config = require(`../args/config.json`);
let _pass;
for (let arg of process.argv)
if (/^-password=/i.test(arg))
_pass = arg.split(/=(.*)/s)[1];
try {
let _tmp = require(`../args/keys.json`);
_tmp = floCrypto.retrieveShamirSecret(_tmp);
if (!_pass) {
console.error('Password not entered!');
process.exit(1);
}
keys.node_priv = Crypto.AES.decrypt(_tmp, _pass);
} catch (error) {
console.error('Unable to load private key!');
process.exit(1);
}
console.info("Logged in as", keys.node_id);
//DB connect
Database.init(config["sql_user"], config["sql_pwd"], config["sql_db"], config["sql_host"]).then(db => {
console.info("Connected to Database");
loadBase().then(base => {
console.log("Load Database successful");
//Set base data from DB to floGlobals
floGlobals.supernodes = base.supernodes;
floGlobals.sn_config = base.sn_config;
floGlobals.appList = base.appList;
floGlobals.appSubAdmins = base.appSubAdmins;
floGlobals.appTrustedIDs = base.appTrustedIDs;
refreshData.base = base;
refreshData.invoke(null)
.then(_ => intra.reconnectNextNode()).catch(_ => null);
//Start Server
const server = new Server(config["port"]);
server.refresher = refreshData;
intra.refresher = refreshData;
}).catch(error => console.error(error));
}).catch(error => console.error(error));
};
function loadBase() {
return new Promise((resolve, reject) => {
DB.createBase().then(result => {
DB.getBase()
.then(result => resolve(result))
.catch(error => reject(error));
}).catch(error => reject(error));
});
};
const refreshData = {
count: null,
base: null,
refresh_instance: null,
invoke(flag = true) {
return new Promise((resolve, reject) => {
const self = this;
console.info("Refresher processor has started at " + Date());
if (self.refresh_instance !== null) {
clearInterval(self.refresh_instance);
self.refresh_instance = null;
}
refreshBlockchainData(self.base, flag).then(result => {
console.log(result);
self.count = floGlobals.sn_config.refreshDelay;
diskCleanUp(self.base)
.then(result => console.log(result))
.catch(warn => console.warn(warn))
.finally(_ => {
console.info("Refresher processor has finished at " + Date());
resolve(true);
});
}).catch(error => {
console.error(error);
reject(false);
}).finally(_ => {
self.refresh_instance = setInterval(() => {
refreshBlockchainData(self.base, true)
.then(result => console.log(result))
.catch(error => console.error(error))
}, INTERVAL_REFRESH_TIME)
});
});
},
get countdown() {
this.count--;
if (this.count <= 0)
this.invoke().then(_ => null).catch(_ => null);
}
};
function refreshBlockchainData(base, flag) {
return new Promise((resolve, reject) => {
readSupernodeConfigFromAPI(base, flag).then(result => {
console.log(result);
cloud(floGlobals.SNStorageID, Object.keys(floGlobals.supernodes));
console.log("NodeList (ordered):", cloud.order);
readAppSubAdminListFromAPI(base)
.then(result => console.log(result))
.catch(warn => console.warn(warn))
.finally(_ => resolve("Refreshed Data from blockchain"));
}).catch(error => reject(error));
});
};
function readSupernodeConfigFromAPI(base, flag) {
return new Promise((resolve, reject) => {
floBlockchainAPI.readData(floGlobals.SNStorageID, {
ignoreOld: base.lastTx[floGlobals.SNStorageID],
sentOnly: true,
pattern: "SuperNodeStorage"
}).then(result => {
let promises = [],
node_change = {},
node_update = new Set();
result.data.reverse().forEach(data => {
var content = JSON.parse(data).SuperNodeStorage;
if (content.removeNodes)
for (let sn of content.removeNodes) {
promises.push(DB.rmSuperNode(sn));
delete base.supernodes[sn];
if (node_change[sn] === true)
delete node_change[sn];
else
node_change[sn] = false;
};
if (content.newNodes)
for (let sn in content.newNodes) {
promises.push(DB.addSuperNode(sn, content.newNodes[sn].pubKey, content.newNodes[sn].uri));
base.supernodes[sn] = {
pubKey: content.newNodes[sn].pubKey,
uri: content.newNodes[sn].uri
};
if (node_change[sn] === false)
delete node_change[sn];
else
node_change[sn] = true;
};
if (content.updateNodes)
for (let sn in content.updateNodes) {
promises.push(DB.updateSuperNode(sn, content.updateNodes[sn]));
base.supernodes[sn].uri = content.updateNodes[sn];
node_update.add(sn);
}
if (content.config)
for (let c in content.config) {
promises.push(DB.setConfig(c, content.config[c]));
base.sn_config[c] = content.config[c];
};
if (content.removeApps)
for (let app of content.removeApps) {
promises.push(DB.rmApp(app));
delete base.appList;
};
if (content.addApps)
for (let app in content.addApps) {
promises.push(DB.addApp(app, content.addApps[app]));
base.appList[app] = content.addApps[app];
};
});
promises.push(DB.setLastTx(floGlobals.SNStorageID, result.totalTxs));
//Check if all save process were successful
Promise.allSettled(promises).then(results => {
if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0))
console.warn("Some data might not have been saved in database correctly");
});
//Process data migration if nodes are changed
if (Object.keys(node_change).length || node_update.size) {
if (flag === null) {
if (Object.keys(node_change).length)
selfDiskMigration(node_change); //When node starts for the 1st time after been inactive, but migration has already taken place.
}
else
intra.dataMigration(node_change, flag);
}
resolve('Updated Supernode Configuration');
}).catch(error => reject(error));
});
};
function readAppSubAdminListFromAPI(base) {
var promises = [];
//Load for each apps
for (let app in base.appList) {
promises.push(new Promise((resolve, reject) => {
floBlockchainAPI.readData(base.appList[app], {
ignoreOld: base.lastTx[base.appList[app]] || 0,
sentOnly: true,
pattern: app
}).then(result => {
let subAdmins = new Set(base.appSubAdmins[app]),
trustedIDs = new Set(base.appTrustedIDs[app]);
result.data.reverse().forEach(data => {
let content = JSON.parse(data)[app];
if (Array.isArray(content.removeSubAdmin))
content.removeSubAdmin.forEach(sa => subAdmins.delete(sa));
if (Array.isArray(content.addSubAdmin))
content.addSubAdmin.forEach(sa => subAdmins.add(sa));
if (Array.isArray(content.removeTrustedID))
content.removeTrustedID.forEach(sa => trustedIDs.delete(sa));
if (Array.isArray(content.addTrustedID))
content.addTrustedID.forEach(sa => trustedIDs.add(sa));
});
base.appSubAdmins[app] = Array.from(subAdmins);
base.appTrustedIDs[app] = Array.from(trustedIDs);
Promise.allSettled([
DB.setLastTx(base.appList[app], result.totalTxs),
DB.setSubAdmin(app, base.appSubAdmins[app]),
DB.setTrustedIDs(app, base.appTrustedIDs[app])
]).then(results => {
if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0))
console.warn(`SubAdmin list for app(${app}) might not have been saved in database`);
});
resolve("Loaded subAdmin List for APP:" + app);
}).catch(error => reject([app, error]));
}));
};
return new Promise((resolve, reject) => {
Promise.allSettled(promises).then(results => {
if (results.reduce((a, r) => r.status === "rejected" ? ++a : a, 0)) {
let error = Object.fromEntries(results.filter(r => r.status === "rejected").map(r => r.reason));
console.debug(error);
reject(`subAdmin List for APPS(${Object.keys(error)}) might not have loaded correctly`);
} else
resolve("Loaded subAdmin List for all APPs successfully");
});
});
};
function diskCleanUp(base) {
return new Promise((resolve, reject) => {
let time = Date.now() - base.sn_config.deleteDelay,
promises = [];
intra._list.serving.forEach(sn => {
//delete all when app is not authorised.
promises.push(DB.clearUnauthorisedAppData(sn, Object.keys(base.appList), time));
//for each authorised app: delete unofficial data (untaged, unknown sender/receiver)
for (let app in base.appList)
promises.push(DB.clearAuthorisedAppData(sn, app, base.appList[app], base.appSubAdmins[app], time));
});
Promise.allSettled(promises).then(results => {
let failed = results.filter(r => r.status === "rejected").map(r => r.reason);
if (failed.length) {
console.error(JSON.stringify(failed));
let success = results.length - failed.length;
reject(`Disk clean-up process has failed at ${100 * success / results.length}%. (Success:${success}|Failed:${failed.count})`);
} else
resolve("Disk clean-up process finished successfully (100%)");
}).catch(error => reject(error));
});
};
function selfDiskMigration(node_change) {
DB.listTable("SHOW TABLES").then(disks => {
disks.forEach(n => {
if (node_change[n] === false)
DB.dropTable(n).then(_ => null).catch(e => console.error(e));
DB.readAllDataStream(n, 0, d => {
let closest = cloud.closestNode(d.receiverID);
if (closest !== n)
DB.deleteData(n, d.vectorClock).then(_ => null).catch(e => console.error(e));
}).then(result => console.debug(`Completed self-disk migration for ${n}`)).catch(error => console.error(error));
});
}).catch(error => console.error(error));
};
module.exports = startNode;

114
src/server.js Normal file
View File

@ -0,0 +1,114 @@
const http = require('http');
const WebSocket = require('ws');
const url = require('url');
const intra = require('./backup/intra');
const client = require('./client');
const { INVALID, INVALID_E_CODE, INTERNAL_E_CODE } = require('./_constants');
module.exports = function Server(port) {
var refresher; //container for refresher
const server = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
if (req.method === "GET") {
//GET: requesting data
let u = url.parse(req.url, true);
if (!u.search)
return res.end("");
console.debug("GET:", u.search);
client.processRequestFromUser(u.query)
.then(result => res.end(JSON.stringify(result[0])))
.catch(error => {
if (error instanceof INVALID)
res.writeHead(INVALID_E_CODE).end(error.message);
else {
console.error(error);
res.writeHead(INTERNAL_E_CODE).end("Unable to process request");
}
});
} else if (req.method === "POST") {
//POST: All data processing (required JSON input)
let data = '';
req.on('data', chunk => data += chunk);
req.on('end', () => {
console.debug("POST:", data);
//process the all data request types
client.processIncomingData(data).then(result => {
res.end(JSON.stringify(result[0]));
if (result[1]) {
refresher.countdown;
if (['DATA', 'TAG', 'NOTE'].includes(result[1]))
sendToLiveRequests(result[0]);
intra.forwardToNextNode(result[2], result[0]);
};
}).catch(error => {
if (error instanceof INVALID)
res.writeHead(INVALID_E_CODE).end(error.message);
else {
console.error(error);
res.writeHead(INTERNAL_E_CODE).end("Unable to process request");
}
});
});
};
});
server.listen(port, (err) => {
if (!err)
console.log(`Server running at port ${port}`);
});
const wsServer = new WebSocket.Server({
server
});
wsServer.on('connection', function connection(ws) {
ws.onmessage = function (evt) {
let message = evt.data;
if (message.startsWith(intra.SUPERNODE_INDICATOR))
intra.processTaskFromSupernode(message, ws);
else {
console.debug("WS: ", message);
try {
var request = JSON.parse(message);
if (request.status !== undefined)
client.processStatusFromUser(request, ws);
else
client.processRequestFromUser(request).then(result => {
ws.send(JSON.stringify(result[0]));
ws._liveReq = request;
}).catch(error => {
if (floGlobals.sn_config.errorFeedback) {
if (error instanceof INVALID)
ws.send(`${INVALID_E_CODE}: ${error.message}`);
else {
console.error(error);
ws.send(`${INTERNAL_E_CODE}: Unable to process request`);
}
}
});
} catch (error) {
//console.error(error);
if (floGlobals.sn_config.errorFeedback)
ws.send(`${INVALID_E_CODE}: Request not in JSON format`);
};
};
};
});
function sendToLiveRequests(data) {
wsServer.clients.forEach(ws => {
if (client.checkIfRequestSatisfy(ws._liveReq, data))
ws.send(JSON.stringify(data));
});
};
Object.defineProperty(this, "http", {
get: () => server
});
Object.defineProperty(this, "webSocket", {
get: () => wsServer
});
Object.defineProperty(this, "refresher", {
set: (r) => refresher = r
});
};

17
src/set_globals.js Normal file
View File

@ -0,0 +1,17 @@
'use strict';
//fetch for node js (used in floBlockchainAPI.js)
global.fetch = require("node-fetch");
//Set browser paramaters from param.json (or param-default.json)
var param;
try {
param = require('../args/param.json');
} catch {
param = require('../args/param-default.json');
} finally {
for (let p in param)
global[p] = param[p];
}
if (!process.argv.includes("--debug"))
global.console.debug = () => null;

2
start.js Normal file
View File

@ -0,0 +1,2 @@
const start = require('./src/main');
start();

View File

@ -1,19 +0,0 @@
#!/bin/sh
current_date=$(date)
echo "$current_date : Starting Supernode" >> logs/app.log
#Read configurations
IFS="="
while read -r var value
do
export "$var"="${value}"
done < .config
#Start the app
echo $current_date >> logs/server.log
app/supernodeWSS.bin $PORT $SERVER_PWD >> logs/server.log &
echo $current_date >> logs/browser.log
$BROWSER app/index.html >> logs/browser.log &
wait

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,177 +0,0 @@
/*
* Copyright (c) 2014 Cesanta Software Limited
* All rights reserved
*/
#include "mongoose.h"
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 *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;
}
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);
}
//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;
mg_send_websocket_frame(c, WEBSOCKET_OP_TEXT, msg.p, (int)msg.len);
}
}
//Broadcast incoming message [sn_c => all]
static void broadcast(const struct mg_str msg) {
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;
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("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;
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("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;
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("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("WARNING: No supernode connected\n");
}
static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
switch (ev) {
case MG_EV_WEBSOCKET_HANDSHAKE_DONE: {
/*New Websocket Connection*/
sys_display(nc, "+Connected+");
if (sn_c != NULL)
sys_unicast(nc, mg_mk_str("$+"));
else
sys_unicast(nc, mg_mk_str("$-"));
break;
}
case MG_EV_WEBSOCKET_FRAME: {
/* New Websocket Message*/
struct websocket_message *wm = (struct websocket_message *) ev_data;
struct mg_str d = {(char *) wm->data, wm->size};
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);
}
break;
}
case MG_EV_HTTP_REQUEST: {
mg_serve_http(nc, (struct http_message *) ev_data, s_http_server_opts);
break;
}
case MG_EV_CLOSE: {
/* 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;
}
}
}
int main(int argc, char** argv) {
s_http_port = argv[1];
server_pwd = argv[2];
struct mg_mgr mgr;
struct mg_connection *nc;
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
setvbuf(stdout, NULL, _IOLBF, 0);
setvbuf(stderr, NULL, _IOLBF, 0);
mg_mgr_init(&mgr, NULL);
nc = mg_bind(&mgr, s_http_port, ev_handler);
mg_set_protocol_http_websocket(nc);
s_http_server_opts.document_root = "app/"; // Serve current directory
s_http_server_opts.enable_directory_listing = "no";
printf("Started on port %s\n", s_http_port);
while (s_signal_received == 0) {
mg_mgr_poll(&mgr, 200);
}
mg_mgr_free(&mgr);
return 0;
}