mirror of
https://github.com/fluencelabs/fluence-js.git
synced 2024-12-03 17:40:18 +00:00
feat(js-client)!: Particle signatures [fixes DXJ-466] (#353)
* Introduce particle signatures * Fix particle id field in particle context * Fix types * Fix review comments * Remove init_peer_id from signature * Fix typo * Fix error msg * Fix async promise constructor antipattern * Refactor utils * Move text encoder outside * Use async/await * Update packages/core/js-client/src/connection/RelayConnection.ts Co-authored-by: shamsartem <shamsartem@gmail.com> * Hide crypto implementation beside KeyPair * Fix verify method * Comment verify method * Use particle signature instead of id * remove async/await from method * Fix type * Update packages/core/js-client/src/particle/interfaces.ts Co-authored-by: folex <0xdxdy@gmail.com> * Update packages/core/js-client/src/particle/Particle.ts Co-authored-by: folex <0xdxdy@gmail.com> * Fix review comment * Update pipe * set logging * try cache --------- Co-authored-by: shamsartem <shamsartem@gmail.com> Co-authored-by: folex <0xdxdy@gmail.com>
This commit is contained in:
parent
15a2c91917
commit
c0b73fec4a
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -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
|
||||
|
3
.npmrc
3
.npmrc
@ -1,2 +1,3 @@
|
||||
auto-install-peers=true
|
||||
save-exact=true
|
||||
save-exact=true
|
||||
side-effects-cache=false
|
@ -11,8 +11,7 @@ describe('FluenceClient usage test suite', () => {
|
||||
await withClient(RELAY, {}, async (peer) => {
|
||||
// arrange
|
||||
|
||||
const result = await new Promise<string[]>((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<string>((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);
|
||||
}
|
||||
|
@ -28,8 +28,7 @@ const log = logger('connection');
|
||||
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) => {
|
||||
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<b
|
||||
(call %init_peer_id% ("callback" "error") [%last_error%])
|
||||
)
|
||||
)`;
|
||||
const particle = peer.internals.createNewParticle(script, ttl);
|
||||
const particle = await peer.internals.createNewParticle(script, ttl);
|
||||
|
||||
const promise = new Promise<string>((resolve, reject) => {
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
@ -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;
|
||||
})
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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<string>((resolve) => {
|
||||
client.internals.regHandler.forParticle(particle.id, 'test', 'test', (req: CallServiceData) => {
|
||||
|
@ -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<IParticle> => {
|
||||
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;
|
||||
|
@ -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<string[]>((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<string>((resolve, reject) => {
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
||||
registerHandlersHelper(peer, particle, {
|
||||
print: {
|
||||
print: (args: Array<Array<string>>) => {
|
||||
print: (args: Array<string>) => {
|
||||
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<string[]>((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<string[]>((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);
|
||||
}
|
||||
|
@ -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<any>((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<any>((resolve, reject) => {
|
||||
peer.internals.regHandler.forParticle(particle.id, 'flow', 'timeout', (req: CallServiceData) => {
|
||||
const [timeout, message] = req.args;
|
||||
|
||||
|
@ -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<string>((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<string>((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<any>((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<any>((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<any>((_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<any>((_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<string[]> {
|
||||
return new Promise<any[]>((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<any[]>((resolve, reject) => {
|
||||
if (particle instanceof Error) {
|
||||
return reject(particle.message);
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ export interface ParticleContext {
|
||||
/**
|
||||
* Particle's signature
|
||||
*/
|
||||
signature?: string;
|
||||
signature: Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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';
|
||||
|
@ -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<KeyPair> {
|
||||
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<KeyPair> {
|
||||
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<Uint8Array> {
|
||||
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<boolean> {
|
||||
return this.key.public.verify(data, signature);
|
||||
return this.publicKey.verify(data, signature);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Particle> {
|
||||
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<IParticle, 'initPeerId' | 'signature' | 'data'>): 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<boolean> => {
|
||||
// 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),
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,7 +79,7 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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([]),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
34
packages/core/js-client/src/util/bytes.ts
Normal file
34
packages/core/js-client/src/util/bytes.ts
Normal file
@ -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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user