Tetraplets (#1)

This commit is contained in:
coder11 2020-12-23 17:24:22 +03:00 committed by GitHub
parent 2876417554
commit f6fd95ce77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 739 additions and 504 deletions

23
.eslintrc.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 12,
sourceType: 'module', // Allows for the use of imports
},
env: {
browser: true,
es2021: true,
},
extends: [
'airbnb-base',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
// Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
'plugin:prettier/recommended',
],
plugins: ['@typescript-eslint', 'prettier'],
rules: {},
settings: {
'import/extensions': ['.js', '.ts'],
},
};

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
bundle/
# Dependency directories
node_modules/
jspm_packages/

8
.prettierrc.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
semi: true,
trailingComma: "all",
singleQuote: true,
printWidth: 120,
tabWidth: 4,
useTabs: false
};

8
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "fluence", "name": "fluence",
"version": "0.7.101", "version": "0.7.102",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -25,9 +25,9 @@
} }
}, },
"@fluencelabs/aquamarine-stepper": { "@fluencelabs/aquamarine-stepper": {
"version": "0.0.21", "version": "0.0.27",
"resolved": "https://registry.npmjs.org/@fluencelabs/aquamarine-stepper/-/aquamarine-stepper-0.0.21.tgz", "resolved": "https://registry.npmjs.org/@fluencelabs/aquamarine-stepper/-/aquamarine-stepper-0.0.27.tgz",
"integrity": "sha512-bw0tdC5+fihzw+BxA02TrNIzMp2reuV21RqPMlDUExh2tbSzHYKBXKOxGsIY10j3QYWpHQZK9N341VnA3nw6Sw==" "integrity": "sha512-UT25immkpJ79/1cBunAr8owEuaLfrhy3njw3BLfonF316gCX6pFBihlivOsvQlvw8/cL5RJDwlkzBLYAf6Lexw=="
}, },
"@sinonjs/commons": { "@sinonjs/commons": {
"version": "1.7.2", "version": "1.7.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "fluence", "name": "fluence",
"version": "0.7.101", "version": "0.7.102",
"description": "the browser js-libp2p client for the Fluence network", "description": "the browser js-libp2p client for the Fluence network",
"main": "./dist/fluence.js", "main": "./dist/fluence.js",
"typings": "./dist/fluence.d.ts", "typings": "./dist/fluence.d.ts",
@ -16,7 +16,7 @@
"author": "Fluence Labs", "author": "Fluence Labs",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fluencelabs/aquamarine-stepper": "0.0.21", "@fluencelabs/aquamarine-stepper": "0.0.27",
"async": "3.2.0", "async": "3.2.0",
"base64-js": "1.3.1", "base64-js": "1.3.1",
"bs58": "4.0.1", "bs58": "4.0.1",

View File

@ -1,27 +1,27 @@
import {getCurrentParticleId, registerService} from "./globalState"; import { getCurrentParticleId, registerService } from './globalState';
import {ServiceMultiple} from "./service"; import { ServiceMultiple } from './service';
import log from "loglevel"; import log from 'loglevel';
let storage: Map<string, Map<string, any>> = new Map(); let storage: Map<string, Map<string, any>> = new Map();
export function addData(particleId: string, data: Map<string, any>, ttl: number) { export function addData(particleId: string, data: Map<string, any>, ttl: number) {
storage.set(particleId, data) storage.set(particleId, data);
setTimeout(() => { setTimeout(() => {
log.debug(`data for ${particleId} is deleted`) log.debug(`data for ${particleId} is deleted`);
storage.delete(particleId) storage.delete(particleId);
}, ttl) }, ttl);
} }
export const storageService = new ServiceMultiple("") export const storageService = new ServiceMultiple('');
storageService.registerFunction("load", (args: any[]) => { storageService.registerFunction('load', (args: any[]) => {
let current = getCurrentParticleId(); let current = getCurrentParticleId();
let data = storage.get(current) let data = storage.get(current);
if (data) { if (data) {
return data.get(args[0]) return data.get(args[0]);
} else { } else {
return {} return {};
} }
}) });
registerService(storageService); registerService(storageService);

View File

@ -14,17 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
import * as PeerId from "peer-id"; import * as PeerId from 'peer-id';
import Multiaddr from "multiaddr" import Multiaddr from 'multiaddr';
import {FluenceClient} from "./fluenceClient"; import { FluenceClient } from './fluenceClient';
import * as log from "loglevel"; import * as log from 'loglevel';
import {LogLevelDesc} from "loglevel"; import { LogLevelDesc } from 'loglevel';
import {parseAstClosure} from "./stepper"; import { parseAstClosure } from './stepper';
log.setLevel('info') log.setLevel('info');
export default class Fluence { export default class Fluence {
static setLogLevel(level: LogLevelDesc): void { static setLogLevel(level: LogLevelDesc): void {
log.setLevel(level); log.setLevel(level);
} }
@ -33,7 +32,7 @@ export default class Fluence {
* Generates new peer id with Ed25519 private key. * Generates new peer id with Ed25519 private key.
*/ */
static async generatePeerId(): Promise<PeerId> { static async generatePeerId(): Promise<PeerId> {
return await PeerId.create({keyType: "Ed25519"}); return await PeerId.create({ keyType: 'Ed25519' });
} }
/** /**
@ -43,10 +42,10 @@ export default class Fluence {
*/ */
static async local(peerId?: PeerId): Promise<FluenceClient> { static async local(peerId?: PeerId): Promise<FluenceClient> {
if (!peerId) { if (!peerId) {
peerId = await Fluence.generatePeerId() peerId = await Fluence.generatePeerId();
} }
let client = new FluenceClient(peerId); let client = new FluenceClient(peerId);
await client.instantiateInterpreter(); await client.instantiateInterpreter();
return client; return client;
@ -69,7 +68,7 @@ export default class Fluence {
/// NOTE & TODO: interpreter is instantiated every time, make it a lazy constant? /// NOTE & TODO: interpreter is instantiated every time, make it a lazy constant?
static async parseAIR(script: string): Promise<string> { static async parseAIR(script: string): Promise<string> {
let closure = await parseAstClosure(); let closure = await parseAstClosure();
return closure(script) return closure(script);
} }
} }
@ -79,6 +78,6 @@ declare global {
} }
} }
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
window.Fluence = Fluence; window.Fluence = Fluence;
} }

View File

@ -14,22 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
import { build, Particle } from './particle';
import { StepperOutcome } from './stepperOutcome';
import * as PeerId from 'peer-id';
import Multiaddr from 'multiaddr';
import { FluenceConnection } from './fluenceConnection';
import { Subscriptions } from './subscriptions';
import { enqueueParticle, getCurrentParticleId, popParticle, setCurrentParticleId } from './globalState';
import { instantiateInterpreter, InterpreterInvoke } from './stepper';
import log from 'loglevel';
import { waitService } from './helpers/waitService';
import { ModuleConfig } from './moduleConfig';
import {build, Particle} from "./particle"; const bs58 = require('bs58');
import {StepperOutcome} from "./stepperOutcome";
import * as PeerId from "peer-id";
import Multiaddr from "multiaddr"
import {FluenceConnection} from "./fluenceConnection";
import {Subscriptions} from "./subscriptions";
import {enqueueParticle, getCurrentParticleId, popParticle, setCurrentParticleId} from "./globalState";
import {instantiateInterpreter, InterpreterInvoke} from "./stepper";
import log from "loglevel";
import {waitService} from "./helpers/waitService";
import {ModuleConfig} from "./moduleConfig";
const bs58 = require('bs58') const INFO_LOG_LEVEL = 2;
const INFO_LOG_LEVEL = 2
export class FluenceClient { export class FluenceClient {
readonly selfPeerId: PeerId; readonly selfPeerId: PeerId;
@ -50,22 +49,21 @@ export class FluenceClient {
* Pass a particle to a interpreter and send a result to other services. * Pass a particle to a interpreter and send a result to other services.
*/ */
private async handleParticle(particle: Particle): Promise<void> { private async handleParticle(particle: Particle): Promise<void> {
// if a current particle is processing, add new particle to the queue // if a current particle is processing, add new particle to the queue
if (getCurrentParticleId() !== undefined && getCurrentParticleId() !== particle.id) { if (getCurrentParticleId() !== undefined && getCurrentParticleId() !== particle.id) {
enqueueParticle(particle); enqueueParticle(particle);
} else { } else {
if (this.interpreter === undefined) { if (this.interpreter === undefined) {
throw new Error("Undefined. Interpreter is not initialized. Use 'Fluence.connect' to create a client.") throw new Error("Undefined. Interpreter is not initialized. Use 'Fluence.connect' to create a client.");
} }
// start particle processing if queue is empty // start particle processing if queue is empty
try { try {
setCurrentParticleId(particle.id) setCurrentParticleId(particle.id);
// check if a particle is relevant // check if a particle is relevant
let now = Date.now(); let now = Date.now();
let actualTtl = particle.timestamp + particle.ttl - now; let actualTtl = particle.timestamp + particle.ttl - now;
if (actualTtl <= 0) { if (actualTtl <= 0) {
log.info(`Particle expired. Now: ${now}, ttl: ${particle.ttl}, ts: ${particle.timestamp}`) log.info(`Particle expired. Now: ${now}, ttl: ${particle.ttl}, ts: ${particle.timestamp}`);
} else { } else {
// if there is no subscription yet, previous data is empty // if there is no subscription yet, previous data is empty
let prevData = []; let prevData = [];
@ -73,35 +71,40 @@ export class FluenceClient {
if (prevParticle) { if (prevParticle) {
prevData = prevParticle.data; prevData = prevParticle.data;
// update a particle in a subscription // update a particle in a subscription
this.subscriptions.update(particle) this.subscriptions.update(particle);
} else { } else {
// set a particle with actual ttl // set a particle with actual ttl
this.subscriptions.subscribe(particle, actualTtl) this.subscriptions.subscribe(particle, actualTtl);
} }
let stepperOutcomeStr = this.interpreter(particle.init_peer_id, particle.script, JSON.stringify(prevData), JSON.stringify(particle.data)) let stepperOutcomeStr = this.interpreter(
particle.init_peer_id,
particle.script,
JSON.stringify(prevData),
JSON.stringify(particle.data),
);
let stepperOutcome: StepperOutcome = JSON.parse(stepperOutcomeStr); let stepperOutcome: StepperOutcome = JSON.parse(stepperOutcomeStr);
if (log.getLevel() <= INFO_LOG_LEVEL) { if (log.getLevel() <= INFO_LOG_LEVEL) {
log.info("inner interpreter outcome:"); log.info('inner interpreter outcome:');
let so = {...stepperOutcome} let so = { ...stepperOutcome };
try { try {
so.data = JSON.parse(Buffer.from(so.data).toString("utf8")); so.data = JSON.parse(Buffer.from(so.data).toString('utf8'));
log.info(so); log.info(so);
} catch (e) { } catch (e) {
log.info("cannot parse StepperOutcome data as JSON: ", e); log.info('cannot parse StepperOutcome data as JSON: ', e);
} }
} }
// update data after aquamarine execution // update data after aquamarine execution
let newParticle: Particle = {...particle}; let newParticle: Particle = { ...particle };
newParticle.data = stepperOutcome.data newParticle.data = stepperOutcome.data;
this.subscriptions.update(newParticle) this.subscriptions.update(newParticle);
// do nothing if there is no `next_peer_pks` or if client isn't connected to the network // do nothing if there is no `next_peer_pks` or if client isn't connected to the network
if (stepperOutcome.next_peer_pks.length > 0 && this.connection) { if (stepperOutcome.next_peer_pks.length > 0 && this.connection) {
await this.connection.sendParticle(newParticle).catch((reason) => { await this.connection.sendParticle(newParticle).catch((reason) => {
console.error(`Error on sending particle with id ${particle.id}: ${reason}`) console.error(`Error on sending particle with id ${particle.id}: ${reason}`);
}); });
} }
} }
@ -112,7 +115,7 @@ export class FluenceClient {
if (nextParticle) { if (nextParticle) {
// update current particle // update current particle
setCurrentParticleId(nextParticle.id); setCurrentParticleId(nextParticle.id);
await this.handleParticle(nextParticle) await this.handleParticle(nextParticle);
} else { } else {
// wait for a new call (do nothing) if there is no new particle in a queue // wait for a new call (do nothing) if there is no new particle in a queue
setCurrentParticleId(undefined); setCurrentParticleId(undefined);
@ -125,21 +128,20 @@ export class FluenceClient {
* Handle incoming particle from a relay. * Handle incoming particle from a relay.
*/ */
private handleExternalParticle(): (particle: Particle) => Promise<void> { private handleExternalParticle(): (particle: Particle) => Promise<void> {
let _this = this; let _this = this;
return async (particle: Particle) => { return async (particle: Particle) => {
let data = particle.data; let data = particle.data;
let error: any = data["protocol!error"] let error: any = data['protocol!error'];
if (error !== undefined) { if (error !== undefined) {
log.error("error in external particle: ") log.error('error in external particle: ');
log.error(error) log.error(error);
} else { } else {
log.info("handle external particle: ") log.info('handle external particle: ');
log.info(particle) log.info(particle);
await _this.handleParticle(particle); await _this.handleParticle(particle);
} }
} };
} }
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
@ -162,13 +164,13 @@ export class FluenceClient {
multiaddr = Multiaddr(multiaddr); multiaddr = Multiaddr(multiaddr);
if (!this.interpreter) { if (!this.interpreter) {
throw Error("you must call 'instantiateInterpreter' before 'connect'") throw Error("you must call 'instantiateInterpreter' before 'connect'");
} }
let nodePeerId = multiaddr.getPeerId(); let nodePeerId = multiaddr.getPeerId();
this.nodePeerIdStr = nodePeerId; this.nodePeerIdStr = nodePeerId;
if (!nodePeerId) { if (!nodePeerId) {
throw Error("'multiaddr' did not contain a valid peer id") throw Error("'multiaddr' did not contain a valid peer id");
} }
let firstConnection: boolean = true; let firstConnection: boolean = true;
@ -186,7 +188,7 @@ export class FluenceClient {
async sendParticle(particle: Particle): Promise<string> { async sendParticle(particle: Particle): Promise<string> {
await this.handleParticle(particle); await this.handleParticle(particle);
return particle.id return particle.id;
} }
async executeParticle(particle: Particle) { async executeParticle(particle: Particle) {
@ -194,21 +196,29 @@ export class FluenceClient {
} }
nodeIdentityCall(): string { nodeIdentityCall(): string {
return `(call "${this.nodePeerIdStr}" ("op" "identity") [] void[])` return `(call "${this.nodePeerIdStr}" ("op" "identity") [] void[])`;
} }
async requestResponse<T>(name: string, call: (nodeId: string) => string, returnValue: string, data: Map<string, any>, handleResponse: (args: any[]) => T, nodeId?: string, ttl?: number): Promise<T> { async requestResponse<T>(
name: string,
call: (nodeId: string) => string,
returnValue: string,
data: Map<string, any>,
handleResponse: (args: any[]) => T,
nodeId?: string,
ttl?: number,
): Promise<T> {
if (!ttl) { if (!ttl) {
ttl = 10000 ttl = 10000;
} }
if (!nodeId) { if (!nodeId) {
nodeId = this.nodePeerIdStr nodeId = this.nodePeerIdStr;
} }
let serviceCall = call(nodeId) let serviceCall = call(nodeId);
let namedPromise = waitService(name, handleResponse, ttl) let namedPromise = waitService(name, handleResponse, ttl);
let script = `(seq let script = `(seq
${this.nodeIdentityCall()} ${this.nodeIdentityCall()}
@ -220,18 +230,24 @@ export class FluenceClient {
(call "${this.selfPeerIdStr}" ("${namedPromise.name}" "") [${returnValue}] void[]) (call "${this.selfPeerIdStr}" ("${namedPromise.name}" "") [${returnValue}] void[])
) )
) )
` `;
let particle = await build(this.selfPeerId, script, data, ttl) let particle = await build(this.selfPeerId, script, data, ttl);
await this.sendParticle(particle); await this.sendParticle(particle);
return namedPromise.promise return namedPromise.promise;
} }
/** /**
* Send a script to add module to a relay. Waiting for a response from a relay. * Send a script to add module to a relay. Waiting for a response from a relay.
*/ */
async addModule(name: string, moduleBase64: string, config?: ModuleConfig, nodeId?: string, ttl?: number): Promise<void> { async addModule(
name: string,
moduleBase64: string,
config?: ModuleConfig,
nodeId?: string,
ttl?: number,
): Promise<void> {
if (!config) { if (!config) {
config = { config = {
name: name, name: name,
@ -239,122 +255,166 @@ export class FluenceClient {
logger_enabled: true, logger_enabled: true,
wasi: { wasi: {
envs: {}, envs: {},
preopened_files: ["/tmp"], preopened_files: ['/tmp'],
mapped_dirs: {}, mapped_dirs: {},
} },
} };
} }
let data = new Map() let data = new Map();
data.set("module_bytes", moduleBase64) data.set('module_bytes', moduleBase64);
data.set("module_config", config) data.set('module_config', config);
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "add_module") [module_bytes module_config] void[])` let call = (nodeId: string) => `(call "${nodeId}" ("dist" "add_module") [module_bytes module_config] void[])`;
return this.requestResponse("addModule", call, "", data, () => {}, nodeId, ttl) return this.requestResponse('addModule', call, '', data, () => {}, nodeId, ttl);
} }
/** /**
* Send a script to add module to a relay. Waiting for a response from a relay. * Send a script to add module to a relay. Waiting for a response from a relay.
*/ */
async addBlueprint(name: string, dependencies: string[], blueprintId?: string, nodeId?: string, ttl?: number): Promise<string> { async addBlueprint(
let returnValue = "blueprint_id"; name: string,
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "add_blueprint") [blueprint] ${returnValue})` dependencies: string[],
blueprintId?: string,
nodeId?: string,
ttl?: number,
): Promise<string> {
let returnValue = 'blueprint_id';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "add_blueprint") [blueprint] ${returnValue})`;
let data = new Map() let data = new Map();
data.set("blueprint", { name: name, dependencies: dependencies, id: blueprintId }) data.set('blueprint', { name: name, dependencies: dependencies, id: blueprintId });
return this.requestResponse("addBlueprint", call, returnValue, data, (args: any[]) => args[0] as string, nodeId, ttl) return this.requestResponse(
'addBlueprint',
call,
returnValue,
data,
(args: any[]) => args[0] as string,
nodeId,
ttl,
);
} }
/** /**
* Send a script to create a service to a relay. Waiting for a response from a relay. * Send a script to create a service to a relay. Waiting for a response from a relay.
*/ */
async createService(blueprintId: string, nodeId?: string, ttl?: number): Promise<string> { async createService(blueprintId: string, nodeId?: string, ttl?: number): Promise<string> {
let returnValue = "service_id"; let returnValue = 'service_id';
let call = (nodeId: string) => `(call "${nodeId}" ("srv" "create") [blueprint_id] ${returnValue})` let call = (nodeId: string) => `(call "${nodeId}" ("srv" "create") [blueprint_id] ${returnValue})`;
let data = new Map() let data = new Map();
data.set("blueprint_id", blueprintId) data.set('blueprint_id', blueprintId);
return this.requestResponse("createService", call, returnValue, data, (args: any[]) => args[0] as string, nodeId, ttl) return this.requestResponse(
'createService',
call,
returnValue,
data,
(args: any[]) => args[0] as string,
nodeId,
ttl,
);
} }
/** /**
* Get all available modules hosted on a connected relay. * Get all available modules hosted on a connected relay.
*/ */
async getAvailableModules(nodeId?: string, ttl?: number): Promise<string[]> { async getAvailableModules(nodeId?: string, ttl?: number): Promise<string[]> {
let returnValue = "modules"; let returnValue = 'modules';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "get_modules") [] ${returnValue})` let call = (nodeId: string) => `(call "${nodeId}" ("dist" "get_modules") [] ${returnValue})`;
return this.requestResponse("getAvailableModules", call, returnValue, new Map(), (args: any[]) => args[0] as string[], nodeId, ttl) return this.requestResponse(
'getAvailableModules',
call,
returnValue,
new Map(),
(args: any[]) => args[0] as string[],
nodeId,
ttl,
);
} }
/** /**
* Get all available blueprints hosted on a connected relay. * Get all available blueprints hosted on a connected relay.
*/ */
async getBlueprints(nodeId: string, ttl?: number): Promise<string[]> { async getBlueprints(nodeId: string, ttl?: number): Promise<string[]> {
let returnValue = "blueprints"; let returnValue = 'blueprints';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "get_blueprints") [] ${returnValue})` let call = (nodeId: string) => `(call "${nodeId}" ("dist" "get_blueprints") [] ${returnValue})`;
return this.requestResponse("getBlueprints", call, returnValue, new Map(), (args: any[]) => args[0] as string[], nodeId, ttl) return this.requestResponse(
'getBlueprints',
call,
returnValue,
new Map(),
(args: any[]) => args[0] as string[],
nodeId,
ttl,
);
} }
/** /**
* Add a provider to DHT network to neighborhood around a key. * Add a provider to DHT network to neighborhood around a key.
*/ */
async addProvider(key: Buffer, providerPeer: string, providerServiceId?: string, nodeId?: string, ttl?: number): Promise<void> { async addProvider(
let call = (nodeId: string) => `(call "${nodeId}" ("dht" "add_provider") [key provider] void[])` key: Buffer,
providerPeer: string,
providerServiceId?: string,
nodeId?: string,
ttl?: number,
): Promise<void> {
let call = (nodeId: string) => `(call "${nodeId}" ("dht" "add_provider") [key provider] void[])`;
key = bs58.encode(key) key = bs58.encode(key);
let provider = { let provider = {
peer: providerPeer, peer: providerPeer,
service_id: providerServiceId service_id: providerServiceId,
} };
let data = new Map() let data = new Map();
data.set("key", key) data.set('key', key);
data.set("provider", provider) data.set('provider', provider);
return this.requestResponse("addProvider", call, "", data, () => {}, nodeId, ttl) return this.requestResponse('addProvider', call, '', data, () => {}, nodeId, ttl);
} }
/** /**
* Get a provider from DHT network from neighborhood around a key.. * Get a provider from DHT network from neighborhood around a key..
*/ */
async getProviders(key: Buffer, nodeId?: string, ttl?: number): Promise<any> { async getProviders(key: Buffer, nodeId?: string, ttl?: number): Promise<any> {
key = bs58.encode(key) key = bs58.encode(key);
let returnValue = "providers" let returnValue = 'providers';
let call = (nodeId: string) => `(call "${nodeId}" ("dht" "get_providers") [key] providers[])` let call = (nodeId: string) => `(call "${nodeId}" ("dht" "get_providers") [key] providers[])`;
let data = new Map() let data = new Map();
data.set("key", key) data.set('key', key);
return this.requestResponse("getProviders", call, returnValue, data, (args) => args[0], nodeId, ttl) return this.requestResponse('getProviders', call, returnValue, data, (args) => args[0], nodeId, ttl);
} }
/** /**
* Get relays neighborhood * Get relays neighborhood
*/ */
async neighborhood(node: string, ttl?: number): Promise<string[]> { async neighborhood(node: string, ttl?: number): Promise<string[]> {
let returnValue = "neighborhood" let returnValue = 'neighborhood';
let call = (nodeId: string) => `(call "${nodeId}" ("dht" "neighborhood") [node] ${returnValue})` let call = (nodeId: string) => `(call "${nodeId}" ("dht" "neighborhood") [node] ${returnValue})`;
let data = new Map() let data = new Map();
data.set("node", node) data.set('node', node);
return this.requestResponse("neighborhood", call, returnValue, data, (args) => args[0] as string[], node, ttl) return this.requestResponse('neighborhood', call, returnValue, data, (args) => args[0] as string[], node, ttl);
} }
/** /**
* Call relays 'identity' method. It should return passed 'fields' * Call relays 'identity' method. It should return passed 'fields'
*/ */
async relayIdentity(fields: string[], data: Map<string, any>, nodeId?: string, ttl?: number): Promise<any> { async relayIdentity(fields: string[], data: Map<string, any>, nodeId?: string, ttl?: number): Promise<any> {
let returnValue = "id"; let returnValue = 'id';
let call = (nodeId: string) => `(call "${nodeId}" ("op" "identity") [${fields.join(" ")}] ${returnValue})` let call = (nodeId: string) => `(call "${nodeId}" ("op" "identity") [${fields.join(' ')}] ${returnValue})`;
return this.requestResponse("getIdentity", call, returnValue, data, (args: any[]) => args[0], nodeId, ttl) return this.requestResponse('getIdentity', call, returnValue, data, (args: any[]) => args[0], nodeId, ttl);
} }
} }

View File

@ -14,27 +14,26 @@
* limitations under the License. * limitations under the License.
*/ */
import Websockets from "libp2p-websockets"; import Websockets from 'libp2p-websockets';
import Mplex from "libp2p-mplex"; import Mplex from 'libp2p-mplex';
import SECIO from "libp2p-secio"; import SECIO from 'libp2p-secio';
import Peer from "libp2p"; import Peer from 'libp2p';
import {decode, encode} from "it-length-prefixed"; import { decode, encode } from 'it-length-prefixed';
import pipe from "it-pipe"; import pipe from 'it-pipe';
import Multiaddr from "multiaddr"; import Multiaddr from 'multiaddr';
import PeerId from "peer-id"; import PeerId from 'peer-id';
import * as log from 'loglevel'; import * as log from 'loglevel';
import {build, parseParticle, Particle, stringifyParticle} from "./particle"; import { build, parseParticle, Particle, stringifyParticle } from './particle';
export const PROTOCOL_NAME = '/fluence/faas/1.0.0'; export const PROTOCOL_NAME = '/fluence/faas/1.0.0';
enum Status { enum Status {
Initializing = "Initializing", Initializing = 'Initializing',
Connected = "Connected", Connected = 'Connected',
Disconnected = "Disconnected" Disconnected = 'Disconnected',
} }
export class FluenceConnection { export class FluenceConnection {
private readonly selfPeerId: PeerId; private readonly selfPeerId: PeerId;
private node: LibP2p; private node: LibP2p;
private readonly address: Multiaddr; private readonly address: Multiaddr;
@ -59,7 +58,7 @@ export class FluenceConnection {
transport: [Websockets], transport: [Websockets],
streamMuxer: [Mplex], streamMuxer: [Mplex],
connEncryption: [SECIO], connEncryption: [SECIO],
peerDiscovery: [] peerDiscovery: [],
}, },
}); });
@ -67,7 +66,7 @@ export class FluenceConnection {
} }
isConnected() { isConnected() {
return this.status === Status.Connected return this.status === Status.Connected;
} }
// connection status. If `Disconnected`, it cannot be reconnected // connection status. If `Disconnected`, it cannot be reconnected
@ -77,29 +76,25 @@ export class FluenceConnection {
if (this.status === Status.Initializing) { if (this.status === Status.Initializing) {
await this.node.start(); await this.node.start();
log.debug("dialing to the node with address: " + this.node.peerId.toB58String()); log.debug('dialing to the node with address: ' + this.node.peerId.toB58String());
await this.node.dial(this.address); await this.node.dial(this.address);
let _this = this; let _this = this;
this.node.handle([PROTOCOL_NAME], async ({connection, stream}) => { this.node.handle([PROTOCOL_NAME], async ({ connection, stream }) => {
pipe( pipe(stream.source, decode(), async function (source: AsyncIterable<string>) {
stream.source, for await (const msg of source) {
decode(), try {
async function (source: AsyncIterable<string>) { let particle = parseParticle(msg);
for await (const msg of source) { log.debug('Particle is received:');
try { log.debug(JSON.stringify(particle, undefined, 2));
let particle = parseParticle(msg); _this.handleCall(particle);
log.debug("Particle is received:"); } catch (e) {
log.debug(JSON.stringify(particle, undefined, 2)); log.error('error on handling a new incoming message: ' + e);
_this.handleCall(particle);
} catch(e) {
log.error("error on handling a new incoming message: " + e);
}
} }
} }
) });
}); });
this.status = Status.Connected; this.status = Status.Connected;
@ -110,7 +105,7 @@ export class FluenceConnection {
private checkConnectedOrThrow() { private checkConnectedOrThrow() {
if (this.status !== Status.Connected) { if (this.status !== Status.Connected) {
throw Error(`connection is in ${this.status} state`) throw Error(`connection is in ${this.status} state`);
} }
} }
@ -120,17 +115,20 @@ export class FluenceConnection {
} }
async buildParticle(script: string, data: Map<string, any>, ttl?: number): Promise<Particle> { async buildParticle(script: string, data: Map<string, any>, ttl?: number): Promise<Particle> {
return build(this.selfPeerId, script, data, ttl) return build(this.selfPeerId, script, data, ttl);
} }
async sendParticle(particle: Particle): Promise<void> { async sendParticle(particle: Particle): Promise<void> {
this.checkConnectedOrThrow(); this.checkConnectedOrThrow();
let particleStr = stringifyParticle(particle); let particleStr = stringifyParticle(particle);
log.debug("send particle: \n" + JSON.stringify(particle, undefined, 2)); log.debug('send particle: \n' + JSON.stringify(particle, undefined, 2));
// create outgoing substream // create outgoing substream
const conn = await this.node.dialProtocol(this.address, PROTOCOL_NAME) as {stream: Stream; protocol: string}; const conn = (await this.node.dialProtocol(this.address, PROTOCOL_NAME)) as {
stream: Stream;
protocol: string;
};
pipe( pipe(
[Buffer.from(particleStr, 'utf8')], [Buffer.from(particleStr, 'utf8')],

View File

@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import {Service} from "./service"; import { Service } from './service';
import {Particle} from "./particle"; import { Particle } from './particle';
// TODO put state with wasm file in each created FluenceClient // TODO put state with wasm file in each created FluenceClient
let services: Map<string, Service> = new Map(); let services: Map<string, Service> = new Map();
@ -39,13 +39,13 @@ export function popParticle(): Particle | undefined {
} }
export function registerService(service: Service) { export function registerService(service: Service) {
services.set(service.serviceId, service) services.set(service.serviceId, service);
} }
export function deleteService(serviceId: string): boolean { export function deleteService(serviceId: string): boolean {
return services.delete(serviceId) return services.delete(serviceId);
} }
export function getService(serviceId: string): Service | undefined { export function getService(serviceId: string): Service | undefined {
return services.get(serviceId) return services.get(serviceId);
} }

View File

@ -1,15 +1,15 @@
/** /**
* Creates service that will wait for a response from external peers. * Creates service that will wait for a response from external peers.
*/ */
import {genUUID} from "../particle"; import { genUUID } from '../particle';
import log from "loglevel"; import log from 'loglevel';
import {ServiceMultiple} from "../service"; import { ServiceMultiple } from '../service';
import {deleteService, registerService} from "../globalState"; import { deleteService, registerService } from '../globalState';
import {delay} from "../utils"; import { delay } from '../utils';
interface NamedPromise<T> { interface NamedPromise<T> {
promise: Promise<T>, promise: Promise<T>;
name: string name: string;
} }
/** /**
@ -19,28 +19,28 @@ interface NamedPromise<T> {
* @param ttl * @param ttl
*/ */
export function waitResult(ttl: number): NamedPromise<any[]> { export function waitResult(ttl: number): NamedPromise<any[]> {
return waitService(genUUID(), (args: any[]) => args, ttl) return waitService(genUUID(), (args: any[]) => args, ttl);
} }
export function waitService<T>(functionName: string, func: (args: any[]) => T, ttl: number): NamedPromise<T> { export function waitService<T>(functionName: string, func: (args: any[]) => T, ttl: number): NamedPromise<T> {
let serviceName = `${functionName}-${genUUID()}`; let serviceName = `${functionName}-${genUUID()}`;
log.info(`Create waiting service '${serviceName}'`) log.info(`Create waiting service '${serviceName}'`);
let service = new ServiceMultiple(serviceName) let service = new ServiceMultiple(serviceName);
registerService(service) registerService(service);
let promise: Promise<T> = new Promise(function (resolve) { let promise: Promise<T> = new Promise(function (resolve) {
service.registerFunction("", (args: any[]) => { service.registerFunction('', (args: any[]) => {
resolve(func(args)) resolve(func(args));
return {} return {};
}) });
}) });
let timeout = delay<T>(ttl, "Timeout on waiting " + serviceName) let timeout = delay<T>(ttl, 'Timeout on waiting ' + serviceName);
return { return {
name: serviceName, name: serviceName,
promise: Promise.race([promise, timeout]).finally(() => { promise: Promise.race([promise, timeout]).finally(() => {
deleteService(serviceName) deleteService(serviceName);
}) }),
} };
} }

View File

@ -15,15 +15,15 @@
*/ */
export interface ModuleConfig { export interface ModuleConfig {
name: string, name: string;
mem_pages_count?: number, mem_pages_count?: number;
logger_enabled?: boolean, logger_enabled?: boolean;
wasi?: Wasi, wasi?: Wasi;
mounted_binaries?: object mounted_binaries?: object;
} }
export interface Wasi { export interface Wasi {
envs?: object, envs?: object;
preopened_files?: string[], preopened_files?: string[];
mapped_dirs?: object, mapped_dirs?: object;
} }

View File

@ -15,31 +15,31 @@
*/ */
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import PeerId from "peer-id"; import PeerId from 'peer-id';
import {encode} from "bs58"; import { encode } from 'bs58';
import {addData} from "./dataStorage"; import { addData } from './dataStorage';
const DEFAULT_TTL = 7000; const DEFAULT_TTL = 7000;
export interface Particle { export interface Particle {
id: string, id: string;
init_peer_id: string, init_peer_id: string;
timestamp: number, timestamp: number;
ttl: number, ttl: number;
script: string, script: string;
// sign upper fields // sign upper fields
signature: string, signature: string;
data: any data: any;
} }
function wrapScript(selfPeerId: string, script: string, fields: string[]): string { function wrapScript(selfPeerId: string, script: string, fields: string[]): string {
fields.forEach((v) => { fields.forEach((v) => {
script = ` script = `
(seq (seq
(call %init_peer_id% ("" "load") ["${v}"] ${v}) (call %init_peer_id% ("" "load") ["${v}"] ${v})
${script} ${script}
) )
` `;
}); });
return script; return script;
@ -47,14 +47,14 @@ function wrapScript(selfPeerId: string, script: string, fields: string[]): strin
export async function build(peerId: PeerId, script: string, data: Map<string, any>, ttl?: number): Promise<Particle> { export async function build(peerId: PeerId, script: string, data: Map<string, any>, ttl?: number): Promise<Particle> {
let id = genUUID(); let id = genUUID();
let currentTime = (new Date()).getTime(); let currentTime = new Date().getTime();
if (ttl === undefined) { if (ttl === undefined) {
ttl = DEFAULT_TTL ttl = DEFAULT_TTL;
} }
addData(id, data, ttl); addData(id, data, ttl);
script = wrapScript(peerId.toB58String(), script, Array.from(data.keys())) script = wrapScript(peerId.toB58String(), script, Array.from(data.keys()));
let particle: Particle = { let particle: Particle = {
id: id, id: id,
@ -62,9 +62,9 @@ export async function build(peerId: PeerId, script: string, data: Map<string, an
timestamp: currentTime, timestamp: currentTime,
ttl: ttl, ttl: ttl,
script: script, script: script,
signature: "", signature: '',
data: [] data: [],
} };
particle.signature = await signParticle(peerId, particle); particle.signature = await signParticle(peerId, particle);
@ -75,16 +75,15 @@ export async function build(peerId: PeerId, script: string, data: Map<string, an
* Copies a particle and stringify it. * Copies a particle and stringify it.
*/ */
export function stringifyParticle(call: Particle): string { export function stringifyParticle(call: Particle): string {
let obj: any = {...call}; let obj: any = { ...call };
obj.action = "Particle" obj.action = 'Particle';
// delete it after signatures will be implemented on nodes // delete it after signatures will be implemented on nodes
obj.signature = [] obj.signature = [];
return JSON.stringify(obj) return JSON.stringify(obj);
} }
export function parseParticle(str: string): Particle { export function parseParticle(str: string): Particle {
let json = JSON.parse(str); let json = JSON.parse(str);
@ -95,8 +94,8 @@ export function parseParticle(str: string): Particle {
ttl: json.ttl, ttl: json.ttl,
script: json.script, script: json.script,
signature: json.signature, signature: json.signature,
data: json.data data: json.data,
} };
} }
export function canonicalBytes(particle: Particle) { export function canonicalBytes(particle: Particle) {
@ -119,12 +118,11 @@ export function canonicalBytes(particle: Particle) {
/** /**
* Sign a particle with a private key from peerId. * Sign a particle with a private key from peerId.
*/ */
export async function signParticle(peerId: PeerId, export async function signParticle(peerId: PeerId, particle: Particle): Promise<string> {
particle: Particle): Promise<string> {
let bufToSign = canonicalBytes(particle); let bufToSign = canonicalBytes(particle);
let signature = await peerId.privKey.sign(bufToSign) let signature = await peerId.privKey.sign(bufToSign);
return encode(signature) return encode(signature);
} }
export function genUUID() { export function genUUID() {

25
src/securityTetraplet.ts Normal file
View File

@ -0,0 +1,25 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface ResolvedTriplet {
peer_pk: string;
service_id: string;
function_name: string;
}
export interface SecurityTetraplet extends ResolvedTriplet {
json_path: string;
}

View File

@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import * as PeerId from "peer-id"; import * as PeerId from 'peer-id';
import {decode, encode} from "bs58" import { decode, encode } from 'bs58';
import {keys} from "libp2p-crypto"; import { keys } from 'libp2p-crypto';
/** /**
* @param seed 32 bytes * @param seed 32 bytes
@ -24,11 +24,11 @@ import {keys} from "libp2p-crypto";
export async function seedToPeerId(seed: string): Promise<PeerId> { export async function seedToPeerId(seed: string): Promise<PeerId> {
let seedArr = decode(seed); let seedArr = decode(seed);
let privateK = await keys.generateKeyPairFromSeed("Ed25519", Uint8Array.from(seedArr), 256); let privateK = await keys.generateKeyPairFromSeed('Ed25519', Uint8Array.from(seedArr), 256);
return await PeerId.createFromPrivKey(privateK.bytes); return await PeerId.createFromPrivKey(privateK.bytes);
} }
export function peerIdToSeed(peerId: PeerId): string { export function peerIdToSeed(peerId: PeerId): string {
let seedBuf = peerId.privKey.marshal().subarray(0, 32) let seedBuf = peerId.privKey.marshal().subarray(0, 32);
return encode(seedBuf) return encode(seedBuf);
} }

View File

@ -14,111 +14,145 @@
* limitations under the License. * limitations under the License.
*/ */
import {getService} from "./globalState"; import { getService } from './globalState';
import { SecurityTetraplet } from './securityTetraplet';
export interface CallServiceResult { export interface CallServiceResult {
ret_code: number, ret_code: number;
result: string result: string;
} }
export abstract class Service { export abstract class Service {
serviceId: string; serviceId: string;
abstract call(fnName: string, args: any[]): CallServiceResult
/**
* Calls the function from local client
* @param fnName - name of the function to call
* @param args - arguments to be passed to the function
* @param tetraplets - array of arrays of tetraplets. First index corresponds to argument number.
* If the argument is not an array the second array will always contain exactly one element.
* If the argument is an array the second index will correspond to the index of element in argument's array
*/
abstract call(fnName: string, args: any[], tetraplets: SecurityTetraplet[][]): CallServiceResult;
} }
/** /**
* Creates one function for all function names. * Creates one function for all function names.
*/ */
export class ServiceOne implements Service { export class ServiceOne implements Service {
serviceId: string; serviceId: string;
fn: (fnName: string, args: any[]) => object fn: (fnName: string, args: any[], tetraplets: SecurityTetraplet[][]) => object;
constructor(serviceId: string, fn: (fnName: string, args: any[]) => object) { constructor(serviceId: string, fn: (fnName: string, args: any[], tetraplets: SecurityTetraplet[][]) => object) {
this.serviceId = serviceId; this.serviceId = serviceId;
this.fn = fn; this.fn = fn;
} }
call(fnName: string, args: any[]): CallServiceResult { /**
* Calls the function from local client
* @param fnName - name of the function to call
* @param args - arguments to be passed to the function
* @param tetraplets - array of arrays of tetraplets. First index corresponds to argument number.
* If the argument is not an array the second array will always contain exactly one element.
* If the argument is an array the second index will correspond to the index of element in argument's array
*/
call(fnName: string, args: any[], tetraplets: SecurityTetraplet[][]): CallServiceResult {
try { try {
let result = this.fn(fnName, args) let result = this.fn(fnName, args, tetraplets);
return { return {
ret_code: 0, ret_code: 0,
result: JSON.stringify(result) result: JSON.stringify(result),
} };
} catch (err) { } catch (err) {
return { return {
ret_code: 1, ret_code: 1,
result: JSON.stringify(err) result: JSON.stringify(err),
} };
} }
} }
} }
/** /**
* Creates function per function name. Returns an error when call a name without registered function. * Creates function per function name. Returns an error when call a name without registered function.
*/ */
export class ServiceMultiple implements Service { export class ServiceMultiple implements Service {
serviceId: string; serviceId: string;
functions: Map<string, (args: any[]) => object> = new Map(); functions: Map<string, (args: any[], tetraplets: SecurityTetraplet[][]) => object> = new Map();
constructor(serviceId: string) { constructor(serviceId: string) {
this.serviceId = serviceId; this.serviceId = serviceId;
} }
registerFunction(fnName: string, fn: (args: any[]) => object) { /**
* Registers a callback function into Aquamarine
* @param fnName - the function name to be registered
* @param fn - callback function which will be called from Aquamarine.
* The callback function has the following parameters:
* args - arguments to be passed to the function
* tetraplets - array of arrays of tetraplets. First index corresponds to argument number.
* If the argument is not an array the second array will always contain exactly one element.
* If the argument is an array the second index will correspond to the index of element in argument's array
*/
registerFunction(fnName: string, fn: (args: any[], tetraplets: SecurityTetraplet[][]) => object) {
this.functions.set(fnName, fn); this.functions.set(fnName, fn);
} }
call(fnName: string, args: any[]): CallServiceResult { /**
let fn = this.functions.get(fnName) * Calls the function from local client
* @param fnName - name of the function to call
* @param args - arguments to be passed to the function
* @param tetraplets - array of arrays of tetraplets. First index corresponds to argument number.
* If the argument is not an array the second array will always contain exactly one element.
* If the argument is an array the second index will correspond to the index of element in argument's array
*/
call(fnName: string, args: any[], tetraplets: SecurityTetraplet[][]): CallServiceResult {
let fn = this.functions.get(fnName);
if (fn) { if (fn) {
try { try {
let result = fn(args) let result = fn(args, tetraplets);
return { return {
ret_code: 0, ret_code: 0,
result: JSON.stringify(result) result: JSON.stringify(result),
} };
} catch (err) { } catch (err) {
return { return {
ret_code: 1, ret_code: 1,
result: JSON.stringify(err) result: JSON.stringify(err),
} };
} }
} else { } else {
let errorMsg = `Error. There is no function ${fnName}` let errorMsg = `Error. There is no function ${fnName}`;
return { return {
ret_code: 1, ret_code: 1,
result: JSON.stringify(errorMsg) result: JSON.stringify(errorMsg),
} };
} }
} }
} }
export function service(service_id: string, fn_name: string, args: string): CallServiceResult { export function service(service_id: string, fn_name: string, args: string, tetraplets: string): CallServiceResult {
try { try {
let argsObject = JSON.parse(args) let argsObject = JSON.parse(args);
if (!Array.isArray(argsObject)) { if (!Array.isArray(argsObject)) {
throw new Error("args is not an array") throw new Error('args is not an array');
} }
let service = getService(service_id)
let tetrapletsObject: SecurityTetraplet[][] = JSON.parse(tetraplets);
let service = getService(service_id);
if (service) { if (service) {
return service.call(fn_name, argsObject) return service.call(fn_name, argsObject, tetrapletsObject);
} else { } else {
return { return {
result: JSON.stringify(`Error. There is no service: ${service_id}`), result: JSON.stringify(`Error. There is no service: ${service_id}`),
ret_code: 0 ret_code: 0,
} };
} }
} catch (err) { } catch (err) {
console.error("Cannot parse arguments: " + JSON.stringify(err)) console.error('Cannot parse arguments: ' + JSON.stringify(err));
return { return {
result: JSON.stringify("Cannot parse arguments: " + JSON.stringify(err)), result: JSON.stringify('Cannot parse arguments: ' + JSON.stringify(err)),
ret_code: 1 ret_code: 1,
} };
} }
} }

View File

@ -14,31 +14,33 @@
* limitations under the License. * limitations under the License.
*/ */
import {toByteArray} from "base64-js"; import { toByteArray } from 'base64-js';
import * as aqua from "./aqua" import * as aqua from './aqua';
import {return_current_peer_id, return_call_service_result, getStringFromWasm0, free} from "./aqua" import { return_current_peer_id, return_call_service_result, getStringFromWasm0, free } from './aqua';
import {service} from "./service"; import { service } from './service';
import PeerId from "peer-id"; import PeerId from 'peer-id';
import log from "loglevel"; import log from 'loglevel';
import {wasmBs64} from "@fluencelabs/aquamarine-stepper"; import { wasmBs64 } from '@fluencelabs/aquamarine-stepper';
import Instance = WebAssembly.Instance; import Instance = WebAssembly.Instance;
import Exports = WebAssembly.Exports; import Exports = WebAssembly.Exports;
import ExportValue = WebAssembly.ExportValue; import ExportValue = WebAssembly.ExportValue;
export type InterpreterInvoke = (init_user_id: string, script: string, prev_data: string, data: string) => string export type InterpreterInvoke = (init_user_id: string, script: string, prev_data: string, data: string) => string;
type ImportObject = { type ImportObject = {
"./aquamarine_client_bg.js": { './aquamarine_client_bg.js': {
__wbg_callserviceimpl_7d3cf77a2722659e: (arg0: any, arg1: any, arg2: any, arg3: any, arg4: any, arg5: any, arg6: any) => void; // fn call_service_impl(service_id: String, fn_name: String, args: String, security_tetraplets: String) -> String;
// prettier-ignore
__wbg_callserviceimpl_7d3cf77a2722659e: (arg0: any, arg1: any, arg2: any, arg3: any, arg4: any, arg5: any, arg6: any, arg7: any, arg8: any, ) => void;
__wbg_getcurrentpeeridimpl_154ce1848a306ff5: (arg0: any) => void; __wbg_getcurrentpeeridimpl_154ce1848a306ff5: (arg0: any) => void;
__wbindgen_throw: (arg: any) => void; __wbindgen_throw: (arg: any) => void;
}; };
host: LogImport host: LogImport;
}; };
type LogImport = { type LogImport = {
log_utf8_string: (level: any, target: any, offset: any, size: any) => void log_utf8_string: (level: any, target: any, offset: any, size: any) => void;
} };
class HostImportsConfig { class HostImportsConfig {
exports: Exports | undefined; exports: Exports | undefined;
@ -54,7 +56,7 @@ class HostImportsConfig {
} }
} }
const interpreter_wasm = toByteArray(wasmBs64) const interpreter_wasm = toByteArray(wasmBs64);
/// Instantiates WebAssembly runtime with AIR interpreter module /// Instantiates WebAssembly runtime with AIR interpreter module
async function interpreterInstance(cfg: HostImportsConfig): Promise<Instance> { async function interpreterInstance(cfg: HostImportsConfig): Promise<Instance> {
@ -77,10 +79,10 @@ async function interpreterInstance(cfg: HostImportsConfig): Promise<Instance> {
/// If export is a function, call it. Otherwise log a warning. /// If export is a function, call it. Otherwise log a warning.
/// NOTE: any here is unavoidable, see Function interface definition /// NOTE: any here is unavoidable, see Function interface definition
function call_export(f: ExportValue, ...argArray: any[]): any { function call_export(f: ExportValue, ...argArray: any[]): any {
if (typeof f === "function") { if (typeof f === 'function') {
return f(); return f();
} else { } else {
log.warn(`can't call export ${f}: it is not a function, but ${typeof f}`) log.warn(`can't call export ${f}: it is not a function, but ${typeof f}`);
} }
} }
@ -93,25 +95,25 @@ function log_import(cfg: HostImportsConfig): LogImport {
switch (level) { switch (level) {
case 1: case 1:
log.error(str) log.error(str);
break; break;
case 2: case 2:
log.warn(str) log.warn(str);
break; break;
case 3: case 3:
log.info(str) log.info(str);
break; break;
case 4: case 4:
log.debug(str) log.debug(str);
break; break;
case 5: case 5:
// we don't want a trace in trace logs // we don't want a trace in trace logs
log.debug(str) log.debug(str);
break; break;
} }
} finally { } finally {
} }
} },
}; };
} }
@ -120,20 +122,23 @@ function newImportObject(cfg: HostImportsConfig, peerId: PeerId): ImportObject {
return { return {
// __wbg_callserviceimpl_c0ca292e3c8c0c97 this is a function generated by bindgen. Could be changed. // __wbg_callserviceimpl_c0ca292e3c8c0c97 this is a function generated by bindgen. Could be changed.
// If so, an error with a new name will be occurred after wasm initialization. // If so, an error with a new name will be occurred after wasm initialization.
"./aquamarine_client_bg.js": { './aquamarine_client_bg.js': {
__wbg_callserviceimpl_7d3cf77a2722659e: (arg0: any, arg1: any, arg2: any, arg3: any, arg4: any, arg5: any, arg6: any) => { // prettier-ignore
__wbg_callserviceimpl_7d3cf77a2722659e: (arg0: any, arg1: any, arg2: any, arg3: any, arg4: any, arg5: any, arg6: any, arg7: any, arg8: any) => {
let wasm = cfg.exports; let wasm = cfg.exports;
try { try {
let serviceId = getStringFromWasm0(wasm, arg1, arg2) let serviceId = getStringFromWasm0(wasm, arg1, arg2);
let fnName = getStringFromWasm0(wasm, arg3, arg4) let fnName = getStringFromWasm0(wasm, arg3, arg4);
let args = getStringFromWasm0(wasm, arg5, arg6); let args = getStringFromWasm0(wasm, arg5, arg6);
let serviceResult = service(serviceId, fnName, args); let tetraplets = getStringFromWasm0(wasm, arg7, arg8);
let resultStr = JSON.stringify(serviceResult) let serviceResult = service(serviceId, fnName, args, tetraplets);
let resultStr = JSON.stringify(serviceResult);
return_call_service_result(wasm, resultStr, arg0); return_call_service_result(wasm, resultStr, arg0);
} finally { } finally {
free(wasm, arg1, arg2) free(wasm, arg1, arg2);
free(wasm, arg3, arg4) free(wasm, arg3, arg4);
free(wasm, arg5, arg6) free(wasm, arg5, arg6);
free(wasm, arg7, arg8);
} }
}, },
__wbg_getcurrentpeeridimpl_154ce1848a306ff5: (arg0: any) => { __wbg_getcurrentpeeridimpl_154ce1848a306ff5: (arg0: any) => {
@ -143,19 +148,19 @@ function newImportObject(cfg: HostImportsConfig, peerId: PeerId): ImportObject {
}, },
__wbindgen_throw: (arg: any) => { __wbindgen_throw: (arg: any) => {
console.log(`wbindgen throw: ${JSON.stringify(arg)}`); console.log(`wbindgen throw: ${JSON.stringify(arg)}`);
} },
}, },
host: log_import(cfg) host: log_import(cfg),
}; };
} }
function newLogImport(cfg: HostImportsConfig): ImportObject { function newLogImport(cfg: HostImportsConfig): ImportObject {
return { return {
host: log_import(cfg), host: log_import(cfg),
"./aquamarine_client_bg.js": { './aquamarine_client_bg.js': {
__wbg_callserviceimpl_7d3cf77a2722659e: _ => {}, __wbg_callserviceimpl_7d3cf77a2722659e: (_) => {},
__wbg_getcurrentpeeridimpl_154ce1848a306ff5: _ => {}, __wbg_getcurrentpeeridimpl_154ce1848a306ff5: (_) => {},
__wbindgen_throw: _ => {} __wbindgen_throw: (_) => {},
}, },
}; };
} }
@ -165,29 +170,28 @@ function newLogImport(cfg: HostImportsConfig): ImportObject {
export async function instantiateInterpreter(peerId: PeerId): Promise<InterpreterInvoke> { export async function instantiateInterpreter(peerId: PeerId): Promise<InterpreterInvoke> {
let cfg = new HostImportsConfig((cfg) => { let cfg = new HostImportsConfig((cfg) => {
return newImportObject(cfg, peerId); return newImportObject(cfg, peerId);
}) });
let instance = await interpreterInstance(cfg); let instance = await interpreterInstance(cfg);
return (init_user_id: string, script: string, prev_data: string, data: string) => { return (init_user_id: string, script: string, prev_data: string, data: string) => {
let logLevel = log.getLevel();
let logLevel = log.getLevel() let logLevelStr = 'info';
let logLevelStr = "info"
if (logLevel === 0) { if (logLevel === 0) {
logLevelStr = "trace" logLevelStr = 'trace';
} else if (logLevel === 1) { } else if (logLevel === 1) {
logLevelStr = "debug" logLevelStr = 'debug';
} else if (logLevel === 2) { } else if (logLevel === 2) {
logLevelStr = "info" logLevelStr = 'info';
} else if (logLevel === 3) { } else if (logLevel === 3) {
logLevelStr = "warn" logLevelStr = 'warn';
} else if (logLevel === 4) { } else if (logLevel === 4) {
logLevelStr = "error" logLevelStr = 'error';
} else if (logLevel === 5) { } else if (logLevel === 5) {
logLevelStr = "off" logLevelStr = 'off';
} }
return aqua.invoke(instance.exports, init_user_id, script, prev_data, data, logLevelStr) return aqua.invoke(instance.exports, init_user_id, script, prev_data, data, logLevelStr);
} };
} }
/// Instantiate AIR interpreter with host imports containing only logger, but not call_service /// Instantiate AIR interpreter with host imports containing only logger, but not call_service
@ -197,6 +201,6 @@ export async function parseAstClosure(): Promise<(script: string) => string> {
let instance = await interpreterInstance(cfg); let instance = await interpreterInstance(cfg);
return (script: string) => { return (script: string) => {
return aqua.ast(instance.exports, script) return aqua.ast(instance.exports, script);
}; };
} }

View File

@ -15,7 +15,7 @@
*/ */
export interface StepperOutcome { export interface StepperOutcome {
ret_code: number, ret_code: number;
data: number[], data: number[];
next_peer_pks: string[] next_peer_pks: string[];
} }

View File

@ -14,8 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import {Particle} from "./particle"; import { Particle } from './particle';
import log from "loglevel"; import log from 'loglevel';
export class Subscriptions { export class Subscriptions {
private subscriptions: Map<string, Particle> = new Map(); private subscriptions: Map<string, Particle> = new Map();
@ -31,9 +31,9 @@ export class Subscriptions {
subscribe(particle: Particle, ttl: number) { subscribe(particle: Particle, ttl: number) {
let _this = this; let _this = this;
setTimeout(() => { setTimeout(() => {
_this.subscriptions.delete(particle.id) _this.subscriptions.delete(particle.id);
log.info(`Particle with id ${particle.id} deleted by timeout`) log.info(`Particle with id ${particle.id} deleted by timeout`);
}, ttl) }, ttl);
this.subscriptions.set(particle.id, particle); this.subscriptions.set(particle.id, particle);
} }
@ -47,10 +47,10 @@ export class Subscriptions {
} }
get(id: string): Particle | undefined { get(id: string): Particle | undefined {
return this.subscriptions.get(id) return this.subscriptions.get(id);
} }
hasSubscription(particle: Particle): boolean { hasSubscription(particle: Particle): boolean {
return this.subscriptions.has(particle.id) return this.subscriptions.has(particle.id);
} }
} }

View File

@ -1,98 +1,133 @@
import 'mocha'; import 'mocha';
import Fluence from "../fluence"; import Fluence from '../fluence';
import {build} from "../particle"; import { build } from '../particle';
import {ServiceMultiple} from "../service"; import { ServiceMultiple } from '../service';
import {registerService} from "../globalState"; import { registerService } from '../globalState';
import {expect} from "chai"; import { expect } from 'chai';
import { SecurityTetraplet } from '../securityTetraplet';
function registerPromiseService<T>(serviceId: string, fnName: string, f: (args: any[]) => T): Promise<T> { function registerPromiseService<T>(
serviceId: string,
fnName: string,
f: (args: any[]) => T,
): Promise<[T, SecurityTetraplet[][]]> {
let service = new ServiceMultiple(serviceId); let service = new ServiceMultiple(serviceId);
registerService(service); registerService(service);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
service.registerFunction(fnName, (args: any[]) => { service.registerFunction(fnName, (args: any[], tetraplets: SecurityTetraplet[][]) => {
resolve(f(args)) resolve([f(args), tetraplets]);
return {result: f(args)} return { result: f(args) };
}) });
}) });
} }
describe("== AIR suite", () => { describe('== AIR suite', () => {
it('check init_peer_id', async function () {
it("check init_peer_id", async function () { let serviceId = 'init_peer';
let serviceId = "init_peer" let fnName = 'id';
let fnName = "id" let checkPromise = registerPromiseService(serviceId, fnName, (args) => args[0]);
let checkPromise = registerPromiseService(serviceId, fnName, (args) => args[0])
let client = await Fluence.local(); let client = await Fluence.local();
let script = `(call %init_peer_id% ("${serviceId}" "${fnName}") [%init_peer_id%])` let script = `(call %init_peer_id% ("${serviceId}" "${fnName}") [%init_peer_id%])`;
let particle = await build(client.selfPeerId, script, new Map()) let particle = await build(client.selfPeerId, script, new Map());
await client.executeParticle(particle); await client.executeParticle(particle);
expect(await checkPromise).to.be.equal(client.selfPeerIdStr) let args = (await checkPromise)[0];
}) expect(args).to.be.equal(client.selfPeerIdStr);
});
it("call local function", async function () { it('call local function', async function () {
let serviceId = "console" let serviceId = 'console';
let fnName = "log" let fnName = 'log';
let checkPromise = registerPromiseService(serviceId, fnName, (args) => args[0]) let checkPromise = registerPromiseService(serviceId, fnName, (args) => args[0]);
let client = await Fluence.local(); let client = await Fluence.local();
let arg = "hello" let arg = 'hello';
let script = `(call %init_peer_id% ("${serviceId}" "${fnName}") ["${arg}"])` let script = `(call %init_peer_id% ("${serviceId}" "${fnName}") ["${arg}"])`;
// Wrap script into particle, so it can be executed by local WASM runtime // Wrap script into particle, so it can be executed by local WASM runtime
let particle = await build(client.selfPeerId, script, new Map()) let particle = await build(client.selfPeerId, script, new Map());
await client.executeParticle(particle); await client.executeParticle(particle);
expect(await checkPromise).to.be.equal(arg) let [args, tetraplets] = await checkPromise;
}) expect(args).to.be.equal(arg);
});
it("check particle arguments", async function () { it('check particle arguments', async function () {
let serviceId = "check" let serviceId = 'check';
let fnName = "args" let fnName = 'args';
let checkPromise = registerPromiseService(serviceId, fnName, (args) => args[0]) let checkPromise = registerPromiseService(serviceId, fnName, (args) => args[0]);
let client = await Fluence.local(); let client = await Fluence.local();
let arg = "arg1" let arg = 'arg1';
let value = "hello" let value = 'hello';
let script = `(call %init_peer_id% ("${serviceId}" "${fnName}") [${arg}])` let script = `(call %init_peer_id% ("${serviceId}" "${fnName}") [${arg}])`;
let data = new Map();
let data = new Map() data.set('arg1', value);
data.set("arg1", value) let particle = await build(client.selfPeerId, script, data);
let particle = await build(client.selfPeerId, script, data)
await client.executeParticle(particle); await client.executeParticle(particle);
expect(await checkPromise).to.be.equal(value) let [args, tetraplets] = await checkPromise;
}) expect(args).to.be.equal(value);
});
it("check chain of services work properly", async function () { it('check security tetraplet', async function () {
let makeDataPromise = registerPromiseService('make_data_service', 'make_data', (args) => {
field: 42;
});
let getDataPromise = registerPromiseService('get_data_service', 'get_data', (args) => args[0]);
let client = await Fluence.local();
let script = `
(seq
(call %init_peer_id% ("make_data_service" "make_data") [] result)
(call %init_peer_id% ("get_data_service" "get_data") [result.$.field])
)`;
let particle = await build(client.selfPeerId, script, new Map());
await client.executeParticle(particle);
await makeDataPromise;
let [args, tetraplets] = await getDataPromise;
let tetraplet = tetraplets[0][0];
expect(tetraplet).to.contain({
service_id: 'make_data_service',
function_name: 'make_data',
json_path: '$.field',
});
});
it('check chain of services work properly', async function () {
this.timeout(5000); this.timeout(5000);
let serviceId1 = "check1" let serviceId1 = 'check1';
let fnName1 = "fn1" let fnName1 = 'fn1';
let checkPromise1 = registerPromiseService(serviceId1, fnName1, (args) => args[0]) let checkPromise1 = registerPromiseService(serviceId1, fnName1, (args) => args[0]);
let serviceId2 = "check2" let serviceId2 = 'check2';
let fnName2 = "fn2" let fnName2 = 'fn2';
let checkPromise2 = registerPromiseService(serviceId2, fnName2, (args) => args[0]) let checkPromise2 = registerPromiseService(serviceId2, fnName2, (args) => args[0]);
let serviceId3 = "check3" let serviceId3 = 'check3';
let fnName3 = "fn3" let fnName3 = 'fn3';
let checkPromise3 = registerPromiseService(serviceId3, fnName3, (args) => args) let checkPromise3 = registerPromiseService(serviceId3, fnName3, (args) => args);
let client = await Fluence.local(); let client = await Fluence.local();
let arg1 = "arg1" let arg1 = 'arg1';
let arg2 = "arg2" let arg2 = 'arg2';
// language=Clojure // language=Clojure
let script = `(seq let script = `(seq
@ -100,16 +135,19 @@ describe("== AIR suite", () => {
(call %init_peer_id% ("${serviceId1}" "${fnName1}") ["${arg1}"] result1) (call %init_peer_id% ("${serviceId1}" "${fnName1}") ["${arg1}"] result1)
(call %init_peer_id% ("${serviceId2}" "${fnName2}") ["${arg2}"] result2)) (call %init_peer_id% ("${serviceId2}" "${fnName2}") ["${arg2}"] result2))
(call %init_peer_id% ("${serviceId3}" "${fnName3}") [result1 result2])) (call %init_peer_id% ("${serviceId3}" "${fnName3}") [result1 result2]))
` `;
let particle = await build(client.selfPeerId, script, new Map()) let particle = await build(client.selfPeerId, script, new Map());
await client.executeParticle(particle); await client.executeParticle(particle);
expect(await checkPromise1).to.be.equal(arg1) let args1 = (await checkPromise1)[0];
expect(await checkPromise2).to.be.equal(arg2) expect(args1).to.be.equal(arg1);
expect(await checkPromise3).to.be.deep.equal([{result: arg1}, {result: arg2}]) let args2 = (await checkPromise2)[0];
}) expect(args2).to.be.equal(arg2);
})
let args3 = (await checkPromise3)[0];
expect(args3).to.be.deep.equal([{ result: arg1 }, { result: arg2 }]);
});
});

View File

@ -1,13 +1,22 @@
import { expect } from 'chai';
import 'mocha'; import 'mocha';
import Fluence from "../fluence"; import Fluence from '../fluence';
describe("== AST parsing suite", () => { describe('== AST parsing suite', () => {
it("parse simple script and return ast", async function () { it('parse simple script and return ast', async function () {
let ast = await Fluence.parseAIR(` let ast = await Fluence.parseAIR(`
(call node ("service" "function") [1 2 3 arg] output) (call node ("service" "function") [1 2 3 arg] output)
`); `);
console.log(ast); ast = JSON.parse(ast);
})
})
expect(ast).to.deep.equal({
Call: {
peer_part: { PeerPk: { Variable: 'node' } },
function_part: { ServiceIdWithFuncName: [{ Literal: 'service' }, { Literal: 'function' }] },
args: [{ Variable: '1' }, { Variable: '2' }, { Variable: '3' }, { Variable: 'arg' }],
output: { Scalar: 'output' },
},
});
});
});

View File

@ -1,28 +1,28 @@
import {expect} from 'chai'; import { expect } from 'chai';
import 'mocha'; import 'mocha';
import {encode} from "bs58" import { encode } from 'bs58';
import Fluence from "../fluence"; import Fluence from '../fluence';
import {certificateFromString, certificateToString, issue} from "../trust/certificate"; import { certificateFromString, certificateToString, issue } from '../trust/certificate';
import {TrustGraph} from "../trust/trust_graph"; import { TrustGraph } from '../trust/trust_graph';
import {nodeRootCert} from "../trust/misc"; import { nodeRootCert } from '../trust/misc';
import {peerIdToSeed, seedToPeerId} from "../seed"; import { peerIdToSeed, seedToPeerId } from '../seed';
import {build} from "../particle"; import { build } from '../particle';
import {Service, ServiceOne} from "../service"; import { Service, ServiceOne } from '../service';
import {registerService} from "../globalState"; import { registerService } from '../globalState';
import {waitResult} from "../helpers/waitService"; import { waitResult } from '../helpers/waitService';
describe("Typescript usage suite", () => { describe('Typescript usage suite', () => {
it('should create private key from seed and back', async function () {
it("should create private key from seed and back", async function () { // prettier-ignore
let seed = [46, 188, 245, 171, 145, 73, 40, 24, 52, 233, 215, 163, 54, 26, 31, 221, 159, 179, 126, 106, 27, 199, 189, 194, 80, 133, 235, 42, 42, 247, 80, 201]; let seed = [46, 188, 245, 171, 145, 73, 40, 24, 52, 233, 215, 163, 54, 26, 31, 221, 159, 179, 126, 106, 27, 199, 189, 194, 80, 133, 235, 42, 42, 247, 80, 201];
let seedStr = encode(seed) let seedStr = encode(seed);
console.log("SEED STR: " + seedStr) console.log('SEED STR: ' + seedStr);
let pid = await seedToPeerId(seedStr) let pid = await seedToPeerId(seedStr);
expect(peerIdToSeed(pid)).to.be.equal(seedStr) expect(peerIdToSeed(pid)).to.be.equal(seedStr);
}) });
it("should serialize and deserialize certificate correctly", async function () { it('should serialize and deserialize certificate correctly', async function () {
let cert = `11 let cert = `11
1111 1111
5566Dn4ZXXbBK5LJdUsE7L3pG9qdAzdPY47adjzkhEx9 5566Dn4ZXXbBK5LJdUsE7L3pG9qdAzdPY47adjzkhEx9
@ -32,7 +32,7 @@ describe("Typescript usage suite", () => {
2EvoZAZaGjKWFVdr36F1jphQ5cW7eK3yM16mqEHwQyr7 2EvoZAZaGjKWFVdr36F1jphQ5cW7eK3yM16mqEHwQyr7
4UAJQWzB3nTchBtwARHAhsn7wjdYtqUHojps9xV6JkuLENV8KRiWM3BhQByx5KijumkaNjr7MhHjouLawmiN1A4d 4UAJQWzB3nTchBtwARHAhsn7wjdYtqUHojps9xV6JkuLENV8KRiWM3BhQByx5KijumkaNjr7MhHjouLawmiN1A4d
1590061123504 1590061123504
1589974723504` 1589974723504`;
let deser = await certificateFromString(cert); let deser = await certificateFromString(cert);
let ser = certificateToString(deser); let ser = certificateToString(deser);
@ -41,54 +41,63 @@ describe("Typescript usage suite", () => {
}); });
// delete `.skip` and run `npm run test` to check service's and certificate's api with Fluence nodes // delete `.skip` and run `npm run test` to check service's and certificate's api with Fluence nodes
it.skip("test certs", async function () { it.skip('test certs', async function () {
this.timeout(15000); this.timeout(15000);
await testCerts(); await testCerts();
}); });
it.skip("", async function () { it.skip('', async function () {
let pid = await Fluence.generatePeerId() let pid = await Fluence.generatePeerId();
let cl = await Fluence.connect("/ip4/138.197.177.2/tcp/9001/ws/p2p/12D3KooWEXNUbCXooUwHrHBbrmjsrpHXoEphPwbjQXEGyzbqKnE9", pid) let cl = await Fluence.connect(
'/ip4/138.197.177.2/tcp/9001/ws/p2p/12D3KooWEXNUbCXooUwHrHBbrmjsrpHXoEphPwbjQXEGyzbqKnE9',
pid,
);
let service = new ServiceOne("test", (fnName: string, args: any[]) => { let service = new ServiceOne('test', (fnName: string, args: any[]) => {
console.log("called: " + args) console.log('called: ' + args);
return {} return {};
}); });
registerService(service); registerService(service);
let namedPromise = waitResult(30000) let namedPromise = waitResult(30000);
let script = ` let script = `
(seq ( (seq (
(call ( "${pid.toB58String()}" ("test" "test") (a b c d) result)) (call ( "${pid.toB58String()}" ("test" "test") (a b c d) result))
(call ( "${pid.toB58String()}" ("${namedPromise.name}" "") (d c b a) void[])) (call ( "${pid.toB58String()}" ("${namedPromise.name}" "") (d c b a) void[]))
)) ))
` `;
let data: Map<string, any> = new Map(); let data: Map<string, any> = new Map();
data.set("a", "some a") data.set('a', 'some a');
data.set("b", "some b") data.set('b', 'some b');
data.set("c", "some c") data.set('c', 'some c');
data.set("d", "some d") data.set('d', 'some d');
let particle = await build(pid, script, data, 30000) let particle = await build(pid, script, data, 30000);
await cl.sendParticle(particle) await cl.sendParticle(particle);
let res = await namedPromise.promise let res = await namedPromise.promise;
expect(res).to.be.equal(["some d", "some c", "some b", "some a"]) expect(res).to.be.equal(['some d', 'some c', 'some b', 'some a']);
}) });
}); });
const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
export async function testCerts() { export async function testCerts() {
let key1 = await Fluence.generatePeerId(); let key1 = await Fluence.generatePeerId();
let key2 = await Fluence.generatePeerId(); let key2 = await Fluence.generatePeerId();
// connect to two different nodes // connect to two different nodes
let cl1 = await Fluence.connect("/dns4/134.209.186.43/tcp/9003/ws/p2p/12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", key1); let cl1 = await Fluence.connect(
let cl2 = await Fluence.connect("/ip4/134.209.186.43/tcp/9002/ws/p2p/12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", key2); '/dns4/134.209.186.43/tcp/9003/ws/p2p/12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb',
key1,
);
let cl2 = await Fluence.connect(
'/ip4/134.209.186.43/tcp/9002/ws/p2p/12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er',
key2,
);
let trustGraph1 = new TrustGraph(cl1); let trustGraph1 = new TrustGraph(cl1);
let trustGraph2 = new TrustGraph(cl2); let trustGraph2 = new TrustGraph(cl2);
@ -109,10 +118,10 @@ export async function testCerts() {
let certs = await trustGraph2.getCertificates(key2.toB58String()); let certs = await trustGraph2.getCertificates(key2.toB58String());
// root certificate could be different because nodes save trusts with bigger `expiresAt` date and less `issuedAt` date // root certificate could be different because nodes save trusts with bigger `expiresAt` date and less `issuedAt` date
expect(certs[0].chain[1].issuedFor.toB58String()).to.be.equal(extended.chain[1].issuedFor.toB58String()) expect(certs[0].chain[1].issuedFor.toB58String()).to.be.equal(extended.chain[1].issuedFor.toB58String());
expect(certs[0].chain[1].signature).to.be.equal(extended.chain[1].signature) expect(certs[0].chain[1].signature).to.be.equal(extended.chain[1].signature);
expect(certs[0].chain[1].expiresAt).to.be.equal(extended.chain[1].expiresAt) expect(certs[0].chain[1].expiresAt).to.be.equal(extended.chain[1].expiresAt);
expect(certs[0].chain[1].issuedAt).to.be.equal(extended.chain[1].issuedAt) expect(certs[0].chain[1].issuedAt).to.be.equal(extended.chain[1].issuedAt);
await cl1.disconnect(); await cl1.disconnect();
await cl2.disconnect(); await cl2.disconnect();

View File

@ -14,28 +14,28 @@
* limitations under the License. * limitations under the License.
*/ */
import {createTrust, Trust, trustFromString, trustToString} from "./trust"; import { createTrust, Trust, trustFromString, trustToString } from './trust';
import * as PeerId from "peer-id"; import * as PeerId from 'peer-id';
const FORMAT = "11"; const FORMAT = '11';
const VERSION = "1111"; const VERSION = '1111';
// TODO verify certificate // TODO verify certificate
// Chain of trusts started from self-signed root trust. // Chain of trusts started from self-signed root trust.
export interface Certificate { export interface Certificate {
chain: Trust[] chain: Trust[];
} }
export function certificateToString(cert: Certificate): string { export function certificateToString(cert: Certificate): string {
let certStr = cert.chain.map(t => trustToString(t)).join("\n"); let certStr = cert.chain.map((t) => trustToString(t)).join('\n');
return `${FORMAT}\n${VERSION}\n${certStr}` return `${FORMAT}\n${VERSION}\n${certStr}`;
} }
export async function certificateFromString(str: string): Promise<Certificate> { export async function certificateFromString(str: string): Promise<Certificate> {
let lines = str.split("\n"); let lines = str.split('\n');
// last line could be empty // last line could be empty
if (!lines[lines.length - 1]) { if (!lines[lines.length - 1]) {
lines.pop() lines.pop();
} }
// TODO do match different formats and versions // TODO do match different formats and versions
@ -44,27 +44,28 @@ export async function certificateFromString(str: string): Promise<Certificate> {
// every trust is 4 lines, certificate lines number without format and version should be divided by 4 // every trust is 4 lines, certificate lines number without format and version should be divided by 4
if ((lines.length - 2) % 4 !== 0) { if ((lines.length - 2) % 4 !== 0) {
throw Error("Incorrect format of the certificate:\n" + str); throw Error('Incorrect format of the certificate:\n' + str);
} }
let chain: Trust[] = []; let chain: Trust[] = [];
let i; let i;
for(i = 2; i < lines.length; i = i + 4) { for (i = 2; i < lines.length; i = i + 4) {
chain.push(await trustFromString(lines[i], lines[i+1], lines[i+2], lines[i+3])) chain.push(await trustFromString(lines[i], lines[i + 1], lines[i + 2], lines[i + 3]));
} }
return {chain}; return { chain };
} }
// Creates new certificate with root trust (self-signed public key) from a key pair. // Creates new certificate with root trust (self-signed public key) from a key pair.
export async function issueRoot(issuedBy: PeerId, export async function issueRoot(
forPk: PeerId, issuedBy: PeerId,
expiresAt: number, forPk: PeerId,
issuedAt: number, expiresAt: number,
issuedAt: number,
): Promise<Certificate> { ): Promise<Certificate> {
if (expiresAt < issuedAt) { if (expiresAt < issuedAt) {
throw Error("Expiration time should be greater then issued time.") throw Error('Expiration time should be greater then issued time.');
} }
let maxDate = new Date(158981172690500).getTime(); let maxDate = new Date(158981172690500).getTime();
@ -74,24 +75,26 @@ export async function issueRoot(issuedBy: PeerId,
let chain = [rootTrust, trust]; let chain = [rootTrust, trust];
return { return {
chain: chain chain: chain,
} };
} }
// Adds a new trust into chain of trust in certificate. // Adds a new trust into chain of trust in certificate.
export async function issue(issuedBy: PeerId, export async function issue(
forPk: PeerId, issuedBy: PeerId,
extendCert: Certificate, forPk: PeerId,
expiresAt: number, extendCert: Certificate,
issuedAt: number): Promise<Certificate> { expiresAt: number,
issuedAt: number,
): Promise<Certificate> {
if (expiresAt < issuedAt) { if (expiresAt < issuedAt) {
throw Error("Expiration time should be greater then issued time.") throw Error('Expiration time should be greater then issued time.');
} }
let lastTrust = extendCert.chain[extendCert.chain.length - 1]; let lastTrust = extendCert.chain[extendCert.chain.length - 1];
if (lastTrust.issuedFor !== issuedBy) { if (lastTrust.issuedFor !== issuedBy) {
throw Error("`issuedFor` should be equal to `issuedBy` in the last trust of the chain.") throw Error('`issuedFor` should be equal to `issuedBy` in the last trust of the chain.');
} }
let trust = await createTrust(forPk, issuedBy, expiresAt, issuedAt); let trust = await createTrust(forPk, issuedBy, expiresAt, issuedAt);
@ -99,6 +102,6 @@ export async function issue(issuedBy: PeerId,
chain.push(trust); chain.push(trust);
return { return {
chain: chain chain: chain,
} };
} }

View File

@ -14,17 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import * as PeerId from "peer-id"; import * as PeerId from 'peer-id';
import {keys} from "libp2p-crypto"; import { keys } from 'libp2p-crypto';
import {Certificate, issueRoot} from "./certificate"; import { Certificate, issueRoot } from './certificate';
/** /**
* Generate root certificate with one of the Fluence trusted key for one day. * Generate root certificate with one of the Fluence trusted key for one day.
*/ */
export async function nodeRootCert(issuedFor: PeerId): Promise<Certificate> { export async function nodeRootCert(issuedFor: PeerId): Promise<Certificate> {
// prettier-ignore
let seed = [46, 188, 245, 171, 145, 73, 40, 24, 52, 233, 215, 163, 54, 26, 31, 221, 159, 179, 126, 106, 27, 199, 189, 194, 80, 133, 235, 42, 42, 247, 80, 201]; let seed = [46, 188, 245, 171, 145, 73, 40, 24, 52, 233, 215, 163, 54, 26, 31, 221, 159, 179, 126, 106, 27, 199, 189, 194, 80, 133, 235, 42, 42, 247, 80, 201];
let privateK = await keys.generateKeyPairFromSeed("Ed25519", Uint8Array.from(seed), 256); let privateK = await keys.generateKeyPairFromSeed('Ed25519', Uint8Array.from(seed), 256);
let peerId = await PeerId.createFromPrivKey(privateK.bytes); let peerId = await PeerId.createFromPrivKey(privateK.bytes);
let issuedAt = new Date(); let issuedAt = new Date();

View File

@ -14,24 +14,29 @@
* limitations under the License. * limitations under the License.
*/ */
import * as PeerId from "peer-id"; import * as PeerId from 'peer-id';
import {decode, encode} from "bs58" import { decode, encode } from 'bs58';
import crypto from 'libp2p-crypto'; import crypto from 'libp2p-crypto';
const ed25519 = crypto.keys.supportedKeys.ed25519; const ed25519 = crypto.keys.supportedKeys.ed25519;
// One element in chain of trust in a certificate. // One element in chain of trust in a certificate.
export interface Trust { export interface Trust {
issuedFor: PeerId, issuedFor: PeerId;
expiresAt: number, expiresAt: number;
signature: string, signature: string;
issuedAt: number issuedAt: number;
} }
export function trustToString(trust: Trust): string { export function trustToString(trust: Trust): string {
return `${encode(trust.issuedFor.pubKey.marshal())}\n${trust.signature}\n${trust.expiresAt}\n${trust.issuedAt}` return `${encode(trust.issuedFor.pubKey.marshal())}\n${trust.signature}\n${trust.expiresAt}\n${trust.issuedAt}`;
} }
export async function trustFromString(issuedFor: string, signature: string, expiresAt: string, issuedAt: string): Promise<Trust> { export async function trustFromString(
issuedFor: string,
signature: string,
expiresAt: string,
issuedAt: string,
): Promise<Trust> {
let pubKey = ed25519.unmarshalEd25519PublicKey(decode(issuedFor)); let pubKey = ed25519.unmarshalEd25519PublicKey(decode(issuedFor));
let peerId = await PeerId.createFromPubKey(pubKey.bytes); let peerId = await PeerId.createFromPubKey(pubKey.bytes);
@ -39,11 +44,16 @@ export async function trustFromString(issuedFor: string, signature: string, expi
issuedFor: peerId, issuedFor: peerId,
signature: signature, signature: signature,
expiresAt: parseInt(expiresAt), expiresAt: parseInt(expiresAt),
issuedAt: parseInt(issuedAt) issuedAt: parseInt(issuedAt),
} };
} }
export async function createTrust(forPk: PeerId, issuedBy: PeerId, expiresAt: number, issuedAt: number): Promise<Trust> { export async function createTrust(
forPk: PeerId,
issuedBy: PeerId,
expiresAt: number,
issuedAt: number,
): Promise<Trust> {
let bytes = toSignMessage(forPk, expiresAt, issuedAt); let bytes = toSignMessage(forPk, expiresAt, issuedAt);
let signature = await issuedBy.privKey.sign(Buffer.from(bytes)); let signature = await issuedBy.privKey.sign(Buffer.from(bytes));
let signatureStr = encode(signature); let signatureStr = encode(signature);
@ -52,7 +62,7 @@ export async function createTrust(forPk: PeerId, issuedBy: PeerId, expiresAt: nu
issuedFor: forPk, issuedFor: forPk,
expiresAt: expiresAt, expiresAt: expiresAt,
signature: signatureStr, signature: signatureStr,
issuedAt: issuedAt issuedAt: issuedAt,
}; };
} }
@ -64,7 +74,7 @@ function toSignMessage(pk: PeerId, expiresAt: number, issuedAt: number): Uint8Ar
bytes.set(numToArray(expiresAt), 32); bytes.set(numToArray(expiresAt), 32);
bytes.set(numToArray(issuedAt), 40); bytes.set(numToArray(issuedAt), 40);
return bytes return bytes;
} }
function numToArray(n: number): number[] { function numToArray(n: number): number[] {
@ -72,7 +82,7 @@ function numToArray(n: number): number[] {
for (let index = 0; index < byteArray.length; index++) { for (let index = 0; index < byteArray.length; index++) {
let byte = n & 0xff; let byte = n & 0xff;
byteArray [index] = byte; byteArray[index] = byte;
n = (n - byte) / 256; n = (n - byte) / 256;
} }

View File

@ -14,14 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import {FluenceClient} from "../fluenceClient"; import { FluenceClient } from '../fluenceClient';
import {Certificate, certificateFromString, certificateToString} from "./certificate"; import { Certificate, certificateFromString, certificateToString } from './certificate';
import * as log from 'loglevel'; import * as log from 'loglevel';
// TODO update after 'aquamarine' implemented // TODO update after 'aquamarine' implemented
// The client to interact with the Fluence trust graph API // The client to interact with the Fluence trust graph API
export class TrustGraph { export class TrustGraph {
client: FluenceClient; client: FluenceClient;
constructor(client: FluenceClient) { constructor(client: FluenceClient) {
@ -42,11 +41,13 @@ export class TrustGraph {
let response: any = {}; let response: any = {};
if (response.reason) { if (response.reason) {
throw Error(response.reason) throw Error(response.reason);
} else if (response.status) { } else if (response.status) {
return response.status return response.status;
} else { } else {
throw Error(`Unexpected response: ${response}. Should be 'status' field for a success response or 'reason' field for an error.`) throw Error(
`Unexpected response: ${response}. Should be 'status' field for a success response or 'reason' field for an error.`,
);
} }
} }
@ -57,16 +58,16 @@ export class TrustGraph {
peer_id: peerId peer_id: peerId
});*/ });*/
let certificatesRaw = resp.certificates let certificatesRaw = resp.certificates;
if (!(certificatesRaw && Array.isArray(certificatesRaw))) { if (!(certificatesRaw && Array.isArray(certificatesRaw))) {
log.error(Array.isArray(certificatesRaw)) log.error(Array.isArray(certificatesRaw));
throw Error("Unexpected. Certificates should be presented in the response as an array.") throw Error('Unexpected. Certificates should be presented in the response as an array.');
} }
let certs = []; let certs = [];
for (let cert of certificatesRaw) { for (let cert of certificatesRaw) {
certs.push(await certificateFromString(cert)) certs.push(await certificateFromString(cert));
} }
return certs; return certs;