feat: Cleaning up technical debts (#295)

This commit is contained in:
Pavel
2023-04-03 21:52:40 +04:00
committed by GitHub
parent 00b62f1459
commit 0b2f12d8ac
94 changed files with 3459 additions and 2943 deletions

View File

@ -12,7 +12,7 @@
"scripts": {
"build": "tsc",
"compile-aqua": "fluence aqua -i ./aqua/ -o ./aqua",
"test": "node ./copy-worker-script-workaround.mjs && vitest run"
"test": "node ./copy-worker-script-workaround.mjs && vitest --threads false run"
},
"repository": "https://github.com/fluencelabs/fluence-js",
"author": "Fluence Labs",

View File

@ -0,0 +1,133 @@
/*
* Copyright 2023 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.
*/
import { ClientConfig, ConnectionState, IFluenceClient, PeerIdB58, RelayOptions } from '@fluencelabs/interfaces';
import { RelayConnection, RelayConnectionConfig } from '../connection/RelayConnection.js';
import { fromOpts, KeyPair } from '../keypair/index.js';
import { FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js';
import { relayOptionToMultiaddr } from '../util/libp2pUtils.js';
import { IAvmRunner, IMarineHost } from '../marine/interfaces.js';
import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js';
import { logger } from '../util/logger.js';
const log = logger('client');
const DEFAULT_TTL_MS = 7000;
const MAX_OUTBOUND_STREAMS = 1024;
const MAX_INBOUND_STREAMS = 1024;
export const makeClientPeerConfig = async (
relay: RelayOptions,
config: ClientConfig,
): Promise<{ peerConfig: PeerConfig; relayConfig: RelayConnectionConfig; keyPair: KeyPair }> => {
const opts = config?.keyPair || { type: 'Ed25519', source: 'random' };
const keyPair = await fromOpts(opts);
const relayAddress = relayOptionToMultiaddr(relay);
return {
peerConfig: {
debug: {
printParticleId: config?.debug?.printParticleId || false,
},
defaultTtlMs: config?.defaultTtlMs || DEFAULT_TTL_MS,
},
relayConfig: {
peerId: keyPair.getLibp2pPeerId(),
relayAddress: relayAddress,
dialTimeoutMs: config?.connectionOptions?.dialTimeoutMs,
maxInboundStreams: config?.connectionOptions?.maxInboundStreams || MAX_OUTBOUND_STREAMS,
maxOutboundStreams: config?.connectionOptions?.maxOutboundStreams || MAX_INBOUND_STREAMS,
},
keyPair: keyPair,
};
};
export class ClientPeer extends FluencePeer implements IFluenceClient {
private relayPeerId: PeerIdB58;
private relayConnection: RelayConnection;
constructor(
peerConfig: PeerConfig,
relayConfig: RelayConnectionConfig,
keyPair: KeyPair,
marine: IMarineHost,
avmRunner: IAvmRunner,
) {
const relayConnection = new RelayConnection(relayConfig);
super(peerConfig, keyPair, marine, new JsServiceHost(), avmRunner, relayConnection);
this.relayPeerId = relayConnection.getRelayPeerId();
this.relayConnection = relayConnection;
}
getPeerId(): string {
return this.keyPair.getPeerId();
}
getPeerSecretKey(): Uint8Array {
return this.keyPair.toEd25519PrivateKey();
}
connectionState: ConnectionState = 'disconnected';
connectionStateChangeHandler: (state: ConnectionState) => void = () => {};
getRelayPeerId(): string {
return this.relayPeerId;
}
onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState {
this.connectionStateChangeHandler = handler;
return this.connectionState;
}
private changeConnectionState(state: ConnectionState) {
this.connectionState = state;
this.connectionStateChangeHandler(state);
}
/**
* Connect to the Fluence network
*/
async connect(): Promise<void> {
return this.start();
}
// /**
// * Disconnect from the Fluence network
// */
async disconnect(): Promise<void> {
return this.stop();
}
async start(): Promise<void> {
log.trace('connecting to Fluence network');
this.changeConnectionState('connecting');
await super.start();
await this.relayConnection.start();
// TODO: check connection (`checkConnection` function) here
this.changeConnectionState('connected');
log.trace('connected');
}
async stop(): Promise<void> {
log.trace('disconnecting from Fluence network');
this.changeConnectionState('disconnecting');
await this.relayConnection.stop();
await super.stop();
this.changeConnectionState('disconnected');
log.trace('disconnected');
}
}

View File

@ -0,0 +1,187 @@
import { it, describe, expect } from 'vitest';
import { handleTimeout } from '../../particle/Particle.js';
import { doNothing } from '../../jsServiceHost/serviceUtils.js';
import { registerHandlersHelper, withClient } from '../../util/testUtils.js';
import { checkConnection } from '../checkConnection.js';
import { nodes, RELAY } from './connection.js';
import { CallServiceData } from '../../jsServiceHost/interfaces.js';
describe('FluenceClient usage test suite', () => {
it('should make a call through network', async () => {
await withClient(RELAY, {}, async (peer) => {
// arrange
const result = await new Promise<string[]>((resolve, reject) => {
const script = `
(xor
(seq
(call %init_peer_id% ("load" "relay") [] init_relay)
(seq
(call init_relay ("op" "identity") ["hello world!"] result)
(call %init_peer_id% ("callback" "callback") [result])
)
)
(seq
(call init_relay ("op" "identity") [])
(call %init_peer_id% ("callback" "error") [%last_error%])
)
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
load: {
relay: () => {
return peer.getRelayPeerId();
},
},
callback: {
callback: (args: any) => {
const [val] = args;
resolve(val);
},
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
expect(result).toBe('hello world!');
});
});
it('check connection should work', async function () {
await withClient(RELAY, {}, async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toEqual(true);
});
});
it('check connection should work with ttl', async function () {
await withClient(RELAY, {}, async (peer) => {
const isConnected = await checkConnection(peer, 10000);
expect(isConnected).toEqual(true);
});
});
it('two clients should work inside the same time javascript process', async () => {
await withClient(RELAY, {}, async (peer1) => {
await withClient(RELAY, {}, async (peer2) => {
const res = new Promise((resolve) => {
peer2.internals.regHandler.common('test', 'test', (req: CallServiceData) => {
resolve(req.args[0]);
return {
result: {},
retCode: 0,
};
});
});
const script = `
(seq
(call "${peer1.getRelayPeerId()}" ("op" "identity") [])
(call "${peer2.getPeerId()}" ("test" "test") ["test"])
)
`;
const particle = peer1.internals.createNewParticle(script);
if (particle instanceof Error) {
throw particle;
}
peer1.internals.initiateParticle(particle, doNothing);
expect(await res).toEqual('test');
});
});
});
describe('should make connection to network', () => {
it('address as string', async () => {
await withClient(nodes[0].multiaddr, {}, async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
});
});
it('address as node', async () => {
await withClient(nodes[0], {}, async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
});
});
it('With connection options: dialTimeout', async () => {
await withClient(RELAY, { connectionOptions: { dialTimeoutMs: 100000 } }, async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
});
});
it('With connection options: skipCheckConnection', async () => {
await withClient(RELAY, { connectionOptions: { skipCheckConnection: true } }, async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
});
});
it('With connection options: defaultTTL', async () => {
await withClient(RELAY, { defaultTtlMs: 1 }, async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeFalsy();
});
});
});
it.skip('Should throw correct error when the client tries to send a particle not to the relay', async () => {
await withClient(RELAY, {}, async (peer) => {
const promise = new Promise((resolve, reject) => {
const script = `
(xor
(call "incorrect_peer_id" ("any" "service") [])
(call %init_peer_id% ("callback" "error") [%last_error%])
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
callback: {
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, (stage) => {
if (stage.stage === 'sendingError') {
reject(stage.errorMessage);
}
});
});
await promise;
await expect(promise).rejects.toMatch(
'Particle is expected to be sent to only the single peer (relay which client is connected to)',
);
});
});
});

View File

@ -15,3 +15,5 @@ export const nodes = [
peerId: '12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
},
];
export const RELAY = nodes[0].multiaddr;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2021 Fluence Labs Limited
* Copyright 2023 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.
@ -13,39 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Buffer } from 'buffer';
import { CallServiceData, CallServiceResult, CallServiceResultType, ResultCodes } from '../interfaces/commonTypes.js';
import { FluencePeer } from './FluencePeer.js';
import { ParticleExecutionStage } from './Particle.js';
import { ClientPeer } from './ClientPeer.js';
import { logger } from '../util/logger.js';
import { WrapFnIntoServiceCall } from '../jsServiceHost/serviceUtils.js';
import { handleTimeout } from '../particle/Particle.js';
const log = logger('connection');
export const MakeServiceCall =
(fn: (args: any[]) => CallServiceResultType) =>
(req: CallServiceData): CallServiceResult => ({
retCode: ResultCodes.success,
result: fn(req.args),
});
export const handleTimeout = (fn: () => void) => (stage: ParticleExecutionStage) => {
if (stage.stage === 'expired') {
fn();
}
};
export const doNothing = (..._args: Array<unknown>) => undefined;
/**
* Checks the network connection by sending a ping-like request to relay node
* @param { FluenceClient } peer - The Fluence Client instance.
* @param { ClientPeer } peer - The Fluence Client instance.
*/
export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<boolean> => {
if (!peer.getStatus().isConnected) {
return false;
}
export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise<boolean> => {
const msg = Math.random().toString(36).substring(7);
const promise = new Promise<string>((resolve, reject) => {
@ -76,8 +56,8 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<
particle.id,
'load',
'relay',
MakeServiceCall(() => {
return peer.getStatus().relayPeerId;
WrapFnIntoServiceCall(() => {
return peer.getRelayPeerId();
}),
);
@ -85,7 +65,7 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<
particle.id,
'load',
'msg',
MakeServiceCall(() => {
WrapFnIntoServiceCall(() => {
return msg;
}),
);
@ -94,7 +74,7 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<
particle.id,
'callback',
'callback',
MakeServiceCall((args) => {
WrapFnIntoServiceCall((args) => {
const [val] = args;
setTimeout(() => {
resolve(val);
@ -107,7 +87,7 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<
particle.id,
'callback',
'error',
MakeServiceCall((args) => {
WrapFnIntoServiceCall((args) => {
const [error] = args;
setTimeout(() => {
reject(error);
@ -131,23 +111,7 @@ export const checkConnection = async (peer: FluencePeer, ttl?: number): Promise<
}
return true;
} catch (e) {
log.error('error on establishing connection. Relay: %s error: %j', e, peer.getStatus().relayPeerId);
log.error('error on establishing connection. Relay: %s error: %j', e, peer.getRelayPeerId());
return false;
}
};
export function jsonify(obj: unknown) {
return JSON.stringify(obj, null, 4);
}
export const isString = (x: unknown): x is string => {
return x !== null && typeof x === 'string';
};
export class ServiceError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, ServiceError.prototype);
}
}

View File

@ -1,13 +1,19 @@
import {
ArrowWithoutCallbacks,
FnConfig,
FunctionCallDef,
NonArrowType,
getArgumentTypes,
isReturnTypeVoid,
IFluenceClient,
CallAquaFunction,
} from '@fluencelabs/interfaces';
/*
* Copyright 2023 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.
*/
import { getArgumentTypes, isReturnTypeVoid, CallAquaFunctionType } from '@fluencelabs/interfaces';
import {
injectRelayService,
@ -34,7 +40,7 @@ const log = logger('aqua');
* @param args - args in the form of JSON where each key corresponds to the name of the argument
* @returns
*/
export const callAquaFunction: CallAquaFunction = ({ def, script, config, peer, args }) => {
export const callAquaFunction: CallAquaFunctionType = ({ def, script, config, peer, args }) => {
log.trace('calling aqua function %j', { def, script, config, args });
const argumentTypes = getArgumentTypes(def);

View File

@ -1,7 +1,22 @@
import { jsonify } from '../js-peer/utils.js';
/*
* Copyright 2023 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.
*/
import { jsonify } from '../util/utils.js';
import { match } from 'ts-pattern';
import type { ArrowType, ArrowWithoutCallbacks, NonArrowType } from '@fluencelabs/interfaces';
import { CallServiceData } from '../interfaces/commonTypes.js';
import { CallServiceData } from '../jsServiceHost/interfaces.js';
/**
* Convert value from its representation in aqua language to representation in typescript

View File

@ -1,11 +1,26 @@
import type { RegisterService } from '@fluencelabs/interfaces';
/*
* Copyright 2023 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.
*/
import type { RegisterServiceType } from '@fluencelabs/interfaces';
import { registerGlobalService, userHandlerService } from './services.js';
import { logger } from '../util/logger.js';
const log = logger('aqua');
export const registerService: RegisterService = ({ peer, def, serviceId, service }) => {
export const registerService: RegisterServiceType = ({ peer, def, serviceId, service }) => {
log.trace('registering aqua service %o', { def, serviceId, service });
// Checking for missing keys

View File

@ -1,18 +1,33 @@
/*
* Copyright 2023 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.
*/
import { SecurityTetraplet } from '@fluencelabs/avm';
import { match } from 'ts-pattern';
import { Particle } from '../js-peer/Particle.js';
import { CallServiceData, GenericCallServiceHandler, ResultCodes } from '../interfaces/commonTypes.js';
import { Particle } from '../particle/Particle.js';
import { aquaArgs2Ts, responseServiceValue2ts, returnType2Aqua, ts2aqua } from './conversions.js';
import {
IFluenceClient,
CallParams,
ArrowWithoutCallbacks,
FunctionCallConstants,
FunctionCallDef,
NonArrowType,
IFluenceInternalApi,
} from '@fluencelabs/interfaces';
import { CallServiceData, GenericCallServiceHandler, ResultCodes } from '../jsServiceHost/interfaces.js';
export interface ServiceDescription {
serviceId: string;
@ -23,7 +38,7 @@ export interface ServiceDescription {
/**
* Creates a service which injects relay's peer id into aqua space
*/
export const injectRelayService = (def: FunctionCallDef, peer: IFluenceClient) => {
export const injectRelayService = (def: FunctionCallDef, peer: IFluenceInternalApi) => {
return {
serviceId: def.names.getDataSrv,
fnName: def.names.relay,
@ -168,10 +183,14 @@ const extractCallParams = (req: CallServiceData, arrow: ArrowWithoutCallbacks):
return callParams;
};
export const registerParticleScopeService = (peer: IFluenceClient, particle: Particle, service: ServiceDescription) => {
export const registerParticleScopeService = (
peer: IFluenceInternalApi,
particle: Particle,
service: ServiceDescription,
) => {
peer.internals.regHandler.forParticle(particle.id, service.serviceId, service.fnName, service.handler);
};
export const registerGlobalService = (peer: IFluenceClient, service: ServiceDescription) => {
export const registerGlobalService = (peer: IFluenceInternalApi, service: ServiceDescription) => {
peer.internals.regHandler.common(service.serviceId, service.fnName, service.handler);
};

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import { PeerIdB58 } from '@fluencelabs/interfaces';
import { FluenceConnection, ParticleHandler } from '../interfaces/index.js';
import { pipe } from 'it-pipe';
import { encode, decode } from 'it-length-prefixed';
import type { PeerId } from '@libp2p/interface-peer-id';
@ -25,23 +24,28 @@ import { mplex } from '@libp2p/mplex';
import { webSockets } from '@libp2p/websockets';
import { all } from '@libp2p/websockets/filters';
import { multiaddr } from '@multiformats/multiaddr';
import type { MultiaddrInput, Multiaddr } from '@multiformats/multiaddr';
import type { Connection } from '@libp2p/interface-connection';
import type { Multiaddr } from '@multiformats/multiaddr';
import map from 'it-map';
import { fromString } from 'uint8arrays/from-string';
import { toString } from 'uint8arrays/to-string';
import { logger } from '../util/logger.js';
import { Subject } from 'rxjs';
import { throwIfHasNoPeerId } from '../util/libp2pUtils.js';
import { IConnection } from './interfaces.js';
import { IParticle } from '../particle/interfaces.js';
import { Particle, serializeToString } from '../particle/Particle.js';
import { IStartable } from '../util/commonTypes.js';
const log = logger('connection');
export const PROTOCOL_NAME = '/fluence/particle/2.0.0';
/**
* Options to configure fluence connection
* Options to configure fluence relay connection
*/
export interface FluenceConnectionOptions {
export interface RelayConnectionConfig {
/**
* Peer id of the Fluence Peer
*/
@ -50,32 +54,57 @@ export interface FluenceConnectionOptions {
/**
* Multiaddress of the relay to make connection to
*/
relayAddress: MultiaddrInput;
relayAddress: Multiaddr;
/**
* The dialing timeout in milliseconds
*/
dialTimeoutMs?: number;
/**
* The maximum number of inbound streams for the libp2p node.
* Default: 1024
*/
maxInboundStreams: number;
/**
* The maximum number of outbound streams for the libp2p node.
* Default: 1024
*/
maxOutboundStreams: number;
}
/**
* Implementation for JS peers which connects to Fluence through relay node
*/
export class RelayConnection extends FluenceConnection {
constructor(
public peerId: PeerIdB58,
private _lib2p2Peer: Libp2p,
private _relayAddress: Multiaddr,
public readonly relayPeerId: PeerIdB58,
) {
super();
export class RelayConnection implements IStartable, IConnection {
private relayAddress: Multiaddr;
private lib2p2Peer: Libp2p | null = null;
constructor(private config: RelayConnectionConfig) {
this.relayAddress = multiaddr(this.config.relayAddress);
throwIfHasNoPeerId(this.relayAddress);
}
private _connection?: Connection;
getRelayPeerId(): string {
// since we check for peer id in constructor, we can safely use ! here
return this.relayAddress.getPeerId()!;
}
supportsRelay(): boolean {
return true;
}
particleSource = new Subject<IParticle>();
async start(): Promise<void> {
// check if already started
if (this.lib2p2Peer !== null) {
return;
}
static async createConnection(options: FluenceConnectionOptions): Promise<RelayConnection> {
const lib2p2Peer = await createLibp2p({
peerId: options.peerId,
peerId: this.config.peerId,
transports: [
webSockets({
filter: all,
@ -83,30 +112,32 @@ export class RelayConnection extends FluenceConnection {
],
streamMuxers: [mplex()],
connectionEncryption: [noise()],
connectionManager: {
dialTimeout: this.config.dialTimeoutMs,
},
});
const relayMultiaddr = multiaddr(options.relayAddress);
const relayPeerId = relayMultiaddr.getPeerId();
if (relayPeerId === null) {
throw new Error('Specified multiaddr is invalid or missing peer id: ' + options.relayAddress);
this.lib2p2Peer = lib2p2Peer;
this.lib2p2Peer.start();
await this.connect();
}
async stop(): Promise<void> {
// check if already stopped
if (this.lib2p2Peer === null) {
return;
}
return new RelayConnection(
// force new line
options.peerId.toString(),
lib2p2Peer,
relayMultiaddr,
relayPeerId,
);
await this.lib2p2Peer.unhandle(PROTOCOL_NAME);
await this.lib2p2Peer.stop();
}
async disconnect() {
await this._lib2p2Peer.unhandle(PROTOCOL_NAME);
await this._lib2p2Peer.stop();
}
async sendParticle(nextPeerIds: PeerIdB58[], particle: IParticle): Promise<void> {
if (this.lib2p2Peer === null) {
throw new Error('Relay connection is not started');
}
async sendParticle(nextPeerIds: PeerIdB58[], particle: string): Promise<void> {
if (nextPeerIds.length !== 1 && nextPeerIds[0] !== this.relayPeerId) {
if (nextPeerIds.length !== 1 && nextPeerIds[0] !== this.getRelayPeerId()) {
throw new Error(
`Relay connection only accepts peer id of the connected relay. Got: ${JSON.stringify(
nextPeerIds,
@ -123,27 +154,23 @@ export class RelayConnection extends FluenceConnection {
const sink = this._connection.streams[0].sink;
*/
const stream = await this._lib2p2Peer.dialProtocol(this._relayAddress, PROTOCOL_NAME);
const stream = await this.lib2p2Peer.dialProtocol(this.relayAddress, PROTOCOL_NAME);
const sink = stream.sink;
pipe(
[fromString(particle)],
[fromString(serializeToString(particle))],
// @ts-ignore
encode(),
sink,
);
}
async connect(onIncomingParticle: ParticleHandler) {
await this._lib2p2Peer.start();
private async connect() {
if (this.lib2p2Peer === null) {
throw new Error('Relay connection is not started');
}
// TODO: make it configurable
const handleOptions = {
maxInboundStreams: 1024,
maxOutboundStreams: 1024,
};
this._lib2p2Peer.handle(
this.lib2p2Peer.handle(
[PROTOCOL_NAME],
async ({ connection, stream }) => {
pipe(
@ -156,7 +183,8 @@ export class RelayConnection extends FluenceConnection {
try {
for await (const msg of source) {
try {
onIncomingParticle(msg);
const particle = Particle.fromString(msg);
this.particleSource.next(particle);
} catch (e) {
log.error('error on handling a new incoming message: %j', e);
}
@ -167,17 +195,20 @@ export class RelayConnection extends FluenceConnection {
},
);
},
handleOptions,
{
maxInboundStreams: this.config.maxInboundStreams,
maxOutboundStreams: this.config.maxOutboundStreams,
},
);
log.debug("dialing to the node with client's address: %s", this._lib2p2Peer.peerId.toString());
log.debug("dialing to the node with client's address: %s", this.lib2p2Peer.peerId.toString());
try {
this._connection = await this._lib2p2Peer.dial(this._relayAddress);
await this.lib2p2Peer.dial(this.relayAddress);
} catch (e: any) {
if (e.name === 'AggregateError' && e._errors?.length === 1) {
const error = e._errors[0];
throw new Error(`Error dialing node ${this._relayAddress}:\n${error.code}\n${error.message}`);
throw new Error(`Error dialing node ${this.relayAddress}:\n${error.code}\n${error.message}`);
} else {
throw e;
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2023 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.
*/
import type { PeerIdB58 } from '@fluencelabs/interfaces';
import type { Subscribable } from 'rxjs';
import { IParticle } from '../particle/interfaces.js';
/**
* Interface for connection used in Fluence Peer.
*/
export interface IConnection {
/**
* Observable that emits particles received from the connection.
*/
particleSource: Subscribable<IParticle>;
/**
* Send particle to the network using the connection.
* @param nextPeerIds - list of peer ids to send the particle to
* @param particle - particle to send
*/
sendParticle(nextPeerIds: PeerIdB58[], particle: IParticle): Promise<void>;
/**
* Get peer id of the relay peer. Throws an error if the connection doesn't support relay.
*/
getRelayPeerId(): PeerIdB58;
/**
* Check if the connection supports relay.
*/
supportsRelay(): boolean;
}

View File

@ -0,0 +1,80 @@
import { it, describe, expect, beforeEach, afterEach } from 'vitest';
import { DEFAULT_CONFIG, FluencePeer } from '../../jsPeer/FluencePeer.js';
import { CallServiceData, ResultCodes } from '../../jsServiceHost/interfaces.js';
import { KeyPair } from '../../keypair/index.js';
import { EphemeralNetworkClient } from '../client.js';
import { EphemeralNetwork, defaultConfig } from '../network.js';
let en: EphemeralNetwork;
let client: FluencePeer;
const relay = defaultConfig.peers[0].peerId;
// TODO: race condition here. Needs to be fixed
describe.skip('Ephemeral networks tests', () => {
beforeEach(async () => {
en = new EphemeralNetwork(defaultConfig);
await en.up();
const kp = await KeyPair.randomEd25519();
client = new EphemeralNetworkClient(DEFAULT_CONFIG, kp, en, relay);
await client.start();
});
afterEach(async () => {
if (client) {
await client.stop();
}
if (en) {
await en.down();
}
});
it('smoke test', async function () {
// arrange
const peers = defaultConfig.peers.map((x) => x.peerId);
const script = `
(seq
(call "${relay}" ("op" "noop") [])
(seq
(call "${peers[1]}" ("op" "noop") [])
(seq
(call "${peers[2]}" ("op" "noop") [])
(seq
(call "${peers[3]}" ("op" "noop") [])
(seq
(call "${peers[4]}" ("op" "noop") [])
(seq
(call "${peers[5]}" ("op" "noop") [])
(seq
(call "${relay}" ("op" "noop") [])
(call %init_peer_id% ("test" "test") [])
)
)
)
)
)
)
)
`;
const particle = client.internals.createNewParticle(script);
const promise = new Promise<string>((resolve) => {
client.internals.regHandler.forParticle(particle.id, 'test', 'test', (req: CallServiceData) => {
resolve('success');
return {
result: 'test',
retCode: ResultCodes.success,
};
});
});
// act
client.internals.initiateParticle(particle, () => {});
// assert
await expect(promise).resolves.toBe('success');
});
});

View File

@ -0,0 +1,39 @@
/*
* Copyright 2023 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.
*/
import { PeerIdB58 } from '@fluencelabs/interfaces';
import { MarineBasedAvmRunner } from '../jsPeer/avm.js';
import { FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js';
import { KeyPair } from '../keypair/index.js';
import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js';
import { WorkerLoader } from '../marine/worker-script/workerLoader.js';
import { MarineBackgroundRunner } from '../marine/worker/index.js';
import { EphemeralNetwork } from './network.js';
import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js';
/**
* Ephemeral network client is a FluencePeer that connects to a relay peer in an ephemeral network.
*/
export class EphemeralNetworkClient extends FluencePeer {
constructor(config: PeerConfig, keyPair: KeyPair, network: EphemeralNetwork, relay: PeerIdB58) {
const workerLoader = new WorkerLoader();
const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader);
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader);
const conn = network.getRelayConnection(keyPair.getPeerId(), relay);
super(config, keyPair, marine, new JsServiceHost(), avm, conn);
}
}

View File

@ -0,0 +1,290 @@
/*
* Copyright 2023 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.
*/
import { PeerIdB58 } from '@fluencelabs/interfaces';
import { fromBase64Sk, KeyPair } from '../keypair/index.js';
import { MarineBackgroundRunner } from '../marine/worker/index.js';
import { WorkerLoaderFromFs } from '../marine/deps-loader/node.js';
import { logger } from '../util/logger.js';
import { Subject } from 'rxjs';
import { Particle } from '../particle/Particle.js';
import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js';
import { MarineBasedAvmRunner } from '../jsPeer/avm.js';
import { DEFAULT_CONFIG, FluencePeer } from '../jsPeer/FluencePeer.js';
import { IConnection } from '../connection/interfaces.js';
import { IAvmRunner, IMarineHost } from '../marine/interfaces.js';
import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js';
const log = logger('ephemeral');
interface EphemeralConfig {
peers: Array<{
peerId: PeerIdB58;
sk: string;
}>;
}
export const defaultConfig = {
peers: [
{
peerId: '12D3KooWJankP2PcEDYCZDdJ26JsU8BMRfdGWyGqbtFiWyoKVtmx',
sk: 'dWNAHhDVuFj9bEieILMu6TcCFRxBJdOPIvAWmf4sZQI=',
},
{
peerId: '12D3KooWSBTB5sYxdwayUyTnqopBwABsnGFY3p4dTx5hABYDtJjV',
sk: 'dOmaxAeu4Th+MJ22vRDLMFTNbiDgKNXar9fW9ofAMgQ=',
},
{
peerId: '12D3KooWQjwf781DJ41moW5RrZXypLdnTbo6aMsoA8QLctGGX8RB',
sk: 'TgzaLlxXuOMDNuuuTKEHUKsW0jM4AmX0gahFvkB1KgE=',
},
{
peerId: '12D3KooWCXWTLFyY1mqKnNAhLQTsjW1zqDzCMbUs8M4a8zdz28HK',
sk: 'hiO2Ta8g2ibMQ7iu5yj9CfN+qQCwE8oRShjr7ortKww=',
},
{
peerId: '12D3KooWPmZpf4ng6GMS39HLagxsXbjiTPLH5CFJpFAHyN6amw6V',
sk: 'LzJtOHTqxfrlHDW40BKiLfjai8JU4yW6/s2zrXLCcQE=',
},
{
peerId: '12D3KooWKrx8PZxM1R9A8tp2jmrFf6c6q1ZQiWfD4QkNgh7fWSoF',
sk: 'XMhlk/xr1FPcp7sKQhS18doXlq1x16EMhBC2NGW2LQ4=',
},
{
peerId: '12D3KooWCbJHvnzSZEXjR1UJmtSUozuJK13iRiCYHLN1gjvm4TZZ',
sk: 'KXPAIqxrSHr7v0ngv3qagcqivFvnQ0xd3s1/rKmi8QU=',
},
{
peerId: '12D3KooWEvKe7WQHp42W4xhHRgTAWQjtDWyH38uJbLHAsMuTtYvD',
sk: 'GCYMAshGnsrNtrHhuT7ayzh5uCzX99J03PmAXoOcCgw=',
},
{
peerId: '12D3KooWSznSHN3BGrSykBXkLkFsqo9SYB73wVauVdqeuRt562cC',
sk: 'UP+SEuznS0h259VbFquzyOJAQ4W5iIwhP+hd1PmUQQ0=',
},
{
peerId: '12D3KooWF57jwbShfnT3c4dNfRDdGjr6SQ3B71m87UVpEpSWHFwi',
sk: '8dl+Crm5RSh0eh+LqLKwX8/Eo4QLpvIjfD8L0wzX4A4=',
},
{
peerId: '12D3KooWBWrzpSg9nwMLBCa2cJubUjTv63Mfy6PYg9rHGbetaV5C',
sk: 'qolc1FcpJ+vHDon0HeXdUYnstjV1wiVx2p0mjblrfAg=',
},
{
peerId: '12D3KooWNkLVU6juM8oyN2SVq5nBd2kp7Rf4uzJH1hET6vj6G5j6',
sk: 'vN6QzWILTM7hSHp+iGkKxiXcqs8bzlnH3FPaRaDGSQY=',
},
{
peerId: '12D3KooWKo1YwGL5vivPiKJMJS7wjtB6B2nJNdSXPkSABT4NKBUU',
sk: 'YbDQ++bsor2kei7rYAsu2SbyoiOYPRzFRZWnNRUpBgQ=',
},
{
peerId: '12D3KooWLUyBKmmNCyxaPkXoWcUFPcy5qrZsUo2E1tyM6CJmGJvC',
sk: 'ptB9eSFMKudAtHaFgDrRK/1oIMrhBujxbMw2Pzwx/wA=',
},
{
peerId: '12D3KooWAEZXME4KMu9FvLezsJWDbYFe2zyujyMnDT1AgcAxgcCk',
sk: 'xtwTOKgAbDIgkuPf7RKiR7gYyZ1HY4mOgFMv3sOUcAQ=',
},
{
peerId: '12D3KooWEhXetsFVAD9h2dRz9XgFpfidho1TCZVhFrczX8h8qgzY',
sk: '1I2MGuiKG1F4FDMiRihVOcOP2mxzOLWJ99MeexK27A4=',
},
{
peerId: '12D3KooWDBfVNdMyV3hPEF4WLBmx9DwD2t2SYuqZ2mztYmDzZWM1',
sk: 'eqJ4Bp7iN4aBXgPH0ezwSg+nVsatkYtfrXv9obI0YQ0=',
},
{
peerId: '12D3KooWSyY7wiSiR4vbXa1WtZawi3ackMTqcQhEPrvqtagoWPny',
sk: 'UVM3SBJhPYIY/gafpnd9/q/Fn9V4BE9zkgrvF1T7Pgc=',
},
{
peerId: '12D3KooWFZmBMGG9PxTs9s6ASzkLGKJWMyPheA5ruaYc2FDkDTmv',
sk: '8RbZfEVpQhPVuhv64uqxENDuSoyJrslQoSQJznxsTQ0=',
},
{
peerId: '12D3KooWBbhUaqqur6KHPunnKxXjY1daCtqJdy4wRji89LmAkVB4',
sk: 'RbgKmG6soWW9uOi7yRedm+0Qck3f3rw6MSnDP7AcBQs=',
},
],
};
export interface IEphemeralConnection extends IConnection {
readonly selfPeerId: PeerIdB58;
readonly connections: Map<PeerIdB58, IEphemeralConnection>;
receiveParticle(particle: Particle): void;
}
export class EphemeralConnection implements IConnection, IEphemeralConnection {
readonly selfPeerId: PeerIdB58;
readonly connections: Map<PeerIdB58, IEphemeralConnection> = new Map();
constructor(selfPeerId: PeerIdB58) {
this.selfPeerId = selfPeerId;
}
connectToOther(other: IEphemeralConnection) {
if (other.selfPeerId === this.selfPeerId) {
return;
}
this.connections.set(other.selfPeerId, other);
other.connections.set(this.selfPeerId, this);
}
disconnectFromOther(other: IEphemeralConnection) {
this.connections.delete(other.selfPeerId);
other.connections.delete(this.selfPeerId);
}
disconnectFromAll() {
for (let other of this.connections.values()) {
this.disconnectFromOther(other);
}
}
particleSource = new Subject<Particle>();
receiveParticle(particle: Particle): void {
this.particleSource.next(Particle.fromString(particle.toString()));
}
async sendParticle(nextPeerIds: string[], particle: Particle): Promise<void> {
const from = this.selfPeerId;
for (let to of nextPeerIds) {
const destConnection = this.connections.get(to);
if (destConnection === undefined) {
log.error('peer %s has no connection with %s', from, to);
continue;
}
// log.trace(`Sending particle from %s, to %j, particleId %s`, from, to, particle.id);
destConnection.receiveParticle(particle);
}
}
getRelayPeerId(): string {
if (this.connections.size === 1) {
return this.connections.keys().next().value;
}
throw new Error('relay is not supported in this Ephemeral network peer');
}
supportsRelay(): boolean {
return this.connections.size === 1;
}
}
class EphemeralPeer extends FluencePeer {
ephemeralConnection: EphemeralConnection;
constructor(keyPair: KeyPair, marine: IMarineHost, avm: IAvmRunner) {
const conn = new EphemeralConnection(keyPair.getPeerId());
super(DEFAULT_CONFIG, keyPair, marine, new JsServiceHost(), avm, conn);
this.ephemeralConnection = conn;
}
}
/**
* Ephemeral network implementation.
* Ephemeral network is a virtual network which runs locally and focuses on p2p interaction by removing connectivity layer out of the equation.
*/
export class EphemeralNetwork {
private peers: Map<PeerIdB58, EphemeralPeer> = new Map();
workerLoader: WorkerLoaderFromFs;
controlModuleLoader: WasmLoaderFromNpm;
avmModuleLoader: WasmLoaderFromNpm;
constructor(public readonly config: EphemeralConfig) {
// shared worker for all the peers
this.workerLoader = new WorkerLoaderFromFs('../../marine/worker-script');
this.controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
this.avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');
}
/**
* Starts the Ephemeral network up
*/
async up(): Promise<void> {
log.trace('starting ephemeral network up...');
const promises = this.config.peers.map(async (x) => {
const kp = await fromBase64Sk(x.sk);
const marine = new MarineBackgroundRunner(this.workerLoader, this.controlModuleLoader);
const avm = new MarineBasedAvmRunner(marine, this.avmModuleLoader);
const peerId = kp.getPeerId();
if (peerId !== x.peerId) {
throw new Error(`Invalid config: peer id ${x.peerId} does not match the secret key ${x.sk}`);
}
return new EphemeralPeer(kp, marine, avm);
});
const peers = await Promise.all(promises);
for (let i = 0; i < peers.length; i++) {
for (let j = 0; j < i; j++) {
if (i === j) {
continue;
}
peers[i].ephemeralConnection.connectToOther(peers[j].ephemeralConnection);
}
}
const startPromises = peers.map((x) => x.start());
await Promise.all(startPromises);
for (let p of peers) {
this.peers.set(p.keyPair.getPeerId(), p);
}
}
/**
* Shuts the ephemeral network down. Will disconnect all connected peers.
*/
async down(): Promise<void> {
log.trace('shutting down ephemeral network...');
const peers = Array.from(this.peers.entries());
const promises = peers.map(async ([k, p]) => {
await p.ephemeralConnection.disconnectFromAll();
await p.stop();
});
await Promise.all(promises);
this.peers.clear();
log.trace('ephemeral network shut down');
}
/**
* Gets a relay connection to the specified peer.
*/
getRelayConnection(peerId: PeerIdB58, relayPeerId: PeerIdB58): IConnection {
const relay = this.peers.get(relayPeerId);
if (relay === undefined) {
throw new Error(`Peer ${relayPeerId} is not found`);
}
const res = new EphemeralConnection(peerId);
res.connectToOther(relay.ephemeralConnection);
return res;
}
}

View File

@ -1,802 +0,0 @@
/*
* Copyright 2021 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.
*/
import 'buffer';
import { RelayConnection } from '../connection/index.js';
import { FluenceConnection, IAvmRunner, IMarine } from '../interfaces/index.js';
import { fromOpts, KeyPair } from '../keypair/index.js';
import {
CallServiceData,
CallServiceResult,
GenericCallServiceHandler,
ResultCodes,
} from '../interfaces/commonTypes.js';
import type {
PeerIdB58,
IFluenceClient,
KeyPairOptions,
RelayOptions,
ClientOptions,
ConnectionState,
} from '@fluencelabs/interfaces/dist/fluenceClient';
import { Particle, ParticleExecutionStage, ParticleQueueItem } from './Particle.js';
import { jsonify, isString, ServiceError } from './utils.js';
import { concatMap, filter, pipe, Subject, tap } from 'rxjs';
import { builtInServices } from './builtins/common.js';
import { defaultSigGuard, Sig } from './builtins/Sig.js';
import { registerSig } from './_aqua/services.js';
import { registerSrv } from './_aqua/single-module-srv.js';
import { Buffer } from 'buffer';
import { JSONValue } from '@fluencelabs/avm';
import { NodeUtils, Srv } from './builtins/SingleModuleSrv.js';
import { registerNodeUtils } from './_aqua/node-utils.js';
import type { MultiaddrInput } from '@multiformats/multiaddr';
import { logger } from '../util/logger.js';
const log = logger('particle');
const DEFAULT_TTL = 7000;
export type PeerConfig = ClientOptions & { relay?: RelayOptions };
type PeerStatus =
| {
isInitialized: false;
peerId: null;
isConnected: false;
relayPeerId: null;
}
| {
isInitialized: true;
peerId: PeerIdB58;
isConnected: false;
relayPeerId: null;
}
| {
isInitialized: true;
peerId: PeerIdB58;
isConnected: true;
relayPeerId: PeerIdB58;
}
| {
isInitialized: true;
peerId: PeerIdB58;
isConnected: true;
isDirect: true;
relayPeerId: null;
};
/**
* This class implements the Fluence protocol for javascript-based environments.
* It provides all the necessary features to communicate with Fluence network
*/
export class FluencePeer implements IFluenceClient {
connectionState: ConnectionState = 'disconnected';
connectionStateChangeHandler: (state: ConnectionState) => void = () => {};
constructor(private marine: IMarine, private avmRunner: IAvmRunner) {}
/**
* Internal contract to cast unknown objects to IFluenceClient.
* If an unknown object has this property then we assume it is in fact a Peer and it implements IFluenceClient
* Check against this variable MUST NOT be coupled with any `FluencePeer` because otherwise it might get bundled
* brining a lot of unnecessary stuff alongside with it
*/
__isFluenceAwesome = true;
/**
* TODO: remove this from here. Switch to `ConnectionState` instead
* @deprecated
*/
getStatus(): PeerStatus {
if (this._keyPair === undefined) {
return {
isInitialized: false,
peerId: null,
isConnected: false,
relayPeerId: null,
};
}
if (this.connection === null) {
return {
isInitialized: true,
peerId: this._keyPair.getPeerId(),
isConnected: false,
relayPeerId: null,
};
}
if (this.connection.relayPeerId === null) {
return {
isInitialized: true,
peerId: this._keyPair.getPeerId(),
isConnected: true,
isDirect: true,
relayPeerId: null,
};
}
return {
isInitialized: true,
peerId: this._keyPair.getPeerId(),
isConnected: true,
relayPeerId: this.connection.relayPeerId,
};
}
getPeerId(): string {
return this.getStatus().peerId!;
}
getRelayPeerId(): string {
return this.getStatus().relayPeerId!;
}
getPeerSecretKey(): Uint8Array {
if (!this._keyPair) {
throw new Error("Can't get key pair: peer is not initialized");
}
return this._keyPair.toEd25519PrivateKey();
}
onConnectionStateChange(handler: (state: ConnectionState) => void): ConnectionState {
this.connectionStateChangeHandler = handler;
return this.connectionState;
}
/**
* Connect to the Fluence network
* @param relay - relay node to connect to
* @param options - client options
*/
async connect(relay: RelayOptions, options?: ClientOptions): Promise<void> {
return this.start({ relay, ...options });
}
/**
* Disconnect from the Fluence network
*/
disconnect(): Promise<void> {
return this.stop();
}
/**
* Initializes the peer: starts the Aqua VM, initializes the default call service handlers
* and (optionally) connect to the Fluence network
* @param config - object specifying peer configuration
*/
async start(config: PeerConfig = {}): Promise<void> {
this.changeConnectionState('connecting');
const keyPair = await makeKeyPair(config.keyPair);
await this.init(config, keyPair);
const conn = await configToConnection(keyPair, config.relay, config.connectionOptions?.dialTimeoutMs);
if (conn !== null) {
await this._connect(conn);
}
this.changeConnectionState('connected');
}
getServices() {
if (this._classServices === undefined) {
throw new Error(`Can't get services: peer is not initialized`);
}
return {
...this._classServices,
};
}
/**
* Registers marine service within the Fluence peer from wasm file.
* Following helper functions can be used to load wasm files:
* * loadWasmFromFileSystem
* * loadWasmFromNpmPackage
* * loadWasmFromServer
* @param wasm - buffer with the wasm file for service
* @param serviceId - the service id by which the service can be accessed in aqua
*/
async registerMarineService(wasm: SharedArrayBuffer | Buffer, serviceId: string): Promise<void> {
if (!this.marine) {
throw new Error("Can't register marine service: peer is not initialized");
}
if (this._containsService(serviceId)) {
throw new Error(`Service with '${serviceId}' id already exists`);
}
await this.marine.createService(wasm, serviceId);
this._marineServices.add(serviceId);
}
/**
* Removes the specified marine service from the Fluence peer
* @param serviceId - the service id to remove
*/
removeMarineService(serviceId: string): void {
this._marineServices.delete(serviceId);
}
/**
* Un-initializes the peer: stops all the underlying workflows, stops the Aqua VM
* and disconnects from the Fluence network
*/
async stop() {
this.changeConnectionState('disconnecting');
this._keyPair = undefined; // This will set peer to non-initialized state and stop particle processing
this._stopParticleProcessing();
await this._disconnect();
await this.marine.stop();
await this.avmRunner.stop();
this._classServices = undefined;
this._particleSpecificHandlers.clear();
this._commonHandlers.clear();
this._marineServices.clear();
this.changeConnectionState('disconnected');
}
// internal api
/**
* @private Is not intended to be used manually. Subject to change
*/
get internals() {
return {
getConnectionState: () => this.connectionState,
getRelayPeerId: () => this.getStatus().relayPeerId,
parseAst: async (air: string): Promise<{ success: boolean; data: any }> => {
const status = this.getStatus();
if (!status.isInitialized) {
new Error("Can't use avm: peer is not initialized");
}
const res = await this.marine.callService('avm', 'ast', [air], undefined);
if (!isString(res)) {
throw new Error(`Call to avm:ast expected to return string. Actual return: ${res}`);
}
try {
if (res.startsWith('error')) {
return {
success: false,
data: res,
};
} else {
return {
success: true,
data: JSON.parse(res),
};
}
} catch (err) {
throw new Error('Failed to call avm. Result: ' + res + '. Error: ' + err);
}
},
createNewParticle: (script: string, ttl: number = this._defaultTTL) => {
const status = this.getStatus();
if (!status.isInitialized) {
return new Error("Can't create new particle: peer is not initialized");
}
return Particle.createNew(script, ttl, status.peerId);
},
/**
* Initiates a new particle execution starting from local peer
* @param particle - particle to start execution of
*/
initiateParticle: (particle: Particle, onStageChange: (stage: ParticleExecutionStage) => void): void => {
const status = this.getStatus();
if (!status.isInitialized) {
throw new Error('Cannot initiate new particle: peer is not initialized');
}
if (this._printParticleId) {
console.log('Particle id: ', particle.id);
}
if (particle.initPeerId === undefined) {
particle.initPeerId = status.peerId;
}
if (particle.ttl === undefined) {
particle.ttl = this._defaultTTL;
}
this._incomingParticles.next({
particle: particle,
onStageChange: onStageChange,
});
},
/**
* Register Call Service handler functions
*/
regHandler: {
/**
* Register handler for all particles
*/
common: (
// force new line
serviceId: string,
fnName: string,
handler: GenericCallServiceHandler,
) => {
this._commonHandlers.set(serviceFnKey(serviceId, fnName), handler);
},
/**
* Register handler which will be called only for particle with the specific id
*/
forParticle: (
particleId: string,
serviceId: string,
fnName: string,
handler: GenericCallServiceHandler,
) => {
let psh = this._particleSpecificHandlers.get(particleId);
if (psh === undefined) {
psh = new Map<string, GenericCallServiceHandler>();
this._particleSpecificHandlers.set(particleId, psh);
}
psh.set(serviceFnKey(serviceId, fnName), handler);
},
},
};
}
/**
* @private Subject to change. Do not use this method directly
*/
async init(config: Omit<PeerConfig, 'keyPair'>, keyPair: KeyPair) {
this._keyPair = keyPair;
const peerId = this._keyPair.getPeerId();
if (config?.debug?.printParticleId) {
this._printParticleId = true;
}
this._defaultTTL = config?.defaultTtlMs ?? DEFAULT_TTL;
await this.marine.start();
await this.avmRunner.start();
registerDefaultServices(this);
this._classServices = {
sig: new Sig(this._keyPair),
srv: new Srv(this),
};
this._classServices.sig.securityGuard = defaultSigGuard(peerId);
registerSig(this, 'sig', this._classServices.sig);
registerSig(this, peerId, this._classServices.sig);
registerSrv(this, 'single_module_srv', this._classServices.srv);
registerNodeUtils(this, 'node_utils', new NodeUtils(this));
this._startParticleProcessing();
}
/**
* @private Subject to change. Do not use this method directly
*/
async _connect(connection: FluenceConnection): Promise<void> {
if (this.connection) {
await this.connection.disconnect();
}
this.connection = connection;
await this.connection.connect(this._onIncomingParticle.bind(this));
}
/**
* @private Subject to change. Do not use this method directly
*/
async _disconnect(): Promise<void> {
await this.connection?.disconnect();
}
// private
private changeConnectionState(state: ConnectionState) {
this.connectionState = state;
this.connectionStateChangeHandler(state);
}
// Queues for incoming and outgoing particles
private _incomingParticles = new Subject<ParticleQueueItem>();
private _outgoingParticles = new Subject<ParticleQueueItem & { nextPeerIds: PeerIdB58[] }>();
// Call service handler
private _marineServices = new Set<string>();
private _particleSpecificHandlers = new Map<string, Map<string, GenericCallServiceHandler>>();
private _commonHandlers = new Map<string, GenericCallServiceHandler>();
private _classServices?: {
sig: Sig;
srv: Srv;
};
private _containsService(serviceId: string): boolean {
return this._marineServices.has(serviceId) || this._commonHandlers.has(serviceId);
}
// Internal peer state
private connection: FluenceConnection | null = null;
private _printParticleId = false;
private _defaultTTL: number = DEFAULT_TTL;
private _keyPair: KeyPair | undefined;
private _timeouts: Array<NodeJS.Timeout> = [];
private _particleQueues = new Map<string, Subject<ParticleQueueItem>>();
private _onIncomingParticle(p: string) {
const particle = Particle.fromString(p);
this._incomingParticles.next({ particle, onStageChange: () => {} });
}
private _startParticleProcessing() {
this._incomingParticles
.pipe(
tap((x) => {
log.debug('id %s. received:', x.particle.id);
log.trace('id %s. data: %j', x.particle.id, {
initPeerId: x.particle.initPeerId,
timestamp: x.particle.timestamp,
tttl: x.particle.ttl,
signature: x.particle.signature,
});
log.trace('id %s. script: %s', x.particle.id, x.particle.script);
log.trace('id %s. call results: %j', x.particle.id, x.particle.callResults);
}),
filterExpiredParticles(this._expireParticle.bind(this)),
)
.subscribe((item) => {
const p = item.particle;
let particlesQueue = this._particleQueues.get(p.id);
if (!particlesQueue) {
particlesQueue = this._createParticlesProcessingQueue();
this._particleQueues.set(p.id, particlesQueue);
const timeout = setTimeout(() => {
this._expireParticle(item);
}, p.actualTtl());
this._timeouts.push(timeout);
}
particlesQueue.next(item);
});
this._outgoingParticles.subscribe((item) => {
// Do not send particle after the peer has been stopped
if (!this.getStatus().isInitialized) {
return;
}
if (!this.connection) {
log.error('id %s. cannot send, peer is not connected', item.particle.id);
item.onStageChange({ stage: 'sendingError' });
return;
}
log.debug('id %s. sending particle into network', item.particle.id);
this.connection
?.sendParticle(item.nextPeerIds, item.particle.toString())
.then(() => {
item.onStageChange({ stage: 'sent' });
})
.catch((e: any) => {
log.error('id %s. send failed %j', item.particle.id, e);
});
});
}
private _expireParticle(item: ParticleQueueItem) {
const particleId = item.particle.id;
log.debug(
'id %s. particle has expired after %d. Deleting particle-related queues and handlers',
item.particle.id,
item.particle.ttl,
);
this._particleQueues.delete(particleId);
this._particleSpecificHandlers.delete(particleId);
item.onStageChange({ stage: 'expired' });
}
private _createParticlesProcessingQueue() {
const particlesQueue = new Subject<ParticleQueueItem>();
let prevData: Uint8Array = Buffer.from([]);
particlesQueue
.pipe(
filterExpiredParticles(this._expireParticle.bind(this)),
concatMap(async (item) => {
const status = this.getStatus();
if (!status.isInitialized || this.marine === undefined) {
// If `.stop()` was called return null to stop particle processing immediately
return null;
}
// IMPORTANT!
// AVM runner execution and prevData <-> newData swapping
// MUST happen sequentially (in a critical section).
// Otherwise the race might occur corrupting the prevData
log.debug('id %s. sending particle to interpreter', item.particle.id);
log.trace('id %s. prevData: %a', item.particle.id, prevData);
const avmCallResult = await this.avmRunner.run(
{
initPeerId: item.particle.initPeerId,
currentPeerId: status.peerId,
timestamp: item.particle.timestamp,
ttl: item.particle.ttl,
},
item.particle.script,
prevData,
item.particle.data,
item.particle.callResults,
);
if (!(avmCallResult instanceof Error) && avmCallResult.retCode === 0) {
const newData = Buffer.from(avmCallResult.data);
prevData = newData;
}
return {
...item,
result: avmCallResult,
};
}),
)
.subscribe((item) => {
// If `.stop()` was called then item will be null and we need to stop particle processing immediately
if (item === null || !this.getStatus().isInitialized) {
return;
}
// Do not proceed further if the particle is expired
if (item.particle.hasExpired()) {
return;
}
// Do not continue if there was an error in particle interpretation
if (item.result instanceof Error) {
log.error('id %s. interpreter failed: %s', item.particle.id, item.result.message);
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.message });
return;
}
if (item.result.retCode !== 0) {
log.error(
'id %s. interpreter failed: retCode: %d, message: %s',
item.particle.id,
item.result.retCode,
item.result.errorMessage,
);
log.trace('id %s. avm data: %a', item.particle.id, item.result.data);
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.errorMessage });
return;
}
log.trace(
'id %s. interpreter result: retCode: %d, avm data: %a',
item.particle.id,
item.result.retCode,
item.result.data,
);
setTimeout(() => {
item.onStageChange({ stage: 'interpreted' });
}, 0);
// send particle further if requested
if (item.result.nextPeerPks.length > 0) {
const newParticle = item.particle.clone();
const newData = Buffer.from(item.result.data);
newParticle.data = newData;
this._outgoingParticles.next({
...item,
particle: newParticle,
nextPeerIds: item.result.nextPeerPks,
});
}
// execute call requests if needed
// and put particle with the results back to queue
if (item.result.callRequests.length > 0) {
for (const [key, cr] of item.result.callRequests) {
const req = {
fnName: cr.functionName,
args: cr.arguments,
serviceId: cr.serviceId,
tetraplets: cr.tetraplets,
particleContext: item.particle.getParticleContext(),
};
if (item.particle.hasExpired()) {
// just in case do not call any services if the particle is already expired
return;
}
this._execSingleCallRequest(req)
.catch((err): CallServiceResult => {
if (err instanceof ServiceError) {
return {
retCode: ResultCodes.error,
result: err.message,
};
}
return {
retCode: ResultCodes.error,
result: `Handler failed. fnName="${req.fnName}" serviceId="${
req.serviceId
}" error: ${err.toString()}`,
};
})
.then((res) => {
const serviceResult = {
result: jsonify(res.result),
retCode: res.retCode,
};
const newParticle = item.particle.clone();
newParticle.callResults = [[key, serviceResult]];
newParticle.data = Buffer.from([]);
particlesQueue.next({ ...item, particle: newParticle });
});
}
} else {
item.onStageChange({ stage: 'localWorkDone' });
}
});
return particlesQueue;
}
private async _execSingleCallRequest(req: CallServiceData): Promise<CallServiceResult> {
const particleId = req.particleContext.particleId;
log.trace('id %s. executing call service handler %j', particleId, req);
if (this.marine && this._marineServices.has(req.serviceId)) {
const result = await this.marine.callService(req.serviceId, req.fnName, req.args, undefined);
return {
retCode: ResultCodes.success,
result: result as JSONValue,
};
}
const key = serviceFnKey(req.serviceId, req.fnName);
const psh = this._particleSpecificHandlers.get(particleId);
let handler: GenericCallServiceHandler | undefined;
// we should prioritize handler for this particle if there is one
// if particle-specific handlers exist for this particle try getting handler there
if (psh !== undefined) {
handler = psh.get(key);
}
// then try to find a common handler for all particles with this service-fn key
// if there is no particle-specific handler, get one from common map
if (handler === undefined) {
handler = this._commonHandlers.get(key);
}
// if no handler is found return useful error message to AVM
if (handler === undefined) {
return {
retCode: ResultCodes.error,
result: `No handler has been registered for serviceId='${req.serviceId}' fnName='${
req.fnName
}' args='${jsonify(req.args)}'`,
};
}
// if we found a handler, execute it
const res = await handler(req);
if (res.result === undefined) {
res.result = null;
}
log.trace('id %s. executed call service handler, req: %j, res: %j ', particleId, req, res);
return res;
}
private _stopParticleProcessing() {
// do not hang if the peer has been stopped while some of the timeouts are still being executed
this._timeouts.forEach((timeout) => {
clearTimeout(timeout);
});
this._particleQueues.clear();
}
}
async function configToConnection(
keyPair: KeyPair,
connection?: RelayOptions,
dialTimeoutMs?: number,
): Promise<FluenceConnection | null> {
if (!connection) {
return null;
}
if (connection instanceof FluenceConnection) {
return connection;
}
let connectToMultiAddr: MultiaddrInput;
// figuring out what was specified as input
const tmp = connection as any;
if (tmp.multiaddr !== undefined) {
// specified as FluenceNode (object with multiaddr and peerId props)
connectToMultiAddr = tmp.multiaddr;
} else {
// specified as MultiaddrInput
connectToMultiAddr = tmp;
}
const res = await RelayConnection.createConnection({
peerId: keyPair.getLibp2pPeerId(),
relayAddress: connectToMultiAddr,
dialTimeoutMs: dialTimeoutMs,
});
return res;
}
function serviceFnKey(serviceId: string, fnName: string) {
return `${serviceId}/${fnName}`;
}
function registerDefaultServices(peer: FluencePeer) {
Object.entries(builtInServices).forEach(([serviceId, service]) => {
Object.entries(service).forEach(([fnName, fn]) => {
peer.internals.regHandler.common(serviceId, fnName, fn);
});
});
}
function filterExpiredParticles(onParticleExpiration: (item: ParticleQueueItem) => void) {
return pipe(
tap((item: ParticleQueueItem) => {
if (item.particle.hasExpired()) {
onParticleExpiration(item);
}
}),
filter((x: ParticleQueueItem) => !x.particle.hasExpired()),
);
}
async function makeKeyPair(opts?: KeyPairOptions) {
opts = opts || { type: 'Ed25519', source: 'random' };
return fromOpts(opts);
}

View File

@ -1,114 +0,0 @@
/*
* 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.
*/
import { fromUint8Array, toUint8Array } from 'js-base64';
import { CallResultsArray, LogLevel } from '@fluencelabs/avm';
import { v4 as uuidv4 } from 'uuid';
import { ParticleContext } from '../interfaces/commonTypes.js';
import { Buffer } from 'buffer';
export class Particle {
// TODO: make it not optional (should be added to the constructor)
signature?: string;
callResults: CallResultsArray = [];
constructor(
public id: string,
public timestamp: number,
public script: string,
public data: Uint8Array,
public ttl: number,
public initPeerId: string,
) {}
static createNew(script: string, ttl: number, initPeerId: string): Particle {
return new Particle(genUUID(), Date.now(), script, Buffer.from([]), ttl, initPeerId);
}
static fromString(str: string): Particle {
const json = JSON.parse(str);
const res = new Particle(
json.id,
json.timestamp,
json.script,
toUint8Array(json.data),
json.ttl,
json.init_peer_id,
);
res.signature = json.signature;
return res;
}
getParticleContext(): ParticleContext {
return {
particleId: this.id,
initPeerId: this.initPeerId,
timestamp: this.timestamp,
ttl: this.ttl,
signature: this.signature,
};
}
actualTtl(): number {
return this.timestamp + this.ttl - Date.now();
}
hasExpired(): boolean {
return this.actualTtl() <= 0;
}
clone(): Particle {
const res = new Particle(this.id, this.timestamp, this.script, this.data, this.ttl, this.initPeerId);
res.signature = this.signature;
res.callResults = this.callResults;
return res;
}
toString(): string {
return JSON.stringify({
action: 'Particle',
id: this.id,
init_peer_id: this.initPeerId,
timestamp: this.timestamp,
ttl: this.ttl,
script: this.script,
// TODO: copy signature from a particle after signatures will be implemented on nodes
signature: [],
data: this.data && fromUint8Array(this.data),
});
}
}
export type ParticleExecutionStage =
| { stage: 'received' }
| { stage: 'interpreted' }
| { stage: 'interpreterError'; errorMessage: string }
| { stage: 'localWorkDone' }
| { stage: 'sent' }
| { stage: 'sendingError' }
| { stage: 'expired' };
export interface ParticleQueueItem {
particle: Particle;
onStageChange: (state: ParticleExecutionStage) => void;
}
function genUUID() {
return uuidv4();
}

View File

@ -1,417 +0,0 @@
import { it, describe, expect } from 'vitest';
import { nodes } from '../connection.js';
import { checkConnection, doNothing, handleTimeout } from '../../utils.js';
import { registerHandlersHelper, mkTestPeer, withPeer, withConnectedPeer } from '../util.js';
import { FluencePeer } from '../../FluencePeer.js';
import { isFluencePeer } from '@fluencelabs/interfaces';
describe('Typescript usage suite', () => {
it('should perform test for FluencePeer class correctly', () => {
// arrange
const peer = mkTestPeer();
const number = 1;
const object = { str: 'Hello!' };
const undefinedVal = undefined;
// act
const isPeerPeer = isFluencePeer(peer);
const isNumberPeer = isFluencePeer(number);
const isObjectPeer = isFluencePeer(object);
const isUndefinedPeer = isFluencePeer(undefinedVal);
// act
expect(isPeerPeer).toBe(true);
expect(isNumberPeer).toBe(false);
expect(isObjectPeer).toBe(false);
expect(isUndefinedPeer).toBe(false);
});
describe('Should expose correct peer status', () => {
it('Should expose correct status for uninitialized peer', () => {
const peer = mkTestPeer();
const status = peer.getStatus();
expect(status.isConnected).toBe(false);
expect(status.isInitialized).toBe(false);
expect(status.peerId).toBe(null);
expect(status.relayPeerId).toBe(null);
});
it('Should expose correct status for initialized but not connected peer', async () => {
await withPeer(async (peer) => {
// arrange
// act
const status = peer.getStatus();
// assert
expect(status.isConnected).toBe(false);
expect(status.isInitialized).toBe(true);
expect(status.peerId).not.toBe(null);
expect(status.relayPeerId).toBe(null);
});
});
it('Should expose correct status for connected peer', async () => {
await withConnectedPeer(async (peer) => {
// arrange
// act
const status = peer.getStatus();
// assert
expect(status.isConnected).toBe(true);
expect(status.isInitialized).toBe(true);
expect(status.peerId).not.toBe(null);
expect(status.relayPeerId).not.toBe(null);
});
});
});
it('should make a call through network', async () => {
await withConnectedPeer(async (peer) => {
// arrange
const result = await new Promise<string[]>((resolve, reject) => {
const script = `
(xor
(seq
(call %init_peer_id% ("load" "relay") [] init_relay)
(seq
(call init_relay ("op" "identity") ["hello world!"] result)
(call %init_peer_id% ("callback" "callback") [result])
)
)
(seq
(call init_relay ("op" "identity") [])
(call %init_peer_id% ("callback" "error") [%last_error%])
)
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
load: {
relay: () => {
return peer.getStatus().relayPeerId;
},
},
callback: {
callback: (args: any) => {
const [val] = args;
resolve(val);
},
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
expect(result).toBe('hello world!');
});
});
it('check connection should work', async function () {
await withConnectedPeer(async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toEqual(true);
});
});
it('check connection should work with ttl', async function () {
await withConnectedPeer(async (peer) => {
const isConnected = await checkConnection(peer, 10000);
expect(isConnected).toEqual(true);
});
});
it('two clients should work inside the same time browser', async () => {
await withConnectedPeer(async (peer1) => {
await withConnectedPeer(async (peer2) => {
const res = new Promise((resolve) => {
peer2.internals.regHandler.common('test', 'test', (req) => {
resolve(req.args[0]);
return {
result: {},
retCode: 0,
};
});
});
const script = `
(seq
(call "${peer1.getStatus().relayPeerId}" ("op" "identity") [])
(call "${peer2.getStatus().peerId}" ("test" "test") ["test"])
)
`;
const particle = peer1.internals.createNewParticle(script);
if (particle instanceof Error) {
throw particle;
}
peer1.internals.initiateParticle(particle, doNothing);
expect(await res).toEqual('test');
});
});
});
describe('should make connection to network', () => {
it('address as string', async () => {
await withConnectedPeer(async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
});
});
it('address as multiaddr', async () => {
await withConnectedPeer(async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
});
});
it('address as node', async () => {
await withConnectedPeer(async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
});
});
it('With connection options: dialTimeout', async () => {
await withPeer(
async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
},
{ relay: nodes[0], connectionOptions: { dialTimeoutMs: 100000 } },
);
});
it('With connection options: skipCheckConnection', async () => {
await withPeer(
async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeTruthy();
},
{ relay: nodes[0], connectionOptions: { skipCheckConnection: true } },
);
});
it('With connection options: defaultTTL', async () => {
await withPeer(
async (peer) => {
const isConnected = await checkConnection(peer);
expect(isConnected).toBeFalsy();
},
{ relay: nodes[0], defaultTtlMs: 1 },
);
});
});
it('Should successfully call identity on local peer', async function () {
await withPeer(async (peer) => {
const res = await new Promise<string>((resolve, reject) => {
const script = `
(seq
(call %init_peer_id% ("op" "identity") ["test"] res)
(call %init_peer_id% ("callback" "callback") [res])
)
`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
callback: {
callback: async (args: any) => {
const [res] = args;
resolve(res);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
expect(res).toBe('test');
});
});
it('Should throw correct message when calling non existing local service', async function () {
await withConnectedPeer(async (peer) => {
const res = callIncorrectService(peer);
await expect(res).rejects.toMatchObject({
message: expect.stringContaining(
`No handler has been registered for serviceId='incorrect' fnName='incorrect' args='[]'\"'`,
),
// instruction: 'call %init_peer_id% ("incorrect" "incorrect") [] res',
});
});
});
it('Should not crash if undefined is passed as a variable', async () => {
await withPeer(async (peer) => {
const res = await new Promise<any>((resolve, reject) => {
const script = `
(seq
(call %init_peer_id% ("load" "arg") [] arg)
(seq
(call %init_peer_id% ("op" "identity") [arg] res)
(call %init_peer_id% ("callback" "callback") [res])
)
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
load: {
arg: () => undefined,
},
callback: {
callback: (args: any) => {
const [val] = args;
resolve(val);
},
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
expect(res).toBe(null);
});
});
it('Should not crash if an error ocurred in user-defined handler', async () => {
await withPeer(async (peer) => {
const promise = new Promise<any>((_resolve, reject) => {
const script = `
(xor
(call %init_peer_id% ("load" "arg") [] arg)
(call %init_peer_id% ("callback" "error") [%last_error%])
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
load: {
arg: () => {
throw new Error('my super custom error message');
},
},
callback: {
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
await expect(promise).rejects.toMatchObject({
message: expect.stringContaining('my super custom error message'),
});
});
});
it('Should return error if particle is created on a stopped peer', async () => {
const peer = mkTestPeer();
const particle = peer.internals.createNewParticle(`(null)`);
expect(particle instanceof Error).toBe(true);
});
it.skip('Should throw correct error when the client tries to send a particle not to the relay', async () => {
await withConnectedPeer(async (peer) => {
const promise = new Promise((resolve, reject) => {
const script = `
(xor
(call "incorrect_peer_id" ("any" "service") [])
(call %init_peer_id% ("callback" "error") [%last_error%])
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
callback: {
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, doNothing);
});
await expect(promise).rejects.toMatch(
'Particle is expected to be sent to only the single peer (relay which client is connected to)',
);
});
});
});
async function callIncorrectService(peer: FluencePeer): Promise<string[]> {
return new Promise<any[]>((resolve, reject) => {
const script = `
(xor
(call %init_peer_id% ("incorrect" "incorrect") [] res)
(call %init_peer_id% ("callback" "error") [%last_error%])
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
callback: {
callback: (args: any) => {
resolve(args);
},
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
}

View File

@ -1,61 +0,0 @@
import { it, describe, expect } from 'vitest';
import { handleTimeout } from '../../utils.js';
import { nodes } from '../connection.js';
import { mkTestPeer, registerHandlersHelper } from '../util.js';
describe('Smoke test', () => {
it('Simple call', async () => {
// arrange
const peer = mkTestPeer();
await peer.start({
relay: nodes[0],
});
const result = await new Promise<string[]>((resolve, reject) => {
const script = `
(xor
(seq
(call %init_peer_id% ("load" "relay") [] init_relay)
(seq
(call init_relay ("op" "identity") ["hello world!"] result)
(call %init_peer_id% ("callback" "callback") [result])
)
)
(seq
(call init_relay ("op" "identity") [])
(call %init_peer_id% ("callback" "error") [%last_error%])
)
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
load: {
relay: () => {
return peer.getStatus().relayPeerId;
},
},
callback: {
callback: (args: any) => {
const [val] = args;
resolve(val);
},
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
await peer.stop();
expect(result).toBe('hello world!');
});
});

View File

@ -1,35 +0,0 @@
import { it, describe, expect, beforeAll, afterAll } from 'vitest';
import { mkTestPeer } from '../util.js';
const peer = mkTestPeer();
describe('Parse ast tests', () => {
beforeAll(async () => {
await peer.start();
});
afterAll(async () => {
await peer.stop();
});
it('Correct ast should be parsed correctly', async function () {
const air = `(null)`;
const res = await peer.internals.parseAst(air);
expect(res).toStrictEqual({
success: true,
data: { Null: null },
});
});
it('Incorrect ast should result in corresponding error', async function () {
const air = `(null`;
const res = await peer.internals.parseAst(air);
expect(res).toStrictEqual({
success: false,
data: expect.stringContaining('error'),
});
});
});

View File

@ -1,84 +0,0 @@
import { KeyPair } from '@fluencelabs/keypair';
import { EphemeralNetwork, defaultConfig } from '../../ephemeral';
import { ResultCodes } from '../../commonTypes';
import { FluencePeer } from '../../FluencePeer';
import { mkTestPeer } from '../util';
let en: EphemeralNetwork;
let peer: FluencePeer;
// TODO: jest tests hang when running this test. Fix it (DXJ-219)
describe.skip('Ephemeral networks tests', () => {
beforeEach(async () => {
en = new EphemeralNetwork(defaultConfig);
await en.up();
const relay = defaultConfig.peers[0].peerId;
peer = mkTestPeer();
await peer.init({
KeyPair: await KeyPair.randomEd25519(),
});
const conn = en.getRelayConnection(relay, peer);
await peer.connect(conn);
});
afterEach(async () => {
if (peer) {
await peer.stop();
}
if (en) {
await en.down();
}
});
it('smoke test', async function () {
const relay = peer.getStatus().relayPeerId!;
const peers = defaultConfig.peers.map((x) => x.peerId);
const script = `
(seq
(call "${relay}" ("op" "noop") [])
(seq
(call "${peers[0]}" ("op" "noop") [])
(seq
(call "${peers[1]}" ("op" "noop") [])
(seq
(call "${peers[2]}" ("op" "noop") [])
(seq
(call "${peers[3]}" ("op" "noop") [])
(seq
(call "${peers[4]}" ("op" "noop") [])
(seq
(call "${relay}" ("op" "noop") [])
(call %init_peer_id% ("test" "test") [])
)
)
)
)
)
)
)
`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
throw particle;
}
const promise = new Promise<string>((resolve) => {
peer.internals.regHandler.forParticle(particle.id, 'test', 'test', (req) => {
resolve('success');
return {
result: 'test',
retCode: ResultCodes.success,
};
});
});
peer.internals.initiateParticle(particle, () => {});
await expect(promise).resolves.toBe('success');
});
});

View File

@ -1,83 +0,0 @@
import * as api from '@fluencelabs/aqua-api/aqua-api.js';
import { promises as fs } from 'fs';
import { FluencePeer, PeerConfig } from '../FluencePeer.js';
import { Particle } from '../Particle.js';
import { MakeServiceCall } from '../utils.js';
import { avmModuleLoader, controlModuleLoader } from '../utilsForNode.js';
import { ServiceDef } from '@fluencelabs/interfaces';
import { callAquaFunction } from '../../compilerSupport/callFunction.js';
import { MarineBackgroundRunner } from '../../marine/worker/index.js';
import { MarineBasedAvmRunner } from '../avm.js';
import { nodes } from './connection.js';
import { WorkerLoaderFromFs } from '../../marine/deps-loader/node.js';
export const registerHandlersHelper = (
peer: FluencePeer,
particle: Particle,
handlers: Record<string, Record<string, any>>,
) => {
Object.entries(handlers).forEach(([serviceId, service]) => {
Object.entries(service).forEach(([fnName, fn]) => {
peer.internals.regHandler.forParticle(particle.id, serviceId, fnName, MakeServiceCall(fn));
});
});
};
export type CompiledFnCall = (peer: FluencePeer, args: { [key: string]: any }) => Promise<unknown>;
export type CompiledFile = {
functions: { [key: string]: CompiledFnCall };
services: { [key: string]: ServiceDef };
};
export const compileAqua = async (aquaFile: string): Promise<CompiledFile> => {
await fs.access(aquaFile);
const compilationResult = await api.Aqua.compile(new api.Path(aquaFile), [], undefined);
if (compilationResult.errors.length > 0) {
throw new Error('Aqua compilation failed. Error: ' + compilationResult.errors.join('/n'));
}
const functions = Object.entries(compilationResult.functions)
.map(([name, fnInfo]) => {
const callFn = (peer: FluencePeer, args: { [key: string]: any }) => {
return callAquaFunction({
def: fnInfo.funcDef,
script: fnInfo.script,
config: {},
peer: peer,
args,
});
};
return { [name]: callFn };
})
.reduce((agg, obj) => {
return { ...agg, ...obj };
}, {});
return { functions, services: compilationResult.services };
};
export const mkTestPeer = () => {
const workerLoader = new WorkerLoaderFromFs('../../marine/worker-script');
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader);
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader);
return new FluencePeer(marine, avm);
};
export const withPeer = async (action: (p: FluencePeer) => Promise<void>, config?: PeerConfig) => {
const p = mkTestPeer();
try {
await p.start(config);
await action(p);
} finally {
await p!.stop();
}
};
export const withConnectedPeer = async (action: (p: FluencePeer) => Promise<void>, config?: PeerConfig) => {
return withPeer(action, { relay: nodes[0] });
};

View File

@ -1,9 +0,0 @@
import { IFluenceClient, ServiceDef } from '@fluencelabs/interfaces';
import { registerService } from '../../compilerSupport/registerService.js';
export const registerServiceImpl = (
peer: IFluenceClient,
def: ServiceDef,
serviceId: string | undefined,
service: any,
) => registerService({ peer, def, service, serviceId });

View File

@ -1,252 +0,0 @@
import { PeerIdB58 } from '@fluencelabs/interfaces';
import { FluenceConnection, ParticleHandler } from '../interfaces/index.js';
import { fromBase64Sk } from '../keypair/index.js';
import { FluencePeer } from './FluencePeer.js';
import { MarineBackgroundRunner } from '../marine/worker/index.js';
import { avmModuleLoader, controlModuleLoader } from './utilsForNode.js';
import { MarineBasedAvmRunner } from './avm.js';
import { WorkerLoaderFromFs } from '../marine/deps-loader/node.js';
import { logger } from '../util/logger.js';
interface EphemeralConfig {
peers: Array<{
peerId: PeerIdB58;
sk: string;
}>;
}
interface PeerAdapter {
isEphemeral: boolean;
peer: FluencePeer;
peerId: PeerIdB58;
onIncoming: ParticleHandler;
connections: Set<PeerIdB58>;
}
const log = logger('ephemeral');
export const defaultConfig = {
peers: [
{
peerId: '12D3KooWJankP2PcEDYCZDdJ26JsU8BMRfdGWyGqbtFiWyoKVtmx',
sk: 'dWNAHhDVuFj9bEieILMu6TcCFRxBJdOPIvAWmf4sZQI=',
},
{
peerId: '12D3KooWSBTB5sYxdwayUyTnqopBwABsnGFY3p4dTx5hABYDtJjV',
sk: 'dOmaxAeu4Th+MJ22vRDLMFTNbiDgKNXar9fW9ofAMgQ=',
},
{
peerId: '12D3KooWQjwf781DJ41moW5RrZXypLdnTbo6aMsoA8QLctGGX8RB',
sk: 'TgzaLlxXuOMDNuuuTKEHUKsW0jM4AmX0gahFvkB1KgE=',
},
{
peerId: '12D3KooWCXWTLFyY1mqKnNAhLQTsjW1zqDzCMbUs8M4a8zdz28HK',
sk: 'hiO2Ta8g2ibMQ7iu5yj9CfN+qQCwE8oRShjr7ortKww=',
},
{
peerId: '12D3KooWPmZpf4ng6GMS39HLagxsXbjiTPLH5CFJpFAHyN6amw6V',
sk: 'LzJtOHTqxfrlHDW40BKiLfjai8JU4yW6/s2zrXLCcQE=',
},
{
peerId: '12D3KooWKrx8PZxM1R9A8tp2jmrFf6c6q1ZQiWfD4QkNgh7fWSoF',
sk: 'XMhlk/xr1FPcp7sKQhS18doXlq1x16EMhBC2NGW2LQ4=',
},
{
peerId: '12D3KooWCbJHvnzSZEXjR1UJmtSUozuJK13iRiCYHLN1gjvm4TZZ',
sk: 'KXPAIqxrSHr7v0ngv3qagcqivFvnQ0xd3s1/rKmi8QU=',
},
{
peerId: '12D3KooWEvKe7WQHp42W4xhHRgTAWQjtDWyH38uJbLHAsMuTtYvD',
sk: 'GCYMAshGnsrNtrHhuT7ayzh5uCzX99J03PmAXoOcCgw=',
},
{
peerId: '12D3KooWSznSHN3BGrSykBXkLkFsqo9SYB73wVauVdqeuRt562cC',
sk: 'UP+SEuznS0h259VbFquzyOJAQ4W5iIwhP+hd1PmUQQ0=',
},
{
peerId: '12D3KooWF57jwbShfnT3c4dNfRDdGjr6SQ3B71m87UVpEpSWHFwi',
sk: '8dl+Crm5RSh0eh+LqLKwX8/Eo4QLpvIjfD8L0wzX4A4=',
},
{
peerId: '12D3KooWBWrzpSg9nwMLBCa2cJubUjTv63Mfy6PYg9rHGbetaV5C',
sk: 'qolc1FcpJ+vHDon0HeXdUYnstjV1wiVx2p0mjblrfAg=',
},
{
peerId: '12D3KooWNkLVU6juM8oyN2SVq5nBd2kp7Rf4uzJH1hET6vj6G5j6',
sk: 'vN6QzWILTM7hSHp+iGkKxiXcqs8bzlnH3FPaRaDGSQY=',
},
{
peerId: '12D3KooWKo1YwGL5vivPiKJMJS7wjtB6B2nJNdSXPkSABT4NKBUU',
sk: 'YbDQ++bsor2kei7rYAsu2SbyoiOYPRzFRZWnNRUpBgQ=',
},
{
peerId: '12D3KooWLUyBKmmNCyxaPkXoWcUFPcy5qrZsUo2E1tyM6CJmGJvC',
sk: 'ptB9eSFMKudAtHaFgDrRK/1oIMrhBujxbMw2Pzwx/wA=',
},
{
peerId: '12D3KooWAEZXME4KMu9FvLezsJWDbYFe2zyujyMnDT1AgcAxgcCk',
sk: 'xtwTOKgAbDIgkuPf7RKiR7gYyZ1HY4mOgFMv3sOUcAQ=',
},
{
peerId: '12D3KooWEhXetsFVAD9h2dRz9XgFpfidho1TCZVhFrczX8h8qgzY',
sk: '1I2MGuiKG1F4FDMiRihVOcOP2mxzOLWJ99MeexK27A4=',
},
{
peerId: '12D3KooWDBfVNdMyV3hPEF4WLBmx9DwD2t2SYuqZ2mztYmDzZWM1',
sk: 'eqJ4Bp7iN4aBXgPH0ezwSg+nVsatkYtfrXv9obI0YQ0=',
},
{
peerId: '12D3KooWSyY7wiSiR4vbXa1WtZawi3ackMTqcQhEPrvqtagoWPny',
sk: 'UVM3SBJhPYIY/gafpnd9/q/Fn9V4BE9zkgrvF1T7Pgc=',
},
{
peerId: '12D3KooWFZmBMGG9PxTs9s6ASzkLGKJWMyPheA5ruaYc2FDkDTmv',
sk: '8RbZfEVpQhPVuhv64uqxENDuSoyJrslQoSQJznxsTQ0=',
},
{
peerId: '12D3KooWBbhUaqqur6KHPunnKxXjY1daCtqJdy4wRji89LmAkVB4',
sk: 'RbgKmG6soWW9uOi7yRedm+0Qck3f3rw6MSnDP7AcBQs=',
},
],
};
/**
* Ephemeral network implementation.
* Ephemeral network is a virtual network which runs locally and focuses on p2p interaction by removing connectivity layer out of the equation.
*/
export class EphemeralNetwork {
private _peers: Map<PeerIdB58, PeerAdapter> = new Map();
constructor(public readonly config: EphemeralConfig) {}
/**
* Starts the Ephemeral network up
*/
async up(): Promise<void> {
log.trace('starting ephemeral network up...');
const allPeerIds = this.config.peers.map((x) => x.peerId);
// shared worker for all the peers
const workerLoader = new WorkerLoaderFromFs('../../marine/worker-script');
const promises = this.config.peers.map(async (x) => {
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader);
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader);
const peer = new FluencePeer(marine, avm);
const sendParticle = async (nextPeerIds: string[], particle: string): Promise<void> => {
this._send(peer.getStatus().peerId!, nextPeerIds, particle);
};
const kp = await fromBase64Sk(x.sk);
if (kp.getPeerId() !== x.peerId) {
throw new Error(`Invalid config: peer id ${x.peerId} does not match the secret key ${x.sk}`);
}
await peer.init({}, kp);
let handler: ParticleHandler | null = null;
const connectionCtor = class extends FluenceConnection {
relayPeerId = null;
async connect(onIncomingParticle: ParticleHandler): Promise<void> {
handler = onIncomingParticle;
}
async disconnect(): Promise<void> {
handler = null;
}
sendParticle = sendParticle;
};
await peer._connect(new connectionCtor());
const peerId = peer.getStatus().peerId!;
const ephPeer: PeerAdapter = {
isEphemeral: true,
connections: new Set(allPeerIds.filter((x) => x !== peerId)),
peer: peer,
peerId: peerId,
onIncoming: handler!,
};
return [peerId, ephPeer] as const;
});
const values = await Promise.all(promises);
this._peers = new Map(values);
log.trace('ephemeral network started...');
}
/**
* Shuts the ephemeral network down. Will disconnect all connected peers.
*/
async down(): Promise<void> {
log.trace('shutting down ephemeral network...');
const peers = Array.from(this._peers.entries());
const promises = peers.map(([k, p]) => {
return p.isEphemeral ? p.peer.stop() : p.peer._disconnect();
});
await Promise.all(promises);
this._peers.clear();
log.trace('ephemeral network shut down');
}
/**
* Gets the FluenceConnection which can be used to connect to the ephemeral networks via the specified relay peer.
*/
getRelayConnection(relay: PeerIdB58, peer: FluencePeer): FluenceConnection {
const me = this;
const relayPeer = this._peers.get(relay);
if (relayPeer === undefined) {
throw new Error(`Relay with peer Id: ${relay} has not been found in ephemeral network`);
}
const connectionCtor = class extends FluenceConnection {
relayPeerId = relay;
async connect(onIncomingParticle: ParticleHandler): Promise<void> {
const peerId = peer.getStatus().peerId!;
me._peers.set(peerId, {
isEphemeral: false,
peer: peer,
onIncoming: onIncomingParticle,
peerId: peerId,
connections: new Set([relay]),
});
relayPeer.connections.add(peerId);
}
async disconnect(): Promise<void> {
const peerId = peer.getStatus().peerId!;
relayPeer.connections.delete(peerId);
me._peers.delete(peerId);
}
async sendParticle(nextPeerIds: string[], particle: string): Promise<void> {
const peerId = peer.getStatus().peerId!;
me._send(peerId, nextPeerIds, particle);
}
};
return new connectionCtor();
}
private async _send(from: PeerIdB58, to: PeerIdB58[], particle: string) {
log.trace(`Sending particle from %s, to %j`, from, to);
const peer = this._peers.get(from);
if (peer === undefined) {
log.error(`Peer ${from} cannot be found in ephemeral network`);
return;
}
for (let dest of to) {
if (!peer.connections.has(dest)) {
log.error(`Peer ${from} has no connection with ${dest}`);
continue;
}
const destPeer = this._peers.get(dest);
if (destPeer === undefined) {
log.error(`peer ${destPeer} cannot be found in ephemeral network`);
continue;
}
destPeer.onIncoming(particle);
}
}
}

View File

@ -1,5 +0,0 @@
import { WorkerLoaderFromFs, WasmLoaderFromFs, WasmLoaderFromNpm } from '../marine/deps-loader/node.js';
// TODO!: after moving to ESM loaders stopped working. Should be fixed in scope of DXJ-194
export const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
export const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');

View File

@ -0,0 +1,557 @@
/*
* Copyright 2021 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.
*/
import { KeyPair } from '../keypair/index.js';
import type { PeerIdB58 } from '@fluencelabs/interfaces';
import {
cloneWithNewData,
getActualTTL,
hasExpired,
Particle,
ParticleExecutionStage,
ParticleQueueItem,
} from '../particle/Particle.js';
import { jsonify, isString } from '../util/utils.js';
import { concatMap, filter, pipe, Subject, tap, Unsubscribable } from 'rxjs';
import { defaultSigGuard, Sig } from '../services/Sig.js';
import { registerSig } from '../services/_aqua/services.js';
import { registerSrv } from '../services/_aqua/single-module-srv.js';
import { Buffer } from 'buffer';
import { Srv } from '../services/SingleModuleSrv.js';
import { logger } from '../util/logger.js';
import { getParticleContext, registerDefaultServices, ServiceError } from '../jsServiceHost/serviceUtils.js';
import { IParticle } from '../particle/interfaces.js';
import { IConnection } from '../connection/interfaces.js';
import { IAvmRunner, IMarineHost } from '../marine/interfaces.js';
import {
CallServiceData,
CallServiceResult,
GenericCallServiceHandler,
IJsServiceHost,
ResultCodes,
} from '../jsServiceHost/interfaces.js';
import { JSONValue } from '../util/commonTypes.js';
const log_particle = logger('particle');
const log_peer = logger('peer');
export type PeerConfig = {
/**
* Sets the default TTL for all particles originating from the peer with no TTL specified.
* If the originating particle's TTL is defined then that value will be used
* If the option is not set default TTL will be 7000
*/
defaultTtlMs: number;
/**
* Enables\disabled various debugging features
*/
debug: {
/**
* If set to true, newly initiated particle ids will be printed to console.
* Useful to see what particle id is responsible for aqua function
*/
printParticleId: boolean;
};
};
export const DEFAULT_CONFIG: PeerConfig = {
debug: {
printParticleId: false,
},
defaultTtlMs: 7000,
};
/**
* This class implements the Fluence protocol for javascript-based environments.
* It provides all the necessary features to communicate with Fluence network
*/
export abstract class FluencePeer {
constructor(
protected readonly config: PeerConfig,
public readonly keyPair: KeyPair,
protected readonly marineHost: IMarineHost,
protected readonly jsServiceHost: IJsServiceHost,
protected readonly avmRunner: IAvmRunner,
protected readonly connection: IConnection,
) {
this._initServices();
}
/**
* Internal contract to cast unknown objects to IFluenceClient.
* If an unknown object has this property then we assume it is in fact a Peer and it implements IFluenceClient
* Check against this variable MUST NOT be coupled with any `FluencePeer` because otherwise it might get bundled
* brining a lot of unnecessary stuff alongside with it
*/
__isFluenceAwesome = true;
async start(): Promise<void> {
log_peer.trace('starting Fluence peer');
if (this.config?.debug?.printParticleId) {
this.printParticleId = true;
}
await this.marineHost.start();
await this.avmRunner.start();
this._startParticleProcessing();
this.isInitialized = true;
log_peer.trace('started Fluence peer');
}
/**
* Un-initializes the peer: stops all the underlying workflows, stops the Aqua VM
* and disconnects from the Fluence network
*/
async stop() {
log_peer.trace('stopping Fluence peer');
this._particleSourceSubscription?.unsubscribe();
this._stopParticleProcessing();
await this.marineHost.stop();
await this.avmRunner.stop();
this.isInitialized = false;
log_peer.trace('stopped Fluence peer');
}
/**
* Registers marine service within the Fluence peer from wasm file.
* Following helper functions can be used to load wasm files:
* * loadWasmFromFileSystem
* * loadWasmFromNpmPackage
* * loadWasmFromServer
* @param wasm - buffer with the wasm file for service
* @param serviceId - the service id by which the service can be accessed in aqua
*/
async registerMarineService(wasm: SharedArrayBuffer | Buffer, serviceId: string): Promise<void> {
if (!this.marineHost) {
throw new Error("Can't register marine service: peer is not initialized");
}
if (this.jsServiceHost.hasService(serviceId)) {
throw new Error(`Service with '${serviceId}' id already exists`);
}
await this.marineHost.createService(wasm, serviceId);
}
/**
* Removes the specified marine service from the Fluence peer
* @param serviceId - the service id to remove
*/
removeMarineService(serviceId: string): void {
this.marineHost.removeService(serviceId);
}
// internal api
/**
* @private Is not intended to be used manually. Subject to change
*/
get internals() {
return {
getServices: () => this._classServices,
getRelayPeerId: () => {
if (this.connection.supportsRelay()) {
return this.connection.getRelayPeerId();
}
throw new Error('Relay is not supported by the current connection');
},
parseAst: async (air: string): Promise<{ success: boolean; data: any }> => {
if (!this.isInitialized) {
new Error("Can't use avm: peer is not initialized");
}
const res = await this.marineHost.callService('avm', 'ast', [air], undefined);
if (!isString(res)) {
throw new Error(`Call to avm:ast expected to return string. Actual return: ${res}`);
}
try {
if (res.startsWith('error')) {
return {
success: false,
data: res,
};
} else {
return {
success: true,
data: JSON.parse(res),
};
}
} catch (err) {
throw new Error('Failed to call avm. Result: ' + res + '. Error: ' + err);
}
},
createNewParticle: (script: string, ttl: number = this.config.defaultTtlMs): IParticle => {
return Particle.createNew(script, this.keyPair.getPeerId(), ttl);
},
/**
* Initiates a new particle execution starting from local peer
* @param particle - particle to start execution of
*/
initiateParticle: (particle: IParticle, onStageChange: (stage: ParticleExecutionStage) => void): void => {
if (!this.isInitialized) {
throw new Error('Cannot initiate new particle: peer is not initialized');
}
if (this.printParticleId) {
console.log('Particle id: ', particle.id);
}
this._incomingParticles.next({
particle: particle,
callResults: [],
onStageChange: onStageChange,
});
},
/**
* Register Call Service handler functions
*/
regHandler: {
/**
* Register handler for all particles
*/
common: this.jsServiceHost.registerGlobalHandler.bind(this.jsServiceHost),
/**
* Register handler which will be called only for particle with the specific id
*/
forParticle: this.jsServiceHost.registerParticleScopeHandler.bind(this.jsServiceHost),
},
};
}
// Queues for incoming and outgoing particles
private _incomingParticles = new Subject<ParticleQueueItem>();
private _outgoingParticles = new Subject<ParticleQueueItem & { nextPeerIds: PeerIdB58[] }>();
private _timeouts: Array<NodeJS.Timeout> = [];
private _particleSourceSubscription?: Unsubscribable;
private _particleQueues = new Map<string, Subject<ParticleQueueItem>>();
// Internal peer state
// @ts-expect-error - initialized in constructor through `_initServices` call
private _classServices: {
sig: Sig;
srv: Srv;
};
private isInitialized = false;
private printParticleId = false;
private _initServices() {
this._classServices = {
sig: new Sig(this.keyPair),
srv: new Srv(this),
};
const peerId = this.keyPair.getPeerId();
registerDefaultServices(this);
this._classServices.sig.securityGuard = defaultSigGuard(peerId);
registerSig(this, 'sig', this._classServices.sig);
registerSig(this, peerId, this._classServices.sig);
registerSrv(this, 'single_module_srv', this._classServices.srv);
}
private _startParticleProcessing() {
this._particleSourceSubscription = this.connection.particleSource.subscribe({
next: (p) => {
this._incomingParticles.next({ particle: p, callResults: [], onStageChange: () => {} });
},
});
this._incomingParticles
.pipe(
tap((item) => {
log_particle.debug('id %s. received:', item.particle.id);
log_particle.trace('id %s. data: %j', item.particle.id, {
initPeerId: item.particle.initPeerId,
timestamp: item.particle.timestamp,
tttl: item.particle.ttl,
signature: item.particle.signature,
});
log_particle.trace('id %s. script: %s', item.particle.id, item.particle.script);
log_particle.trace('id %s. call results: %j', item.particle.id, item.callResults);
}),
filterExpiredParticles(this._expireParticle.bind(this)),
)
.subscribe((item) => {
const p = item.particle;
let particlesQueue = this._particleQueues.get(p.id);
if (!particlesQueue) {
particlesQueue = this._createParticlesProcessingQueue();
this._particleQueues.set(p.id, particlesQueue);
const timeout = setTimeout(() => {
this._expireParticle(item);
}, getActualTTL(p));
this._timeouts.push(timeout);
}
particlesQueue.next(item);
});
this._outgoingParticles.subscribe((item) => {
// Do not send particle after the peer has been stopped
if (!this.isInitialized) {
return;
}
log_particle.debug(
'id %s. sending particle into network. Next peer ids: %s',
item.particle.id,
item.nextPeerIds.toString(),
);
this.connection
?.sendParticle(item.nextPeerIds, item.particle)
.then(() => {
item.onStageChange({ stage: 'sent' });
})
.catch((e: any) => {
log_particle.error('id %s. send failed %j', item.particle.id, e);
item.onStageChange({ stage: 'sendingError', errorMessage: e.toString() });
});
});
}
private _expireParticle(item: ParticleQueueItem) {
const particleId = item.particle.id;
log_particle.debug(
'id %s. particle has expired after %d. Deleting particle-related queues and handlers',
item.particle.id,
item.particle.ttl,
);
this._particleQueues.delete(particleId);
this.jsServiceHost.removeParticleScopeHandlers(particleId);
item.onStageChange({ stage: 'expired' });
}
private _createParticlesProcessingQueue() {
const particlesQueue = new Subject<ParticleQueueItem>();
let prevData: Uint8Array = Buffer.from([]);
particlesQueue
.pipe(
filterExpiredParticles(this._expireParticle.bind(this)),
concatMap(async (item) => {
if (!this.isInitialized || this.marineHost === undefined) {
// If `.stop()` was called return null to stop particle processing immediately
return null;
}
// IMPORTANT!
// AVM runner execution and prevData <-> newData swapping
// MUST happen sequentially (in a critical section).
// Otherwise the race might occur corrupting the prevData
log_particle.debug('id %s. sending particle to interpreter', item.particle.id);
log_particle.trace('id %s. prevData: %a', item.particle.id, prevData);
const avmCallResult = await this.avmRunner.run(
{
initPeerId: item.particle.initPeerId,
currentPeerId: this.keyPair.getPeerId(),
timestamp: item.particle.timestamp,
ttl: item.particle.ttl,
},
item.particle.script,
prevData,
item.particle.data,
item.callResults,
);
if (!(avmCallResult instanceof Error) && avmCallResult.retCode === 0) {
const newData = Buffer.from(avmCallResult.data);
prevData = newData;
}
return {
...item,
result: avmCallResult,
};
}),
)
.subscribe((item) => {
// If peer was stopped, do not proceed further
if (item === null || !this.isInitialized) {
return;
}
// Do not proceed further if the particle is expired
if (hasExpired(item.particle)) {
return;
}
// Do not continue if there was an error in particle interpretation
if (item.result instanceof Error) {
log_particle.error('id %s. interpreter failed: %s', item.particle.id, item.result.message);
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.message });
return;
}
if (item.result.retCode !== 0) {
log_particle.error(
'id %s. interpreter failed: retCode: %d, message: %s',
item.particle.id,
item.result.retCode,
item.result.errorMessage,
);
log_particle.trace('id %s. avm data: %a', item.particle.id, item.result.data);
item.onStageChange({ stage: 'interpreterError', errorMessage: item.result.errorMessage });
return;
}
log_particle.trace(
'id %s. interpreter result: retCode: %d, avm data: %a',
item.particle.id,
item.result.retCode,
item.result.data,
);
setTimeout(() => {
item.onStageChange({ stage: 'interpreted' });
}, 0);
// send particle further if requested
if (item.result.nextPeerPks.length > 0) {
const newParticle = cloneWithNewData(item.particle, Buffer.from(item.result.data));
this._outgoingParticles.next({
...item,
particle: newParticle,
nextPeerIds: item.result.nextPeerPks,
});
}
// execute call requests if needed
// and put particle with the results back to queue
if (item.result.callRequests.length > 0) {
for (const [key, cr] of item.result.callRequests) {
const req = {
fnName: cr.functionName,
args: cr.arguments,
serviceId: cr.serviceId,
tetraplets: cr.tetraplets,
particleContext: getParticleContext(item.particle),
};
if (hasExpired(item.particle)) {
// just in case do not call any services if the particle is already expired
return;
}
this._execSingleCallRequest(req)
.catch((err): CallServiceResult => {
if (err instanceof ServiceError) {
return {
retCode: ResultCodes.error,
result: err.message,
};
}
return {
retCode: ResultCodes.error,
result: `Service call failed. fnName="${req.fnName}" serviceId="${
req.serviceId
}" error: ${err.toString()}`,
};
})
.then((res) => {
const serviceResult = {
result: jsonify(res.result),
retCode: res.retCode,
};
const newParticle = cloneWithNewData(item.particle, Buffer.from([]));
particlesQueue.next({
...item,
particle: newParticle,
callResults: [[key, serviceResult]],
});
});
}
} else {
item.onStageChange({ stage: 'localWorkDone' });
}
});
return particlesQueue;
}
private async _execSingleCallRequest(req: CallServiceData): Promise<CallServiceResult> {
const particleId = req.particleContext.particleId;
log_particle.trace('id %s. executing call service handler %j', particleId, req);
if (this.marineHost && this.marineHost.hasService(req.serviceId)) {
const result = await this.marineHost.callService(req.serviceId, req.fnName, req.args, undefined);
return {
retCode: ResultCodes.success,
result: result as JSONValue,
};
}
let res = await this.jsServiceHost.callService(req);
if (res === null) {
res = {
retCode: ResultCodes.error,
result: `No service found for service call: serviceId='${req.serviceId}', fnName='${
req.fnName
}' args='${jsonify(req.args)}'`,
};
}
log_particle.trace('id %s. executed call service handler, req: %j, res: %j ', particleId, req, res);
return res;
}
private _stopParticleProcessing() {
// do not hang if the peer has been stopped while some of the timeouts are still being executed
this._timeouts.forEach((timeout) => {
clearTimeout(timeout);
});
this._particleQueues.clear();
}
}
function filterExpiredParticles(onParticleExpiration: (item: ParticleQueueItem) => void) {
return pipe(
tap((item: ParticleQueueItem) => {
if (hasExpired(item.particle)) {
onParticleExpiration(item);
}
}),
filter((x: ParticleQueueItem) => !hasExpired(x.particle)),
);
}

View File

@ -1,9 +1,8 @@
import { it, describe, expect } from 'vitest';
import { registerHandlersHelper, withPeer } from '../../util/testUtils.js';
import { handleTimeout } from '../../particle/Particle.js';
import { handleTimeout } from '../../utils.js';
import { registerHandlersHelper, withPeer } from '../util.js';
describe('Avm spec', () => {
describe('Basic AVM functionality in Fluence Peer tests', () => {
it('Simple call', async () => {
await withPeer(async (peer) => {
const res = await new Promise<string[]>((resolve, reject) => {

View File

@ -0,0 +1,29 @@
import { it, describe, expect } from 'vitest';
import { withPeer } from '../../util/testUtils.js';
describe('Parse ast tests', () => {
it('Correct ast should be parsed correctly', async () => {
withPeer(async (peer) => {
const air = `(null)`;
const res = await peer.internals.parseAst(air);
expect(res).toStrictEqual({
success: true,
data: { Null: null },
});
});
});
it('Incorrect ast should result in corresponding error', async () => {
withPeer(async (peer) => {
const air = `(null`;
const res = await peer.internals.parseAst(air);
expect(res).toStrictEqual({
success: false,
data: expect.stringContaining('error'),
});
});
});
});

View File

@ -0,0 +1,178 @@
import { it, describe, expect } from 'vitest';
import { isFluencePeer } from '@fluencelabs/interfaces';
import { mkTestPeer, registerHandlersHelper, withPeer } from '../../util/testUtils.js';
import { handleTimeout } from '../../particle/Particle.js';
import { FluencePeer } from '../FluencePeer.js';
describe('FluencePeer usage test suite', () => {
it('should perform test for FluencePeer class correctly', async () => {
// arrange
const peer = await mkTestPeer();
const number = 1;
const object = { str: 'Hello!' };
const undefinedVal = undefined;
// act
const isPeerPeer = isFluencePeer(peer);
const isNumberPeer = isFluencePeer(number);
const isObjectPeer = isFluencePeer(object);
const isUndefinedPeer = isFluencePeer(undefinedVal);
// act
expect(isPeerPeer).toBe(true);
expect(isNumberPeer).toBe(false);
expect(isObjectPeer).toBe(false);
expect(isUndefinedPeer).toBe(false);
});
it('Should successfully call identity on local peer', async function () {
await withPeer(async (peer) => {
const res = await new Promise<string>((resolve, reject) => {
const script = `
(seq
(call %init_peer_id% ("op" "identity") ["test"] res)
(call %init_peer_id% ("callback" "callback") [res])
)
`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
callback: {
callback: async (args: any) => {
const [res] = args;
resolve(res);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
expect(res).toBe('test');
});
});
it('Should throw correct message when calling non existing local service', async function () {
await withPeer(async (peer) => {
const res = callIncorrectService(peer);
await expect(res).rejects.toMatchObject({
message: expect.stringContaining(
`"No service found for service call: serviceId='incorrect', fnName='incorrect' args='[]'"`,
),
instruction: 'call %init_peer_id% ("incorrect" "incorrect") [] res',
});
});
});
it('Should not crash if undefined is passed as a variable', async () => {
await withPeer(async (peer) => {
const res = await new Promise<any>((resolve, reject) => {
const script = `
(seq
(call %init_peer_id% ("load" "arg") [] arg)
(seq
(call %init_peer_id% ("op" "identity") [arg] res)
(call %init_peer_id% ("callback" "callback") [res])
)
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
load: {
arg: () => undefined,
},
callback: {
callback: (args: any) => {
const [val] = args;
resolve(val);
},
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
expect(res).toBe(null);
});
});
it('Should not crash if an error ocurred in user-defined handler', async () => {
await withPeer(async (peer) => {
const promise = new Promise<any>((_resolve, reject) => {
const script = `
(xor
(call %init_peer_id% ("load" "arg") [] arg)
(call %init_peer_id% ("callback" "error") [%last_error%])
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
load: {
arg: () => {
throw new Error('my super custom error message');
},
},
callback: {
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
await expect(promise).rejects.toMatchObject({
message: expect.stringContaining('my super custom error message'),
});
});
});
});
async function callIncorrectService(peer: FluencePeer): Promise<string[]> {
return new Promise<any[]>((resolve, reject) => {
const script = `
(xor
(call %init_peer_id% ("incorrect" "incorrect") [] res)
(call %init_peer_id% ("callback" "error") [%last_error%])
)`;
const particle = peer.internals.createNewParticle(script);
if (particle instanceof Error) {
return reject(particle.message);
}
registerHandlersHelper(peer, particle, {
callback: {
callback: (args: any) => {
resolve(args);
},
error: (args: any) => {
const [error] = args;
reject(error);
},
},
});
peer.internals.initiateParticle(particle, handleTimeout(reject));
});
}

View File

@ -1,9 +1,24 @@
/*
* Copyright 2023 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.
*/
import type { CallResultsArray, InterpreterResult, RunParameters } from '@fluencelabs/avm';
import { deserializeAvmResult, serializeAvmArgs } from '@fluencelabs/avm';
import type { IMarine, IAvmRunner, IWasmLoader } from '../interfaces/index.js';
import { IAvmRunner, IMarineHost, IWasmLoader } from '../marine/interfaces.js';
export class MarineBasedAvmRunner implements IAvmRunner {
constructor(private marine: IMarine, private avmWasmLoader: IWasmLoader) {}
constructor(private marine: IMarineHost, private avmWasmLoader: IWasmLoader) {}
async run(
runParams: RunParameters,

View File

@ -0,0 +1,111 @@
/*
* Copyright 2023 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.
*/
import { CallServiceData, CallServiceResult, GenericCallServiceHandler, IJsServiceHost } from './interfaces.js';
export class JsServiceHost implements IJsServiceHost {
private particleScopeHandlers = new Map<string, Map<string, GenericCallServiceHandler>>();
private commonHandlers = new Map<string, GenericCallServiceHandler>();
/**
* Returns true if any handler for the specified serviceId is registered
*/
hasService(serviceId: string): boolean {
return this.commonHandlers.has(serviceId) || this.particleScopeHandlers.has(serviceId);
}
/**
* Removes all handlers associated with the specified particle scope
* @param particleId Particle ID to remove handlers for
*/
removeParticleScopeHandlers(particleId: string): void {
this.particleScopeHandlers.delete(particleId);
}
/**
* Find call service handler for specified particle
* @param serviceId Service ID as specified in `call` air instruction
* @param fnName Function name as specified in `call` air instruction
* @param particleId Particle ID
*/
getHandler(serviceId: string, fnName: string, particleId: string): GenericCallServiceHandler | null {
const key = serviceFnKey(serviceId, fnName);
const psh = this.particleScopeHandlers.get(particleId);
let handler: GenericCallServiceHandler | undefined = undefined;
// we should prioritize handler for this particle if there is one
// if particle-scoped handler exist for this particle try getting handler there
if (psh !== undefined) {
handler = psh.get(key);
}
// then try to find a common handler for all particles with this service-fn key
// if there is no particle-specific handler, get one from common map
if (handler === undefined) {
handler = this.commonHandlers.get(key);
}
return handler || null;
}
/**
* Execute service call for specified call service data. Return null if no handler was found
*/
async callService(req: CallServiceData): Promise<CallServiceResult | null> {
const handler = this.getHandler(req.serviceId, req.fnName, req.particleContext.particleId);
if (handler === null) {
return null;
}
const result = await handler(req);
// Otherwise AVM might break
if (result.result === undefined) {
result.result = null;
}
return result;
}
/**
* Register handler for all particles
*/
registerGlobalHandler(serviceId: string, fnName: string, handler: GenericCallServiceHandler): void {
this.commonHandlers.set(serviceFnKey(serviceId, fnName), handler);
}
/**
* Register handler which will be called only for particle with the specific id
*/
registerParticleScopeHandler(
particleId: string,
serviceId: string,
fnName: string,
handler: GenericCallServiceHandler,
): void {
let psh = this.particleScopeHandlers.get(particleId);
if (psh === undefined) {
psh = new Map<string, GenericCallServiceHandler>();
this.particleScopeHandlers.set(particleId, psh);
}
psh.set(serviceFnKey(serviceId, fnName), handler);
}
}
function serviceFnKey(serviceId: string, fnName: string) {
return `${serviceId}/${fnName}`;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 Fluence Labs Limited
* Copyright 2023 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.
@ -13,9 +13,54 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { PeerIdB58 } from '@fluencelabs/interfaces';
import type { SecurityTetraplet } from '@fluencelabs/avm';
import { JSONValue } from '../util/commonTypes.js';
/**
* JS Service host a low level interface for managing pure javascript services.
* It operates on a notion of Call Service Handlers - functions which are called when a `call` air instruction is executed on the local peer.
*/
export interface IJsServiceHost {
/**
* Returns true if any handler for the specified serviceId is registered
*/
hasService(serviceId: string): boolean;
/**
* Find call service handler for specified particle
* @param serviceId Service ID as specified in `call` air instruction
* @param fnName Function name as specified in `call` air instruction
* @param particleId Particle ID
*/
getHandler(serviceId: string, fnName: string, particleId: string): GenericCallServiceHandler | null;
/**
* Execute service call for specified call service data
*/
callService(req: CallServiceData): Promise<CallServiceResult | null>;
/**
* Register handler for all particles
*/
registerGlobalHandler(serviceId: string, fnName: string, handler: GenericCallServiceHandler): void;
/**
* Register handler which will be called only for particle with the specific id
*/
registerParticleScopeHandler(
particleId: string,
serviceId: string,
fnName: string,
handler: GenericCallServiceHandler,
): void;
/**
* Removes all handlers associated with the specified particle scope
* @param particleId Particle ID to remove handlers for
*/
removeParticleScopeHandlers(particleId: string): void;
}
export enum ResultCodes {
success = 0,
@ -106,7 +151,3 @@ export interface CallServiceResult {
*/
result: CallServiceResultType;
}
export type JSONValue = string | number | boolean | null | { [x: string]: JSONValue } | Array<JSONValue>;
export type JSONArray = Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };

View File

@ -0,0 +1,60 @@
/*
* Copyright 2023 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.
*/
import { FluencePeer } from '../jsPeer/FluencePeer.js';
import { IParticle } from '../particle/interfaces.js';
import { builtInServices } from '../services/builtins.js';
import {
CallServiceData,
CallServiceResult,
CallServiceResultType,
ParticleContext,
ResultCodes,
} from './interfaces.js';
export const doNothing = (..._args: Array<unknown>) => undefined;
export const WrapFnIntoServiceCall =
(fn: (args: any[]) => CallServiceResultType) =>
(req: CallServiceData): CallServiceResult => ({
retCode: ResultCodes.success,
result: fn(req.args),
});
export class ServiceError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, ServiceError.prototype);
}
}
export const getParticleContext = (particle: IParticle): ParticleContext => {
return {
particleId: particle.id,
initPeerId: particle.initPeerId,
timestamp: particle.timestamp,
ttl: particle.ttl,
signature: particle.signature,
};
};
export function registerDefaultServices(peer: FluencePeer) {
Object.entries(builtInServices).forEach(([serviceId, service]) => {
Object.entries(service).forEach(([fnName, fn]) => {
peer.internals.regHandler.common(serviceId, fnName, fn);
});
});
}

View File

@ -3,14 +3,14 @@ import { it, describe, expect, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as url from 'url';
import * as path from 'path';
import { compileAqua, withPeer } from '../util.js';
import { compileAqua, withPeer } from '../../util/testUtils.js';
let aqua: any;
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
describe('Marine js tests', () => {
beforeAll(async () => {
const pathToAquaFiles = path.join(__dirname, '../../../../aqua_test/marine-js.aqua');
const pathToAquaFiles = path.join(__dirname, '../../../aqua_test/marine-js.aqua');
const { services, functions } = await compileAqua(pathToAquaFiles);
aqua = functions;
});
@ -18,7 +18,7 @@ describe('Marine js tests', () => {
it('should call marine service correctly', async () => {
await withPeer(async (peer) => {
// arrange
const wasm = await fs.promises.readFile(path.join(__dirname, '../data/greeting.wasm'));
const wasm = await fs.promises.readFile(path.join(__dirname, '../../../data_for_test/greeting.wasm'));
await peer.registerMarineService(wasm, 'greeting');
// act

View File

@ -1,10 +1,25 @@
/*
* Copyright 2023 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.
*/
// @ts-ignore
import { BlobWorker } from 'threads';
import { fromBase64, toUint8Array } from 'js-base64';
// @ts-ignore
import type { WorkerImplementation } from 'threads/dist/types/master';
import { LazyLoader } from '../../interfaces/index.js';
import { Buffer } from 'buffer';
import { LazyLoader } from '../interfaces.js';
export class InlinedWorkerLoader extends LazyLoader<WorkerImplementation> {
constructor(b64script: string) {

View File

@ -1,5 +1,19 @@
/*
* Copyright 2023 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.
*/
import { createRequire } from 'module';
import { LazyLoader } from '../../interfaces/index.js';
// @ts-ignore
import type { WorkerImplementation } from 'threads/dist/types/master';
@ -8,6 +22,7 @@ import { Worker } from 'threads';
import { Buffer } from 'buffer';
import * as fs from 'fs';
import * as path from 'path';
import { LazyLoader } from '../interfaces.js';
const require = createRequire(import.meta.url);

View File

@ -1,5 +1,22 @@
/*
* Copyright 2023 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.
*/
import { Buffer } from 'buffer';
import { LazyLoader } from '../../interfaces/index.js';
import { LazyLoader } from '../interfaces.js';
// @ts-ignore
import type { WorkerImplementation } from 'threads/dist/types/master';
const bufferToSharedArrayBuffer = (buffer: Buffer): SharedArrayBuffer => {
const sab = new SharedArrayBuffer(buffer.length);
@ -17,7 +34,7 @@ const bufferToSharedArrayBuffer = (buffer: Buffer): SharedArrayBuffer => {
* @param filePath - path to the wasm file relative to current origin
* @returns Either SharedArrayBuffer or Buffer with the wasm file
*/
export const loadWasmFromServer = async (filePath: string): Promise<SharedArrayBuffer | Buffer> => {
export const loadWasmFromUrl = async (filePath: string): Promise<SharedArrayBuffer | Buffer> => {
const fullUrl = window.location.origin + '/' + filePath;
const res = await fetch(fullUrl);
const ab = await res.arrayBuffer();
@ -33,8 +50,14 @@ export const loadWasmFromServer = async (filePath: string): Promise<SharedArrayB
return buffer;
};
export class WebLoaderFromUrl extends LazyLoader<SharedArrayBuffer | Buffer> {
export class WasmLoaderFromUrl extends LazyLoader<SharedArrayBuffer | Buffer> {
constructor(filePath: string) {
super(() => loadWasmFromServer(filePath));
super(() => loadWasmFromUrl(filePath));
}
}
export class WorkerLoaderFromUrl extends LazyLoader<WorkerImplementation> {
constructor(scriptPath: string) {
super(() => new Worker(scriptPath));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 Fluence Labs Limited
* Copyright 2023 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.
@ -13,28 +13,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { PeerIdB58 } from '@fluencelabs/interfaces';
import type { JSONArray, JSONObject, LogLevel } from '@fluencelabs/marine-js/dist/types';
import type { RunParameters, CallResultsArray, InterpreterResult } from '@fluencelabs/avm';
import { CallResultsArray, InterpreterResult, RunParameters } from '@fluencelabs/avm';
import { IStartable, JSONArray, JSONObject } from '../util/commonTypes.js';
import { Buffer } from 'buffer';
// @ts-ignore
import type { WorkerImplementation } from 'threads/dist/types/master';
export type ParticleHandler = (particle: string) => void;
/**
* Base class for connectivity layer to Fluence Network
* Contract for marine host implementations. Marine host is responsible for creating calling and removing marine services
*/
export abstract class FluenceConnection {
abstract readonly relayPeerId: PeerIdB58 | null;
abstract connect(onIncomingParticle: ParticleHandler): Promise<void>;
abstract disconnect(): Promise<void>;
abstract sendParticle(nextPeerIds: PeerIdB58[], particle: string): Promise<void>;
}
export interface IMarine extends IModule {
export interface IMarineHost extends IStartable {
/**
* Creates marine service from the given module and service id
*/
createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string): Promise<void>;
/**
* Removes marine service with the given service id
*/
removeService(serviceId: string): void;
/**
* Returns true if any service with the specified service id is registered
*/
hasService(serviceId: string): boolean;
/**
* Calls the specified function of the specified service with the given arguments
*/
callService(
serviceId: string,
functionName: string,
@ -43,7 +49,13 @@ export interface IMarine extends IModule {
): Promise<unknown>;
}
export interface IAvmRunner extends IModule {
/**
* Interface for different implementations of AVM runner
*/
export interface IAvmRunner extends IStartable {
/**
* Run AVM interpreter with the specified parameters
*/
run(
runParams: RunParameters,
air: string,
@ -53,20 +65,27 @@ export interface IAvmRunner extends IModule {
): Promise<InterpreterResult | Error>;
}
export interface IModule {
start(): Promise<void>;
stop(): Promise<void>;
}
/**
* Interface for something which can hold a value
*/
export interface IValueLoader<T> {
getValue(): T;
}
export interface IWasmLoader extends IValueLoader<SharedArrayBuffer | Buffer>, IModule {}
/**
* Interface for something which can load wasm files
*/
export interface IWasmLoader extends IValueLoader<SharedArrayBuffer | Buffer>, IStartable {}
export interface IWorkerLoader extends IValueLoader<WorkerImplementation>, IModule {}
/**
* Interface for something which can thread.js based worker
*/
export interface IWorkerLoader extends IValueLoader<WorkerImplementation>, IStartable {}
export class LazyLoader<T> implements IModule, IValueLoader<T> {
/**
* Lazy loader for some value. Value is loaded only when `start` method is called
*/
export class LazyLoader<T> implements IStartable, IValueLoader<T> {
private value: T | null = null;
constructor(private loadValue: () => Promise<T> | T) {}

View File

@ -17,6 +17,7 @@
import { MarineService } from '@fluencelabs/marine-js/dist/MarineService';
import type { Env, MarineServiceConfig } from '@fluencelabs/marine-js/dist/config';
import type { JSONArray, JSONObject, LogMessage } from '@fluencelabs/marine-js/dist/types';
import { Buffer } from 'buffer';
// @ts-ignore
import { Observable, Subject } from 'threads/observable';
// @ts-ignore

View File

@ -1,9 +1,23 @@
import { LazyLoader } from '../../interfaces/index.js';
/*
* Copyright 2023 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.
*/
// @ts-ignore
import type { WorkerImplementation } from 'threads/dist/types/master';
// @ts-ignore
import { Worker } from 'threads';
import { LazyLoader } from '../interfaces.js';
export class WorkerLoader extends LazyLoader<WorkerImplementation> {
constructor() {

View File

@ -14,29 +14,40 @@
* limitations under the License.
*/
import type { JSONArray, JSONObject, LogLevel } from '@fluencelabs/marine-js/dist/types';
import type { JSONArray, JSONObject } from '@fluencelabs/marine-js/dist/types';
import { LogFunction, logLevelToEnv } from '@fluencelabs/marine-js/dist/types';
import type { IMarine, IWorkerLoader, IWasmLoader } from '../../interfaces/index.js';
import type { MarineBackgroundInterface } from '../worker-script/index.js';
// @ts-ignore
import { spawn, Thread } from 'threads';
// @ts-ignore
import type { ModuleThread } from 'threads';
import { Buffer } from 'buffer';
import { MarineLogger, marineLogger } from '../../util/logger.js';
import { IMarineHost, IWasmLoader, IWorkerLoader } from '../interfaces.js';
export class MarineBackgroundRunner implements IMarine {
export class MarineBackgroundRunner implements IMarineHost {
private marineServices = new Set<string>();
private workerThread?: ModuleThread<MarineBackgroundInterface>;
private loggers: Map<string, MarineLogger> = new Map();
constructor(private workerLoader: IWorkerLoader, private controlModuleLoader: IWasmLoader) {}
hasService(serviceId: string): boolean {
return this.marineServices.has(serviceId);
}
removeService(serviceId: string): void {
this.marineServices.delete(serviceId);
}
async start(): Promise<void> {
if (this.workerThread) {
return;
}
this.marineServices = new Set();
await this.workerLoader.start();
await this.controlModuleLoader.start();
const worker = this.workerLoader.getValue();
@ -53,7 +64,7 @@ export class MarineBackgroundRunner implements IMarine {
await this.workerThread.init(wasm);
}
createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string): Promise<void> {
async createService(serviceModule: SharedArrayBuffer | Buffer, serviceId: string): Promise<void> {
if (!this.workerThread) {
throw 'Worker is not initialized';
}
@ -62,7 +73,8 @@ export class MarineBackgroundRunner implements IMarine {
// We enable all possible log levels passing the control for exact printouts to the logger
const env = logLevelToEnv('trace');
this.loggers.set(serviceId, marineLogger(serviceId));
return this.workerThread.createService(serviceModule, serviceId, undefined, env);
await this.workerThread.createService(serviceModule, serviceId, undefined, env);
this.marineServices.add(serviceId);
}
callService(
@ -83,6 +95,7 @@ export class MarineBackgroundRunner implements IMarine {
return;
}
this.marineServices.clear();
await this.workerThread.terminate();
await Thread.terminate(this.workerThread);
}

View File

@ -0,0 +1,129 @@
/*
* 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.
*/
import { fromUint8Array, toUint8Array } from 'js-base64';
import { CallResultsArray } from '@fluencelabs/avm';
import { v4 as uuidv4 } from 'uuid';
import { Buffer } from 'buffer';
import { IParticle } from './interfaces.js';
export class Particle implements IParticle {
readonly signature: undefined;
constructor(
public readonly id: string,
public readonly timestamp: number,
public readonly script: string,
public readonly data: Uint8Array,
public readonly ttl: number,
public readonly initPeerId: string,
) {
this.signature = undefined;
}
static createNew(script: string, initPeerId: string, ttl: number): Particle {
return new Particle(uuidv4(), Date.now(), script, Buffer.from([]), ttl, initPeerId);
}
static fromString(str: string): Particle {
const json = JSON.parse(str);
const res = new Particle(
json.id,
json.timestamp,
json.script,
toUint8Array(json.data),
json.ttl,
json.init_peer_id,
);
return res;
}
}
/**
* Returns actual ttl of a particle, i.e. ttl - time passed since particle creation
*/
export const getActualTTL = (particle: IParticle): number => {
return particle.timestamp + particle.ttl - Date.now();
};
/**
* Returns true if particle has expired
*/
export const hasExpired = (particle: IParticle): boolean => {
return getActualTTL(particle) <= 0;
};
/**
* Creates a particle clone with new data
*/
export const cloneWithNewData = (particle: IParticle, newData: Uint8Array): IParticle => {
return new Particle(particle.id, particle.timestamp, particle.script, newData, particle.ttl, particle.initPeerId);
};
/**
* Creates a deep copy of a particle
*/
export const fullClone = (particle: IParticle): IParticle => {
return JSON.parse(JSON.stringify(particle));
};
/**
* Serializes particle into string suitable for sending through network
*/
export const serializeToString = (particle: IParticle): string => {
return JSON.stringify({
action: 'Particle',
id: particle.id,
init_peer_id: particle.initPeerId,
timestamp: particle.timestamp,
ttl: particle.ttl,
script: particle.script,
// TODO: copy signature from a particle after signatures will be implemented on nodes
signature: [],
data: particle.data && fromUint8Array(particle.data),
});
};
/**
* When particle is executed, it goes through different stages. The type describes all possible stages and their parameters
*/
export type ParticleExecutionStage =
| { stage: 'received' }
| { stage: 'interpreted' }
| { stage: 'interpreterError'; errorMessage: string }
| { stage: 'localWorkDone' }
| { stage: 'sent' }
| { stage: 'sendingError'; errorMessage: string }
| { stage: 'expired' };
/**
* Particle queue item is a wrapper around particle, which contains additional information about particle execution
*/
export interface ParticleQueueItem {
particle: IParticle;
callResults: CallResultsArray;
onStageChange: (state: ParticleExecutionStage) => void;
}
/**
* Helper function to handle particle at expired stage
*/
export const handleTimeout = (fn: () => void) => (stage: ParticleExecutionStage) => {
if (stage.stage === 'expired') {
fn();
}
};

View File

@ -0,0 +1,59 @@
/*
* Copyright 2023 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.
*/
import { PeerIdB58 } from '@fluencelabs/interfaces';
/**
* Immutable part of the particle.
*/
export interface IImmutableParticlePart {
/**
* Particle id
*/
readonly id: string;
/**
* Particle timestamp. Specifies when the particle was created.
*/
readonly timestamp: number;
/**
* Particle's air script
*/
readonly script: string;
/**
* Particle's ttl. Specifies how long the particle is valid in milliseconds.
*/
readonly ttl: number;
/**
* Peer id where the particle was initiated.
*/
readonly initPeerId: PeerIdB58;
// TODO: implement particle signatures
readonly signature: undefined;
}
/**
* Particle is a data structure that is used to transfer data between peers in Fluence network.
*/
export interface IParticle extends IImmutableParticlePart {
/**
* Mutable particle data
*/
data: Uint8Array;
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2023 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.
*/
import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces';
import { defaultGuard } from './SingleModuleSrv.js';
import { NodeUtilsDef, registerNodeUtils } from './_aqua/node-utils.js';
import { SecurityGuard } from './securityGuard.js';
import { readFile } from 'fs/promises';
import { FluencePeer } from '../jsPeer/FluencePeer.js';
export class NodeUtils implements NodeUtilsDef {
constructor(private peer: FluencePeer) {
this.securityGuard_readFile = defaultGuard(this.peer);
}
securityGuard_readFile: SecurityGuard<'path'>;
async read_file(path: string, callParams: CallParams<'path'>) {
if (!this.securityGuard_readFile(callParams)) {
return {
success: false,
error: 'Security guard validation failed',
content: null,
};
}
try {
// Strange enough, but Buffer type works here, while reading with encoding 'utf-8' doesn't
const data: any = await readFile(path);
return {
success: true,
content: data,
error: null,
};
} catch (err: any) {
return {
success: false,
error: err.message,
content: null,
};
}
}
}
// HACK:: security guard functions must be ported to user API
export const doRegisterNodeUtils = (peer: any) => {
registerNodeUtils(peer, 'node_utils', new NodeUtils(peer));
};

View File

@ -1,6 +1,23 @@
/*
* Copyright 2023 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.
*/
import { CallParams, PeerIdB58 } from '@fluencelabs/interfaces';
import { KeyPair } from '../../keypair/index.js';
import { SigDef } from '../_aqua/services.js';
import { KeyPair } from '../keypair/index.js';
import { FluencePeer } from '../jsPeer/FluencePeer.js';
import { SigDef } from './_aqua/services.js';
import { allowOnlyParticleOriginatedAt, allowServiceFn, and, or, SecurityGuard } from './securityGuard.js';
export const defaultSigGuard = (peerId: PeerIdB58) => {
@ -18,11 +35,7 @@ export const defaultSigGuard = (peerId: PeerIdB58) => {
};
export class Sig implements SigDef {
private _keyPair: KeyPair;
constructor(keyPair: KeyPair) {
this._keyPair = keyPair;
}
constructor(private keyPair: KeyPair) {}
/**
* Configurable security guard for sign method
@ -35,7 +48,7 @@ export class Sig implements SigDef {
* Gets the public key of KeyPair. Required by aqua
*/
get_peer_id() {
return this._keyPair.getPeerId();
return this.keyPair.getPeerId();
}
/**
@ -53,7 +66,7 @@ export class Sig implements SigDef {
};
}
const signedData = await this._keyPair.signBytes(Uint8Array.from(data));
const signedData = await this.keyPair.signBytes(Uint8Array.from(data));
return {
success: true,
@ -66,6 +79,10 @@ export class Sig implements SigDef {
* Verifies the signature. Required by aqua
*/
verify(signature: number[], data: number[]): Promise<boolean> {
return this._keyPair.verify(Uint8Array.from(data), Uint8Array.from(signature));
return this.keyPair.verify(Uint8Array.from(data), Uint8Array.from(signature));
}
}
export const getDefaultSig = (peer: FluencePeer) => {
peer.registerMarineService;
};

View File

@ -1,13 +1,28 @@
/*
* Copyright 2023 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.
*/
import { v4 as uuidv4 } from 'uuid';
import { SrvDef } from '../_aqua/single-module-srv.js';
import { NodeUtilsDef } from '../_aqua/node-utils.js';
import { FluencePeer } from '../FluencePeer.js';
import { SrvDef } from './_aqua/single-module-srv.js';
import { FluencePeer } from '../jsPeer/FluencePeer.js';
import { CallParams } from '@fluencelabs/interfaces';
import { Buffer } from 'buffer';
import { allowOnlyParticleOriginatedAt, SecurityGuard } from './securityGuard.js';
export const defaultGuard = (peer: FluencePeer) => {
return allowOnlyParticleOriginatedAt<any>(peer.getStatus().peerId!);
return allowOnlyParticleOriginatedAt<any>(peer.keyPair.getPeerId());
};
export class Srv implements SrvDef {
@ -32,10 +47,11 @@ export class Srv implements SrvDef {
try {
const newServiceId = uuidv4();
const buffer = Buffer.from(wasm_b64_content, 'base64');
const sab = new SharedArrayBuffer(buffer.length);
const tmp = new Uint8Array(sab);
tmp.set(buffer, 0);
await this.peer.registerMarineService(sab, newServiceId);
// TODO:: figure out why SharedArrayBuffer is not working here
// const sab = new SharedArrayBuffer(buffer.length);
// const tmp = new Uint8Array(sab);
// tmp.set(buffer, 0);
await this.peer.registerMarineService(buffer, newServiceId);
this.services.add(newServiceId);
return {
@ -83,49 +99,3 @@ export class Srv implements SrvDef {
return Array.from(this.services.values());
}
}
export class NodeUtils implements NodeUtilsDef {
constructor(private peer: FluencePeer) {
this.securityGuard_readFile = defaultGuard(this.peer);
}
securityGuard_readFile: SecurityGuard<'path'>;
async read_file(path: string, callParams: CallParams<'path'>) {
// TODO: split node-only and universal services into different client packages
// if (!isNode) {
// return {
// success: false,
// error: 'read_file is only supported in node.js',
// content: null,
// };
// }
if (!this.securityGuard_readFile(callParams)) {
return {
success: false,
error: 'Security guard validation failed',
content: null,
};
}
try {
// eval('require') is needed so that
// webpack will complain about missing dependencies for web target
const r = eval('require');
const fs = r('fs').promises;
const data = await fs.readFile(path);
return {
success: true,
content: data,
error: null,
};
} catch (err: any) {
return {
success: false,
error: err.message,
content: null,
};
}
}
}

View File

@ -2,11 +2,11 @@ import { it, describe, expect, test } from 'vitest';
import { CallParams } from '@fluencelabs/interfaces';
import { toUint8Array } from 'js-base64';
import { CallServiceData } from '../../../interfaces/commonTypes.js';
import { KeyPair } from '../../../keypair/index.js';
import { Sig, defaultSigGuard } from '../../builtins/Sig.js';
import { allowServiceFn } from '../../builtins/securityGuard.js';
import { builtInServices } from '../../builtins/common.js';
import { KeyPair } from '../../keypair/index.js';
import { Sig, defaultSigGuard } from '../Sig.js';
import { allowServiceFn } from '../securityGuard.js';
import { builtInServices } from '../builtins.js';
import { CallServiceData } from '../../jsServiceHost/interfaces.js';
const a10b20 = `{
"a": 10,

View File

@ -1,9 +1,9 @@
import { it, describe, expect, beforeEach, afterEach } from 'vitest';
import { Particle } from '../../Particle.js';
import { doNothing } from '../../utils.js';
import { FluencePeer } from '../../FluencePeer.js';
import { mkTestPeer } from '../util.js';
import { Particle } from '../../particle/Particle.js';
import { FluencePeer } from '../../jsPeer/FluencePeer.js';
import { mkTestPeer } from '../../util/testUtils.js';
import { doNothing } from '../../jsServiceHost/serviceUtils.js';
let peer: FluencePeer;
@ -15,7 +15,7 @@ describe('Sig service test suite', () => {
});
beforeEach(async () => {
peer = mkTestPeer();
peer = await mkTestPeer();
await peer.start();
});
@ -56,7 +56,7 @@ describe('Sig service test suite', () => {
};
});
});
const p = peer.internals.createNewParticle(script) as Particle;
const p = peer.internals.createNewParticle(script);
await peer.internals.initiateParticle(p, doNothing);
const [nestedFirst, nestedSecond, outerFirst, outerSecond, outerFirstString, outerFirstParsed] = await promise;

View File

@ -2,11 +2,11 @@ import { it, describe, expect, beforeAll } from 'vitest';
import * as path from 'path';
import * as url from 'url';
import { KeyPair } from '../../../keypair/index.js';
import { allowServiceFn } from '../../builtins/securityGuard.js';
import { Sig } from '../../builtins/Sig.js';
import { compileAqua, withPeer } from '../util.js';
import { registerService } from '../../../compilerSupport/registerService.js';
import { KeyPair } from '../../keypair/index.js';
import { allowServiceFn } from '../securityGuard.js';
import { Sig } from '../Sig.js';
import { registerService } from '../../compilerSupport/registerService.js';
import { compileAqua, withPeer } from '../../util/testUtils.js';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
@ -16,7 +16,7 @@ let dataProviderDef: any;
describe('Sig service test suite', () => {
beforeAll(async () => {
const pathToAquaFiles = path.join(__dirname, '../../../../aqua_test/sigService.aqua');
const pathToAquaFiles = path.join(__dirname, '../../../aqua_test/sigService.aqua');
const { services, functions } = await compileAqua(pathToAquaFiles);
aqua = functions;
@ -75,12 +75,13 @@ describe('Sig service test suite', () => {
customSig.securityGuard = allowServiceFn('wrong', 'wrong');
const result = await aqua.callSig(peer, { sigId: 'CustomSig' });
expect(result.success).toBe(false);
});
});
it('Default sig service should be resolvable by peer id', async () => {
await withPeer(async (peer) => {
const sig = peer.getServices().sig;
const sig = peer.internals.getServices().sig;
const data = [1, 2, 3, 4, 5];
registerService({
@ -95,7 +96,7 @@ describe('Sig service test suite', () => {
});
const callAsSigRes = await aqua.callSig(peer, { sigId: 'sig' });
const callAsPeerIdRes = await aqua.callSig(peer, { sigId: peer.getStatus().peerId });
const callAsPeerIdRes = await aqua.callSig(peer, { sigId: peer.keyPair.getPeerId() });
expect(callAsSigRes.success).toBe(false);
expect(callAsPeerIdRes.success).toBe(false);
@ -104,7 +105,7 @@ describe('Sig service test suite', () => {
const callAsSigResAfterGuardChange = await aqua.callSig(peer, { sigId: 'sig' });
const callAsPeerIdResAfterGuardChange = await aqua.callSig(peer, {
sigId: peer.getStatus().peerId,
sigId: peer.keyPair.getPeerId(),
});
expect(callAsSigResAfterGuardChange.success).toBe(true);

View File

@ -1,14 +1,16 @@
import { it, describe, expect, beforeAll } from 'vitest';
import * as path from 'path';
import * as url from 'url';
import { compileAqua, withPeer } from '../util.js';
import { compileAqua, withPeer } from '../../util/testUtils.js';
import { registerNodeUtils } from '../_aqua/node-utils.js';
import { NodeUtils } from '../NodeUtils.js';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
let aqua: any;
describe('Srv service test suite', () => {
beforeAll(async () => {
const pathToAquaFiles = path.join(__dirname, '../../../../aqua_test/srv.aqua');
const pathToAquaFiles = path.join(__dirname, '../../../aqua_test/srv.aqua');
const { services, functions } = await compileAqua(pathToAquaFiles);
aqua = functions;
});
@ -16,7 +18,8 @@ describe('Srv service test suite', () => {
it('Use custom srv service, success path', async () => {
await withPeer(async (peer) => {
// arrange
const wasm = path.join(__dirname, '../data/greeting.wasm');
registerNodeUtils(peer, 'node_utils', new NodeUtils(peer));
const wasm = path.join(__dirname, '../../../data_for_test/greeting.wasm');
// act
const res = await aqua.happy_path(peer, { file_path: wasm });
@ -29,7 +32,8 @@ describe('Srv service test suite', () => {
it('List deployed services', async () => {
await withPeer(async (peer) => {
// arrange
const wasm = path.join(__dirname, '../data/greeting.wasm');
registerNodeUtils(peer, 'node_utils', new NodeUtils(peer));
const wasm = path.join(__dirname, '../../../data_for_test/greeting.wasm');
// act
const res = await aqua.list_services(peer, { file_path: wasm });
@ -42,19 +46,21 @@ describe('Srv service test suite', () => {
it('Correct error for removed services', async () => {
await withPeer(async (peer) => {
// arrange
const wasm = path.join(__dirname, '../data/greeting.wasm');
registerNodeUtils(peer, 'node_utils', new NodeUtils(peer));
const wasm = path.join(__dirname, '../../../data_for_test/greeting.wasm');
// act
const res = await aqua.service_removed(peer, { file_path: wasm });
// assert
expect(res).toMatch('No handler has been registered for serviceId');
expect(res).toMatch('No service found for service call');
});
});
it('Correct error for file not found', async () => {
await withPeer(async (peer) => {
// arrange
registerNodeUtils(peer, 'node_utils', new NodeUtils(peer));
// act
const res = await aqua.file_not_found(peer, {});
@ -67,6 +73,7 @@ describe('Srv service test suite', () => {
it('Correct error for removing non existing service', async () => {
await withPeer(async (peer) => {
// arrange
registerNodeUtils(peer, 'node_utils', new NodeUtils(peer));
// act
const res = await aqua.removing_non_exiting(peer, {});

View File

@ -6,9 +6,8 @@
* Aqua version: 0.7.7-362
*
*/
import { CallParams } from '@fluencelabs/interfaces';
import { registerServiceImpl } from './util.js';
import { FluencePeer } from '../FluencePeer.js';
import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces';
import { registerService } from '../../compilerSupport/registerService.js';
// Services
@ -21,10 +20,12 @@ export interface NodeUtilsDef {
| Promise<{ content: string | null; error: string | null; success: boolean }>;
}
export function registerNodeUtils(peer: FluencePeer, serviceId: string, service: any) {
registerServiceImpl(
peer,
{
export function registerNodeUtils(peer: IFluenceInternalApi, serviceId: string, service: any) {
registerService({
peer: peer,
service: service,
serviceId: serviceId,
def: {
defaultServiceId: 'node_utils',
functions: {
tag: 'labeledProduct',
@ -73,9 +74,7 @@ export function registerNodeUtils(peer: FluencePeer, serviceId: string, service:
},
},
},
serviceId,
service,
);
});
}
// Functions

View File

@ -6,9 +6,8 @@
* Aqua version: 0.7.7-362
*
*/
import { CallParams } from '@fluencelabs/interfaces';
import { registerServiceImpl } from './util.js';
import { FluencePeer } from '../FluencePeer.js';
import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces';
import { registerService } from '../../compilerSupport/registerService.js';
// Services
@ -27,10 +26,12 @@ export interface SigDef {
) => boolean | Promise<boolean>;
}
export function registerSig(peer: FluencePeer, serviceId: string, service: any) {
registerServiceImpl(
peer,
{
export function registerSig(peer: IFluenceInternalApi, serviceId: string, service: any) {
registerService({
peer: peer as any,
service: service,
serviceId: serviceId,
def: {
defaultServiceId: 'sig',
functions: {
tag: 'labeledProduct',
@ -131,9 +132,7 @@ export function registerSig(peer: FluencePeer, serviceId: string, service: any)
},
},
},
serviceId,
service,
);
});
}
// Functions

View File

@ -6,9 +6,8 @@
* Aqua version: 0.7.7-362
*
*/
import { CallParams } from '@fluencelabs/interfaces';
import { registerServiceImpl } from './util.js';
import { FluencePeer } from '../FluencePeer.js';
import { CallParams, IFluenceInternalApi } from '@fluencelabs/interfaces';
import { registerService } from '../../compilerSupport/registerService.js';
// Services
@ -26,10 +25,12 @@ export interface SrvDef {
) => { error: string | null; success: boolean } | Promise<{ error: string | null; success: boolean }>;
}
export function registerSrv(peer: FluencePeer, serviceId: string, service: any) {
registerServiceImpl(
peer,
{
export function registerSrv(peer: IFluenceInternalApi, serviceId: string, service: any) {
registerService({
peer: peer as any,
serviceId,
service,
def: {
defaultServiceId: 'single_module_srv',
functions: {
tag: 'labeledProduct',
@ -130,9 +131,7 @@ export function registerSrv(peer: FluencePeer, serviceId: string, service: any)
},
},
},
serviceId,
service,
);
});
}
// Functions

View File

@ -19,9 +19,9 @@ import * as bs58 from 'bs58';
import { sha256 } from 'multiformats/hashes/sha2';
import { CallServiceResult } from '@fluencelabs/avm';
import { GenericCallServiceHandler, ResultCodes } from '../../interfaces/commonTypes.js';
import { jsonify } from '../utils.js';
import { isString, jsonify } from '../util/utils.js';
import { Buffer } from 'buffer';
import { GenericCallServiceHandler, ResultCodes } from '../jsServiceHost/interfaces.js';
//@ts-ignore
const { encode, decode } = bs58.default;
@ -595,11 +595,3 @@ const checkForArgumentType = (req: { args: Array<unknown> }, index: number, type
return error(`Argument ${index} expected to be of type ${type}, Got ${actual}`);
}
};
export const isString = (unknown: unknown): unknown is string => {
return unknown !== null && typeof unknown === 'string';
};
export const isObject = (unknown: unknown): unknown is object => {
return unknown !== null && typeof unknown === 'object';
};

View File

@ -1,3 +1,19 @@
/*
* Copyright 2023 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.
*/
import { SecurityTetraplet } from '@fluencelabs/avm';
import { CallParams, PeerIdB58 } from '@fluencelabs/interfaces';

View File

@ -0,0 +1,24 @@
/*
* Copyright 2023 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 IStartable {
start(): Promise<void>;
stop(): Promise<void>;
}
export type JSONValue = string | number | boolean | null | { [x: string]: JSONValue } | Array<JSONValue>;
export type JSONArray = Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue };

View File

@ -0,0 +1,35 @@
/*
* Copyright 2023 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.
*/
import { RelayOptions } from '@fluencelabs/interfaces';
import { multiaddr, Multiaddr } from '@multiformats/multiaddr';
import { isString } from './utils.js';
export function relayOptionToMultiaddr(relay: RelayOptions): Multiaddr {
const multiaddrString = isString(relay) ? relay : relay.multiaddr;
const ma = multiaddr(multiaddrString);
throwIfHasNoPeerId(ma);
return ma;
}
export function throwIfHasNoPeerId(ma: Multiaddr): void {
const peerId = ma.getPeerId();
if (!peerId) {
throw new Error('Specified multiaddr is invalid or missing peer id: ' + ma.toString());
}
}

View File

@ -1,5 +1,21 @@
/*
* Copyright 2023 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.
*/
import debug from 'debug';
import { Particle } from '../js-peer/Particle.js';
import { Buffer } from 'buffer';
// Format avm data as a string
debug.formatters.a = (avmData: Uint8Array) => {

View File

@ -0,0 +1,143 @@
/*
* Copyright 2023 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.
*/
import * as api from '@fluencelabs/aqua-api/aqua-api.js';
import { promises as fs } from 'fs';
import { DEFAULT_CONFIG, FluencePeer, PeerConfig } from '../jsPeer/FluencePeer.js';
import { Particle } from '../particle/Particle.js';
import { ClientConfig, IFluenceClient, RelayOptions, ServiceDef } from '@fluencelabs/interfaces';
import { callAquaFunction } from '../compilerSupport/callFunction.js';
import { MarineBackgroundRunner } from '../marine/worker/index.js';
import { MarineBasedAvmRunner } from '../jsPeer/avm.js';
import { WorkerLoader } from '../marine/worker-script/workerLoader.js';
import { KeyPair } from '../keypair/index.js';
import { Subject, Subscribable } from 'rxjs';
import { WrapFnIntoServiceCall } from '../jsServiceHost/serviceUtils.js';
import { JsServiceHost } from '../jsServiceHost/JsServiceHost.js';
import { ClientPeer, makeClientPeerConfig } from '../clientPeer/ClientPeer.js';
import { WasmLoaderFromNpm } from '../marine/deps-loader/node.js';
import { IConnection } from '../connection/interfaces.js';
export const registerHandlersHelper = (
peer: FluencePeer,
particle: Particle,
handlers: Record<string, Record<string, any>>,
) => {
Object.entries(handlers).forEach(([serviceId, service]) => {
Object.entries(service).forEach(([fnName, fn]) => {
peer.internals.regHandler.forParticle(particle.id, serviceId, fnName, WrapFnIntoServiceCall(fn));
});
});
};
export type CompiledFnCall = (peer: IFluenceClient, args: { [key: string]: any }) => Promise<unknown>;
export type CompiledFile = {
functions: { [key: string]: CompiledFnCall };
services: { [key: string]: ServiceDef };
};
export const compileAqua = async (aquaFile: string): Promise<CompiledFile> => {
await fs.access(aquaFile);
const compilationResult = await api.Aqua.compile(new api.Path(aquaFile), [], undefined);
if (compilationResult.errors.length > 0) {
throw new Error('Aqua compilation failed. Error: ' + compilationResult.errors.join('/n'));
}
const functions = Object.entries(compilationResult.functions)
.map(([name, fnInfo]) => {
const callFn = (peer: IFluenceClient, args: { [key: string]: any }) => {
return callAquaFunction({
def: fnInfo.funcDef,
script: fnInfo.script,
config: {},
peer: peer,
args,
});
};
return { [name]: callFn };
})
.reduce((agg, obj) => {
return { ...agg, ...obj };
}, {});
return { functions, services: compilationResult.services };
};
class NoopConnection implements IConnection {
getRelayPeerId(): string {
return 'nothing_here';
}
supportsRelay(): boolean {
return true;
}
particleSource: Subscribable<Particle> = new Subject<Particle>();
sendParticle(nextPeerIds: string[], particle: Particle): Promise<void> {
return Promise.resolve();
}
}
export class TestPeer extends FluencePeer {
constructor(keyPair: KeyPair, connection: IConnection) {
const workerLoader = new WorkerLoader();
const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader);
const jsHost = new JsServiceHost();
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader);
super(DEFAULT_CONFIG, keyPair, marine, jsHost, avm, connection);
}
}
export const mkTestPeer = async () => {
const kp = await KeyPair.randomEd25519();
const conn = new NoopConnection();
return new TestPeer(kp, conn);
};
export const withPeer = async (action: (p: FluencePeer) => Promise<void>) => {
const p = await mkTestPeer();
try {
await p.start();
await action(p);
} finally {
await p.stop();
}
};
export const withClient = async (
relay: RelayOptions,
config: ClientConfig,
action: (client: ClientPeer) => Promise<void>,
) => {
const workerLoader = new WorkerLoader();
const controlModuleLoader = new WasmLoaderFromNpm('@fluencelabs/marine-js', 'marine-js.wasm');
const avmModuleLoader = new WasmLoaderFromNpm('@fluencelabs/avm', 'avm.wasm');
const marine = new MarineBackgroundRunner(workerLoader, controlModuleLoader);
const avm = new MarineBasedAvmRunner(marine, avmModuleLoader);
const { keyPair, peerConfig, relayConfig } = await makeClientPeerConfig(relay, config);
const client = new ClientPeer(peerConfig, relayConfig, keyPair, marine, avm);
try {
await client.connect();
await action(client);
} finally {
await client.disconnect();
}
};

View File

@ -0,0 +1,27 @@
/*
* Copyright 2021 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 function jsonify(obj: unknown) {
return JSON.stringify(obj, null, 4);
}
export const isString = (unknown: unknown): unknown is string => {
return unknown !== null && typeof unknown === 'string';
};
export const isObject = (unknown: unknown): unknown is object => {
return unknown !== null && typeof unknown === 'object';
};