diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 53062cdb..e66f0541 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -97,8 +97,6 @@ jobs: "@fluencelabs/marine-js": "${{ inputs.marine-js-version }}" } - - uses: browser-actions/setup-chrome@v1 - - run: pnpm -r --no-frozen-lockfile i - run: pnpm -r build - run: pnpm -r test diff --git a/.npmrc b/.npmrc index 16dde58b..735c7cc5 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,3 @@ auto-install-peers=true -save-exact=true \ No newline at end of file +save-exact=true +side-effects-cache=false \ No newline at end of file diff --git a/packages/core/js-client/src/clientPeer/__test__/client.spec.ts b/packages/core/js-client/src/clientPeer/__test__/client.spec.ts index 57a39cd3..06c52194 100644 --- a/packages/core/js-client/src/clientPeer/__test__/client.spec.ts +++ b/packages/core/js-client/src/clientPeer/__test__/client.spec.ts @@ -11,8 +11,7 @@ describe('FluenceClient usage test suite', () => { await withClient(RELAY, {}, async (peer) => { // arrange - const result = await new Promise((resolve, reject) => { - const script = ` + const script = ` (xor (seq (call %init_peer_id% ("load" "relay") [] init_relay) @@ -26,8 +25,10 @@ describe('FluenceClient usage test suite', () => { (call %init_peer_id% ("callback" "error") [%last_error%]) ) )`; - const particle = peer.internals.createNewParticle(script); + const particle = await peer.internals.createNewParticle(script); + + const result = await new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } @@ -92,7 +93,7 @@ describe('FluenceClient usage test suite', () => { (call "${peer2.getPeerId()}" ("test" "test") ["test"]) ) `; - const particle = peer1.internals.createNewParticle(script); + const particle = await peer1.internals.createNewParticle(script); if (particle instanceof Error) { throw particle; @@ -149,14 +150,13 @@ describe('FluenceClient usage test suite', () => { 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 = ` + const script = ` (xor (call "incorrect_peer_id" ("any" "service") []) (call %init_peer_id% ("callback" "error") [%last_error%]) )`; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + const promise = new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } diff --git a/packages/core/js-client/src/clientPeer/checkConnection.ts b/packages/core/js-client/src/clientPeer/checkConnection.ts index f84db715..c969be16 100644 --- a/packages/core/js-client/src/clientPeer/checkConnection.ts +++ b/packages/core/js-client/src/clientPeer/checkConnection.ts @@ -28,8 +28,7 @@ const log = logger('connection'); export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise => { const msg = Math.random().toString(36).substring(7); - const promise = new Promise((resolve, reject) => { - const script = ` + const script = ` (xor (seq (call %init_peer_id% ("load" "relay") [] init_relay) @@ -46,8 +45,9 @@ export const checkConnection = async (peer: ClientPeer, ttl?: number): Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } diff --git a/packages/core/js-client/src/compilerSupport/callFunction.ts b/packages/core/js-client/src/compilerSupport/callFunction.ts index b2855fe1..9f562aa6 100644 --- a/packages/core/js-client/src/compilerSupport/callFunction.ts +++ b/packages/core/js-client/src/compilerSupport/callFunction.ts @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getArgumentTypes, isReturnTypeVoid, CallAquaFunctionType } from '@fluencelabs/interfaces'; +import { CallAquaFunctionType, getArgumentTypes, isReturnTypeVoid } from '@fluencelabs/interfaces'; import { + errorHandlingService, injectRelayService, + injectValueService, registerParticleScopeService, responseService, - errorHandlingService, ServiceDescription, userHandlerService, - injectValueService, } from './services.js'; import { logger } from '../util/logger.js'; +import { IParticle } from '../particle/interfaces.js'; const log = logger('aqua'); @@ -40,13 +41,13 @@ 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: CallAquaFunctionType = ({ def, script, config, peer, args }) => { +export const callAquaFunction: CallAquaFunctionType = async ({ def, script, config, peer, args }) => { log.trace('calling aqua function %j', { def, script, config, args }); const argumentTypes = getArgumentTypes(def); - const promise = new Promise((resolve, reject) => { - const particle = peer.internals.createNewParticle(script, config?.ttl); + const particle = await peer.internals.createNewParticle(script, config?.ttl); + return new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } @@ -92,7 +93,5 @@ export const callAquaFunction: CallAquaFunctionType = ({ def, script, config, pe ); } }); - }); - - return promise; + }) }; diff --git a/packages/core/js-client/src/compilerSupport/services.ts b/packages/core/js-client/src/compilerSupport/services.ts index 834bbe7b..7da4c51e 100644 --- a/packages/core/js-client/src/compilerSupport/services.ts +++ b/packages/core/js-client/src/compilerSupport/services.ts @@ -28,6 +28,7 @@ import { IFluenceInternalApi, } from '@fluencelabs/interfaces'; import { CallServiceData, GenericCallServiceHandler, ResultCodes } from '../jsServiceHost/interfaces.js'; +import { fromUint8Array } from 'js-base64'; export interface ServiceDescription { serviceId: string; @@ -177,6 +178,7 @@ const extractCallParams = (req: CallServiceData, arrow: ArrowWithoutCallbacks): const callParams = { ...req.particleContext, + signature: fromUint8Array(req.particleContext.signature), tetraplets, }; diff --git a/packages/core/js-client/src/connection/RelayConnection.ts b/packages/core/js-client/src/connection/RelayConnection.ts index b98a5b58..774a1677 100644 --- a/packages/core/js-client/src/connection/RelayConnection.ts +++ b/packages/core/js-client/src/connection/RelayConnection.ts @@ -15,7 +15,7 @@ */ import { PeerIdB58 } from '@fluencelabs/interfaces'; import { pipe } from 'it-pipe'; -import { encode, decode } from 'it-length-prefixed'; +import { decode, encode } from 'it-length-prefixed'; import type { PeerId } from '@libp2p/interface/peer-id'; import { createLibp2p, Libp2p } from 'libp2p'; @@ -23,8 +23,7 @@ import { noise } from '@chainsafe/libp2p-noise'; import { yamux } from '@chainsafe/libp2p-yamux'; import { webSockets } from '@libp2p/websockets'; import { all } from '@libp2p/websockets/filters'; -import { multiaddr } from '@multiformats/multiaddr'; -import type { Multiaddr } from '@multiformats/multiaddr'; +import { multiaddr, type Multiaddr } from '@multiformats/multiaddr'; import map from 'it-map'; import { fromString } from 'uint8arrays/from-string'; @@ -35,9 +34,13 @@ 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 { Particle, serializeToString, verifySignature } from '../particle/Particle.js'; import { identifyService } from 'libp2p/identify'; import { pingService } from 'libp2p/ping'; +import { unmarshalPublicKey } from '@libp2p/crypto/keys'; +import { peerIdFromString } from '@libp2p/peer-id'; +import { Stream } from '@libp2p/interface/connection'; +import { KeyPair } from '../keypair/index.js'; const log = logger('connection'); @@ -170,6 +173,31 @@ export class RelayConnection implements IConnection { ); log.trace('data written to sink'); } + + private async processIncomingMessage(msg: string, stream: Stream) { + let particle: Particle | undefined; + try { + particle = Particle.fromString(msg); + log.trace('got particle from stream with id %s and particle id %s', stream.id, particle.id); + const initPeerId = peerIdFromString(particle.initPeerId); + + if (initPeerId.publicKey === undefined) { + log.error('cannot retrieve public key from init_peer_id. particle id: %s. init_peer_id: %s', particle.id, particle.initPeerId); + return; + } + + const isVerified = await verifySignature(particle, initPeerId.publicKey); + if (isVerified) { + this.particleSource.next(particle); + } else { + log.trace('particle signature is incorrect. rejecting particle with id: %s', particle.id); + } + } catch (e) { + const particleId = particle?.id; + const particleIdMessage = typeof particleId === 'string' ? `. particle id: ${particleId}` : ''; + log.error(`error on handling an incoming message: %O%s`, e, particleIdMessage); + } + } private async connect() { if (this.lib2p2Peer === null) { @@ -178,30 +206,20 @@ export class RelayConnection implements IConnection { await this.lib2p2Peer.handle( [PROTOCOL_NAME], - async ({ connection, stream }) => { - pipe( - stream.source, - // @ts-ignore - decode(), - // @ts-ignore - (source) => map(source, (buf) => toString(buf.subarray())), - async (source) => { - try { - for await (const msg of source) { - try { - const particle = Particle.fromString(msg); - log.trace('got particle from stream with id %s and particle id %s', stream.id, particle.id); - this.particleSource.next(particle); - } catch (e) { - log.error('error on handling a new incoming message: %j', e); - } - } - } catch (e) { - log.error('connection closed: %j', e); + async ({ connection, stream }) => pipe( + stream.source, + decode(), + (source) => map(source, (buf) => toString(buf.subarray())), + async (source) => { + try { + for await (const msg of source) { + await this.processIncomingMessage(msg, stream); } - }, - ); - }, + } catch (e) { + log.error('connection closed: %j', e); + } + }, + ), { maxInboundStreams: this.config.maxInboundStreams, maxOutboundStreams: this.config.maxOutboundStreams, diff --git a/packages/core/js-client/src/ephemeral/__test__/ephemeral.spec.ts b/packages/core/js-client/src/ephemeral/__test__/ephemeral.spec.ts index ee4d489b..c9aad47e 100644 --- a/packages/core/js-client/src/ephemeral/__test__/ephemeral.spec.ts +++ b/packages/core/js-client/src/ephemeral/__test__/ephemeral.spec.ts @@ -59,7 +59,7 @@ describe.skip('Ephemeral networks tests', () => { ) `; - const particle = client.internals.createNewParticle(script); + const particle = await client.internals.createNewParticle(script); const promise = new Promise((resolve) => { client.internals.regHandler.forParticle(particle.id, 'test', 'test', (req: CallServiceData) => { diff --git a/packages/core/js-client/src/jsPeer/FluencePeer.ts b/packages/core/js-client/src/jsPeer/FluencePeer.ts index 6208cf58..661bafe8 100644 --- a/packages/core/js-client/src/jsPeer/FluencePeer.ts +++ b/packages/core/js-client/src/jsPeer/FluencePeer.ts @@ -64,6 +64,7 @@ import { ResultCodes, } from '../jsServiceHost/interfaces.js'; import { JSONValue } from '../util/commonTypes.js'; +import { fromUint8Array } from 'js-base64'; const log_particle = logger('particle'); const log_peer = logger('peer'); @@ -217,8 +218,8 @@ export abstract class FluencePeer { } }, - createNewParticle: (script: string, ttl: number = this.config.defaultTtlMs): IParticle => { - return Particle.createNew(script, this.keyPair.getPeerId(), ttl); + createNewParticle: (script: string, ttl: number = this.config.defaultTtlMs): Promise => { + return Particle.createNew(script, this.keyPair.getPeerId(), ttl, this.keyPair); }, /** @@ -317,7 +318,7 @@ export abstract class FluencePeer { log_particle.trace('id %s. call results: %j', item.particle.id, item.callResults); }), filterExpiredParticles(this._expireParticle.bind(this)), - groupBy(item => item.particle.id), + groupBy(item => fromUint8Array(item.particle.signature)), mergeMap(group$ => { let prevData: Uint8Array = Buffer.from([]); let firstRun = true; diff --git a/packages/core/js-client/src/jsPeer/__test__/avm.spec.ts b/packages/core/js-client/src/jsPeer/__test__/avm.spec.ts index 543c3a30..548fe4c4 100644 --- a/packages/core/js-client/src/jsPeer/__test__/avm.spec.ts +++ b/packages/core/js-client/src/jsPeer/__test__/avm.spec.ts @@ -5,19 +5,19 @@ import { handleTimeout } from '../../particle/Particle.js'; describe('Basic AVM functionality in Fluence Peer tests', () => { it('Simple call', async () => { await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const script = ` + const script = ` (call %init_peer_id% ("print" "print") ["1"]) `; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + + const res = await new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } registerHandlersHelper(peer, particle, { print: { - print: (args: Array>) => { + print: (args: Array) => { const [res] = args; resolve(res); }, @@ -33,9 +33,7 @@ describe('Basic AVM functionality in Fluence Peer tests', () => { it('Par call', async () => { await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const res: any[] = []; - const script = ` + const script = ` (seq (par (call %init_peer_id% ("print" "print") ["1"]) @@ -44,7 +42,10 @@ describe('Basic AVM functionality in Fluence Peer tests', () => { (call %init_peer_id% ("print" "print") ["2"]) ) `; - const particle = peer.internals.createNewParticle(script); + const particle = await peer.internals.createNewParticle(script); + + const res = await new Promise((resolve, reject) => { + const res: any[] = []; if (particle instanceof Error) { return reject(particle.message); @@ -70,8 +71,7 @@ describe('Basic AVM functionality in Fluence Peer tests', () => { it('Timeout in par call: race', async () => { await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const script = ` + const script = ` (seq (call %init_peer_id% ("op" "identity") ["slow_result"] arg) (seq @@ -86,8 +86,9 @@ describe('Basic AVM functionality in Fluence Peer tests', () => { ) ) `; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + + const res = await new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } @@ -109,8 +110,7 @@ describe('Basic AVM functionality in Fluence Peer tests', () => { it('Timeout in par call: wait', async () => { await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const script = ` + const script = ` (seq (call %init_peer_id% ("op" "identity") ["timeout_msg"] arg) (seq @@ -136,8 +136,9 @@ describe('Basic AVM functionality in Fluence Peer tests', () => { ) ) `; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + + const res = await new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } diff --git a/packages/core/js-client/src/jsPeer/__test__/par.spec.ts b/packages/core/js-client/src/jsPeer/__test__/par.spec.ts index 00b0a4f1..88182c5a 100644 --- a/packages/core/js-client/src/jsPeer/__test__/par.spec.ts +++ b/packages/core/js-client/src/jsPeer/__test__/par.spec.ts @@ -21,8 +21,7 @@ import { CallServiceData, ResultCodes } from '../../jsServiceHost/interfaces.js' describe('FluencePeer flow tests', () => { it('should execute par instruction in parallel', async function () { await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const script = ` + const script = ` (par (seq (call %init_peer_id% ("flow" "timeout") [1000 "test1"] res1) @@ -35,8 +34,9 @@ describe('FluencePeer flow tests', () => { ) `; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + + const res = await new Promise((resolve, reject) => { peer.internals.regHandler.forParticle(particle.id, 'flow', 'timeout', (req: CallServiceData) => { const [timeout, message] = req.args; diff --git a/packages/core/js-client/src/jsPeer/__test__/peer.spec.ts b/packages/core/js-client/src/jsPeer/__test__/peer.spec.ts index 114b64b8..09b88067 100644 --- a/packages/core/js-client/src/jsPeer/__test__/peer.spec.ts +++ b/packages/core/js-client/src/jsPeer/__test__/peer.spec.ts @@ -28,15 +28,15 @@ describe('FluencePeer usage test suite', () => { it('Should successfully call identity on local peer', async function () { await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const script = ` + const script = ` (seq (call %init_peer_id% ("op" "identity") ["test"] res) (call %init_peer_id% ("callback" "callback") [res]) ) `; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + + const res = await new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } @@ -72,8 +72,7 @@ describe('FluencePeer usage test suite', () => { it('Should not crash if undefined is passed as a variable', async () => { await withPeer(async (peer) => { - const res = await new Promise((resolve, reject) => { - const script = ` + const script = ` (seq (call %init_peer_id% ("load" "arg") [] arg) (seq @@ -81,8 +80,9 @@ describe('FluencePeer usage test suite', () => { (call %init_peer_id% ("callback" "callback") [res]) ) )`; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + + const res = await new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } @@ -112,14 +112,14 @@ describe('FluencePeer usage test suite', () => { it('Should not crash if an error ocurred in user-defined handler', async () => { await withPeer(async (peer) => { - const promise = new Promise((_resolve, reject) => { - const script = ` + const script = ` (xor (call %init_peer_id% ("load" "arg") [] arg) (call %init_peer_id% ("callback" "error") [%last_error%]) )`; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + + const promise = new Promise((_resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } @@ -149,14 +149,14 @@ describe('FluencePeer usage test suite', () => { }); async function callIncorrectService(peer: FluencePeer): Promise { - return new Promise((resolve, reject) => { - const script = ` + const script = ` (xor (call %init_peer_id% ("incorrect" "incorrect") [] res) (call %init_peer_id% ("callback" "error") [%last_error%]) )`; - const particle = peer.internals.createNewParticle(script); - + const particle = await peer.internals.createNewParticle(script); + + return new Promise((resolve, reject) => { if (particle instanceof Error) { return reject(particle.message); } diff --git a/packages/core/js-client/src/jsServiceHost/interfaces.ts b/packages/core/js-client/src/jsServiceHost/interfaces.ts index ada283e7..09a879e6 100644 --- a/packages/core/js-client/src/jsServiceHost/interfaces.ts +++ b/packages/core/js-client/src/jsServiceHost/interfaces.ts @@ -94,7 +94,7 @@ export interface ParticleContext { /** * Particle's signature */ - signature?: string; + signature: Uint8Array; } /** diff --git a/packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts b/packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts index 7df50512..79961b02 100644 --- a/packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts +++ b/packages/core/js-client/src/keypair/__test__/KeyPair.spec.ts @@ -1,3 +1,18 @@ +/* + * 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 { it, describe, expect } from 'vitest'; import { toUint8Array } from 'js-base64'; import * as bs58 from 'bs58'; diff --git a/packages/core/js-client/src/keypair/index.ts b/packages/core/js-client/src/keypair/index.ts index b5bc7478..a6ce6003 100644 --- a/packages/core/js-client/src/keypair/index.ts +++ b/packages/core/js-client/src/keypair/index.ts @@ -15,9 +15,9 @@ */ import type { PeerId } from '@libp2p/interface/peer-id'; -import { generateKeyPairFromSeed, generateKeyPair } from '@libp2p/crypto/keys'; -import { createFromPrivKey } from '@libp2p/peer-id-factory'; -import type { PrivateKey } from '@libp2p/interface/keys'; +import { generateKeyPairFromSeed, generateKeyPair, unmarshalPublicKey } from '@libp2p/crypto/keys'; +import { createFromPrivKey, createFromPubKey } from '@libp2p/peer-id-factory'; +import type { PrivateKey, PublicKey } from '@libp2p/interface/keys'; import { toUint8Array } from 'js-base64'; import * as bs58 from 'bs58'; import { KeyPairOptions } from '@fluencelabs/interfaces'; @@ -33,7 +33,11 @@ export class KeyPair { return this.libp2pPeerId; } - constructor(private key: PrivateKey, private libp2pPeerId: PeerId) {} + constructor( + private privateKey: PrivateKey | undefined, + private publicKey: PublicKey, + private libp2pPeerId: PeerId + ) {} /** * Generates new KeyPair from ed25519 private key represented as a 32 byte array @@ -43,7 +47,7 @@ export class KeyPair { static async fromEd25519SK(seed: Uint8Array): Promise { const key = await generateKeyPairFromSeed('Ed25519', seed, 256); const lib2p2Pid = await createFromPrivKey(key); - return new KeyPair(key, lib2p2Pid); + return new KeyPair(key, key.public, lib2p2Pid); } /** @@ -53,7 +57,7 @@ export class KeyPair { static async randomEd25519(): Promise { const key = await generateKeyPair('Ed25519'); const lib2p2Pid = await createFromPrivKey(key); - return new KeyPair(key, lib2p2Pid); + return new KeyPair(key, key.public, lib2p2Pid); } getPeerId(): string { @@ -64,15 +68,21 @@ export class KeyPair { * @returns 32 byte private key */ toEd25519PrivateKey(): Uint8Array { - return this.key.marshal().subarray(0, 32); + if (this.privateKey === undefined) { + throw new Error('Private key not supplied'); + } + return this.privateKey.marshal().subarray(0, 32); } signBytes(data: Uint8Array): Promise { - return this.key.sign(data); + if (this.privateKey === undefined) { + throw new Error('Private key not supplied'); + } + return this.privateKey.sign(data); } verify(data: Uint8Array, signature: Uint8Array): Promise { - return this.key.public.verify(data, signature); + return this.publicKey.verify(data, signature); } } diff --git a/packages/core/js-client/src/particle/Particle.ts b/packages/core/js-client/src/particle/Particle.ts index c44c3b7e..5482ca18 100644 --- a/packages/core/js-client/src/particle/Particle.ts +++ b/packages/core/js-client/src/particle/Particle.ts @@ -14,15 +14,17 @@ * limitations under the License. */ -import { fromUint8Array, toUint8Array } from 'js-base64'; +import { atob, 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'; +import { concat } from 'uint8arrays/concat'; +import { numberToLittleEndianBytes } from '../util/bytes.js'; +import { KeyPair } from '../keypair/index.js'; +import { unmarshalPublicKey } from '@libp2p/crypto/keys'; export class Particle implements IParticle { - readonly signature: undefined; - constructor( public readonly id: string, public readonly timestamp: number, @@ -30,12 +32,15 @@ export class Particle implements IParticle { public readonly data: Uint8Array, public readonly ttl: number, public readonly initPeerId: string, - ) { - this.signature = undefined; - } + public readonly signature: Uint8Array + ) {} - static createNew(script: string, initPeerId: string, ttl: number): Particle { - return new Particle(uuidv4(), Date.now(), script, Buffer.from([]), ttl, initPeerId); + static async createNew(script: string, initPeerId: string, ttl: number, keyPair: KeyPair): Promise { + const id = uuidv4(); + const timestamp = Date.now(); + const message = buildParticleMessage({ id, timestamp, ttl, script }); + const signature = await keyPair.signBytes(message); + return new Particle(id, Date.now(), script, Buffer.from([]), ttl, initPeerId, signature); } static fromString(str: string): Particle { @@ -47,12 +52,27 @@ export class Particle implements IParticle { toUint8Array(json.data), json.ttl, json.init_peer_id, + new Uint8Array(json.signature) ); return res; } } +const en = new TextEncoder(); + +/** + * Builds particle message for signing + */ +export const buildParticleMessage = ({ id, timestamp, ttl, script }: Omit): Uint8Array => { + return concat([ + en.encode(id), + numberToLittleEndianBytes(timestamp, 'u64'), + numberToLittleEndianBytes(ttl, 'u32'), + en.encode(script), + ]); +} + /** * Returns actual ttl of a particle, i.e. ttl - time passed since particle creation */ @@ -67,11 +87,21 @@ export const hasExpired = (particle: IParticle): boolean => { return getActualTTL(particle) <= 0; }; +/** + * Validates that particle signature is correct + */ +export const verifySignature = async (particle: IParticle, publicKey: Uint8Array): Promise => { + // TODO: Uncomment this when nox roll out particle signatures + return true; + // const message = buildParticleMessage(particle); + // return unmarshalPublicKey(publicKey).verify(message, particle.signature); +} + /** * 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); + return new Particle(particle.id, particle.timestamp, particle.script, newData, particle.ttl, particle.initPeerId, particle.signature); }; /** @@ -92,8 +122,7 @@ export const serializeToString = (particle: IParticle): string => { timestamp: particle.timestamp, ttl: particle.ttl, script: particle.script, - // TODO: copy signature from a particle after signatures will be implemented on nodes - signature: [], + signature: Array.from(particle.signature), data: particle.data && fromUint8Array(particle.data), }); }; diff --git a/packages/core/js-client/src/particle/interfaces.ts b/packages/core/js-client/src/particle/interfaces.ts index 551092e2..3f7a131a 100644 --- a/packages/core/js-client/src/particle/interfaces.ts +++ b/packages/core/js-client/src/particle/interfaces.ts @@ -44,8 +44,10 @@ export interface IImmutableParticlePart { */ readonly initPeerId: PeerIdB58; - // TODO: implement particle signatures - readonly signature: undefined; + /** + * Particle's signature of concatenation of bytes of all immutable particle fields. + */ + readonly signature: Uint8Array; } /** diff --git a/packages/core/js-client/src/services/Sig.ts b/packages/core/js-client/src/services/Sig.ts index 20b99441..64703bfd 100644 --- a/packages/core/js-client/src/services/Sig.ts +++ b/packages/core/js-client/src/services/Sig.ts @@ -79,7 +79,7 @@ export class Sig implements SigDef { * Verifies the signature. Required by aqua */ verify(signature: number[], data: number[]): Promise { - return this.keyPair.verify(Uint8Array.from(data), Uint8Array.from(signature)); + return this.keyPair.verify(Uint8Array.from(data), Uint8Array.from(signature)) } } diff --git a/packages/core/js-client/src/services/__test__/builtInHandler.spec.ts b/packages/core/js-client/src/services/__test__/builtInHandler.spec.ts index 99320670..8601f2d2 100644 --- a/packages/core/js-client/src/services/__test__/builtInHandler.spec.ts +++ b/packages/core/js-client/src/services/__test__/builtInHandler.spec.ts @@ -122,7 +122,7 @@ describe('Tests for default handler', () => { initPeerId: 'init peer id', timestamp: 595951200, ttl: 595961200, - signature: 'sig', + signature: new Uint8Array([]), }, }; @@ -156,7 +156,7 @@ describe('Tests for default handler', () => { initPeerId: 'init peer id', timestamp: 595951200, ttl: 595961200, - signature: 'sig', + signature: new Uint8Array([]), }, }; diff --git a/packages/core/js-client/src/services/__test__/jsonBuiltin.spec.ts b/packages/core/js-client/src/services/__test__/jsonBuiltin.spec.ts index c13aab98..02cf4667 100644 --- a/packages/core/js-client/src/services/__test__/jsonBuiltin.spec.ts +++ b/packages/core/js-client/src/services/__test__/jsonBuiltin.spec.ts @@ -56,7 +56,7 @@ describe('Sig service test suite', () => { }; }); }); - const p = peer.internals.createNewParticle(script); + const p = await peer.internals.createNewParticle(script); await peer.internals.initiateParticle(p, doNothing); const [nestedFirst, nestedSecond, outerFirst, outerSecond, outerFirstString, outerFirstParsed] = await promise; diff --git a/packages/core/js-client/src/util/bytes.ts b/packages/core/js-client/src/util/bytes.ts new file mode 100644 index 00000000..5e19461a --- /dev/null +++ b/packages/core/js-client/src/util/bytes.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +type Size = 'u32' | 'u64'; + +const sizeMap = { + 'u32': 4, + 'u64': 8 +} as const; + +function numberToBytes(n: number, s: Size, littleEndian: boolean) { + const size = sizeMap[s]; + const buffer = new ArrayBuffer(size); + const dv = new DataView(buffer); + dv.setUint32(0, n, littleEndian); + return new Uint8Array(buffer); +} + +export function numberToLittleEndianBytes(n: number, s: Size) { + return numberToBytes(n, s, true); +} \ No newline at end of file