diff --git a/packages/cactus-common/src/main/typescript/js-object-signer.ts b/packages/cactus-common/src/main/typescript/js-object-signer.ts index f630bc510f..d0d6ee084e 100644 --- a/packages/cactus-common/src/main/typescript/js-object-signer.ts +++ b/packages/cactus-common/src/main/typescript/js-object-signer.ts @@ -108,7 +108,7 @@ export class JsObjectSigner { * @param data * @returns {string} */ - private dataHash(data: unknown): string { + public dataHash(data: unknown): string { const hashObj = new sha3.SHA3Hash(256); hashObj.update(stringify(data)); const hashMsg = hashObj.digest(`hex`); diff --git a/packages/cactus-plugin-bungee-hermes/README.md b/packages/cactus-plugin-bungee-hermes/README.md index c3ef15d7c0..2ada98ce32 100644 --- a/packages/cactus-plugin-bungee-hermes/README.md +++ b/packages/cactus-plugin-bungee-hermes/README.md @@ -72,6 +72,8 @@ Endpoints exposed: - GetPublicKey - GetAvailableStrategies - VerifyMerkleRoot + - MergeViewsV1 + - ProcessViewV1 ## Running the tests @@ -80,7 +82,9 @@ Endpoints exposed: - **fabric-test-basic.test.ts**: A test using strategy-fabric and a fabric connector, testing creating views for different timeframes and states. - **besu-test-pruning.test.ts**: A test using strategy-besu and a besu connector, testing creating views for specific timeframes. - **fabric-test-pruning.test.ts**: A test using strategy-fabric and a fabric connector, testing creating views for specific timeframes. - - **bungee-api-test.test.ts**: A more complex test, using both besu and fabric strategies, fabric and besu connectors, and calls to bungee-hermes API. Tests bungee's API and functionality with multiple strategies. + - **bungee-api-test.test.ts**: A more complex test, using multiple strategies, connectors, and calls to bungee-hermes API. Tests new functionalities like view proof validation. + - **bungee-merge-views.test.ts**: A test using besu-strategy and connector, to demonstrate and test the mergeView functionality of bungee-hermes. + - **bungee-process-views.test.ts**: A test using besu-strategy and connector, to demonstrate and test processView functionality of the plugin. Tests developed using JEST testing framework. @@ -152,8 +156,26 @@ Note that each strategy can be used to query different ledgers (ledgers of the s Each strategy implements the logic to query information from each different ledger (i.e. capture set of asset states), while bungee-hermes plugin handles the snapshot and view creation. -'View' object contains a 'viewProof'. viewProof is composed by two merkle trees, one for stateProofs and another for transactionProofs. -One can check if the content of a view has no inconsistencies, by querying the VerifyMerkleRoot endpoint with the appropriate input: +We can merge views to create an IntegratedView. Integrated views are the result of merging multiple views, with or without using a privacy policy. +IntegratedViews have a map of ExtendedStates (stateId -> ExtendedState). And ExtendedState is the data gathered for a specific stateId, from different views map(viewId -> State). + +Example: +```typescript +const mergeViewsNoPolicyReq = await bungeeApi.mergeViewsV1({ + //for previously obtained views + serializedViews: [JSON.stringify(view.view), JSON.stringify(view2.view)], + //no policy. When policy is requested, take note that order of arguments matter + mergePolicy: undefined, + policyArguments: [], + }); +expect(mergeViewsNoPolicyReq.status).toBe(200); +expect(mergeViewsNoPolicyReq.data.integratedView).toBeTruthy(); +expect(mergeViewsNoPolicyReq.data.signature).toBeTruthy(); +``` + + +'View' object contains a 'viewProof'. 'viewProof' is composed by two merkle trees, one for 'stateProofs' and another for 'transactionProofs'. The analogous exists for IntegratedView objects, a merkle tree is created for all transactions and states in the IntegratedView. +One can check if the content of a view or integratedView has no inconsistencies, by querying the VerifyMerkleRoot endpoint with the appropriate input: ```typescript //using a previously created View object @@ -162,10 +184,9 @@ One can check if the content of a view has no inconsistencies, by querying the V .getStateBins() .map((x) => JSON.stringify(x.getStateProof())); const transactionProofs: string[] = []; - view? - .getAllTransactions() - .forEach((t) => transactionProofs.push(JSON.stringify(t.getProof()))); - + view?.getAllTransactions() + .forEach((t) => transactionProofs.push(JSON.stringify(t.getProof()))), + ); const verifyStateRoot = await bungeeApi.verifyMerkleRoot({ input: stateProofs?.reverse(), //check integrity, order should not matter root: proof?.statesMerkleRoot, @@ -179,12 +200,30 @@ One can check if the content of a view has no inconsistencies, by querying the V expect(verifyTransactionsRoot.data.result).toBeTrue(); ``` +Lastly, we can also process views according to a privacy-policy as follows: + +```typescript +import { hideState } from "../../../main/typescript/view-creation/privacy-policies"; + +bungee.addViewPrivacyPolicy("policy1", hideState); + +const processed = await bungeeApi.processViewV1({ + serializedView: JSON.stringify({ + view: JSON.stringify(view.view as View), + signature: view.signature, + }), + policyId: "hideState", + policyArguments: [BESU_ASSET_ID], + }); +``` +This example uses the hideState policy, which takes as arguments a stateId to remove from the new view. +When we apply a policy, the old view metadata is stored in the new View for traceability. This process can be applied multiple times ## Contributing We welcome contributions to Hyperledger Cactus in many forms, and there’s always plenty to do! -Please review [CONTIRBUTING.md](https://github.com/hyperledger/cactus/blob/main/CONTRIBUTING.md "CONTIRBUTING.md") to get started. +Please review [CONTRIBUTING.md](https://github.com/hyperledger/cactus/blob/main/CONTRIBUTING.md "CONTRIBUTING.md") to get started. ## License This distribution is published under the Apache License Version 2.0 found in the [LICENSE ](https://github.com/hyperledger/cactus/blob/main/LICENSE "LICENSE ")file. \ No newline at end of file diff --git a/packages/cactus-plugin-bungee-hermes/src/main/json/openapi.json b/packages/cactus-plugin-bungee-hermes/src/main/json/openapi.json index fcd859943c..4320b2fb1c 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/json/openapi.json +++ b/packages/cactus-plugin-bungee-hermes/src/main/json/openapi.json @@ -26,6 +26,18 @@ }, "components": { "schemas": { + "PrivacyPolicyOpts":{ + "description": "identifier of the policy used to process a view", + "type": "string", + "enum": ["pruneState"], + "x-enum-varnames":["PruneState"] + }, + "MergePolicyOpts":{ + "description": "identifier of the policy used to merge views (can be none)", + "type": "string", + "enum": ["pruneState", "pruneStateFromView", "NONE"], + "x-enum-varnames":["PruneState", "PruneStateFromView", "NONE"] + }, "CreateViewRequest":{ "type":"object", "description": "Request object for createViewV1 endpoint", @@ -80,7 +92,7 @@ }, "CreateViewResponse":{ "type":"object", - "description": "This is the response for a viewRequests", + "description": "This is the response for a CreateViewRequest or ProcessViewRequest", "properties": { "view":{ "type":"string", @@ -138,6 +150,72 @@ "example": "true" } } + }, + "MergeViewsResponse":{ + "type":"object", + "description": "This is the response of a mergeViewRequest", + "properties": { + "integratedView":{ + "type":"string", + "example": "Object" + }, + "signature":{ + "type": "string", + "example": "signature of Object" + } + }, + "example": {"integratedView": "Object", "signature":"signature of Object"} + }, + "MergeViewsRequest":{ + "type":"object", + "description": "This is the input for a mergeViewRequest", + "required": ["serializedViews", "mergePolicy"], + "properties": { + "serializedViews":{ + "nullable": false, + "type":"array", + "description": "Array of serialized views", + "items":{ + "type": "string", + "nullable": false, + "example": "View object stringified" + } + }, + "mergePolicy":{ + "$ref" : "#/components/schemas/MergePolicyOpts" + }, + "policyArguments":{ + "type": "array", + "description": "Arguments for the privacy policy function. Order is important", + "items":{ + "type": "string" + } + } + }, + "example": {"serializedViews": ["View 1", "View2"], "mergePolicy": "undefined" } + }, + "ProcessViewRequest":{ + "type":"object", + "description": "This is the input for a mergeViewRequest", + "required": ["serializedView", "policyId", "policyArguments"], + "properties": { + "serializedView":{ + "type": "string", + "nullable": false, + "example": "View object stringified" + }, + "policyId":{ + "$ref" : "#/components/schemas/PrivacyPolicyOpts" + }, + "policyArguments":{ + "type": "array", + "description": "Arguments for the privacy policy function. Order is important", + "items":{ + "type": "string" + } + } + }, + "example": {"serializedView": "View 1", "policyId": "policy 1", "policyArguments": ["stateId"]} } } }, @@ -286,6 +364,88 @@ } } } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/merge-views": { + "get": { + "x-hyperledger-cacti": { + "http": { + "verbLowerCase": "get", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/merge-views" + } + }, + "operationId": "mergeViewsV1", + "summary": "Merges multiple views according to a privacy policy", + "description": "", + "parameters": [], + "requestBody": { + "required": true, + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MergeViewsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MergeViewsResponse" + } + } + } + }, + "404": { + "description": "Could not complete request." + } + } + } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/process-view": { + "get": { + "x-hyperledger-cacti": { + "http": { + "verbLowerCase": "get", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/process-view" + } + }, + "operationId": "processViewV1", + "summary": "Creates a Blockchain View.", + "description": "", + "parameters": [], + "requestBody": { + "required": true, + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessViewRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateViewResponse" + }, + "example": {"view": "Object", "signature":"signature of Object"} + } + } + }, + "404": { + "description": "Could not complete request." + } + } + } } + } } \ No newline at end of file diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/generated/openapi/typescript-axios/api.ts index 816addfb7b..5ae413b289 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -86,7 +86,7 @@ export interface CreateViewRequestNetworkDetails { 'participant': string; } /** - * This is the response for a viewRequests + * This is the response for a CreateViewRequest or ProcessViewRequest * @export * @interface CreateViewResponse */ @@ -117,6 +117,107 @@ export interface GetPublicKeyResponse { */ 'pubKey'?: string; } +/** + * identifier of the policy used to merge views (can be none) + * @export + * @enum {string} + */ + +export const MergePolicyOpts = { + PruneState: 'pruneState', + PruneStateFromView: 'pruneStateFromView', + NONE: 'NONE' +} as const; + +export type MergePolicyOpts = typeof MergePolicyOpts[keyof typeof MergePolicyOpts]; + + +/** + * This is the input for a mergeViewRequest + * @export + * @interface MergeViewsRequest + */ +export interface MergeViewsRequest { + /** + * Array of serialized views + * @type {Array} + * @memberof MergeViewsRequest + */ + 'serializedViews': Array; + /** + * + * @type {MergePolicyOpts} + * @memberof MergeViewsRequest + */ + 'mergePolicy': MergePolicyOpts; + /** + * Arguments for the privacy policy function. Order is important + * @type {Array} + * @memberof MergeViewsRequest + */ + 'policyArguments'?: Array; +} + + +/** + * This is the response of a mergeViewRequest + * @export + * @interface MergeViewsResponse + */ +export interface MergeViewsResponse { + /** + * + * @type {string} + * @memberof MergeViewsResponse + */ + 'integratedView'?: string; + /** + * + * @type {string} + * @memberof MergeViewsResponse + */ + 'signature'?: string; +} +/** + * identifier of the policy used to process a view + * @export + * @enum {string} + */ + +export const PrivacyPolicyOpts = { + PruneState: 'pruneState' +} as const; + +export type PrivacyPolicyOpts = typeof PrivacyPolicyOpts[keyof typeof PrivacyPolicyOpts]; + + +/** + * This is the input for a mergeViewRequest + * @export + * @interface ProcessViewRequest + */ +export interface ProcessViewRequest { + /** + * + * @type {string} + * @memberof ProcessViewRequest + */ + 'serializedView': string; + /** + * + * @type {PrivacyPolicyOpts} + * @memberof ProcessViewRequest + */ + 'policyId': PrivacyPolicyOpts; + /** + * Arguments for the privacy policy function. Order is important + * @type {Array} + * @memberof ProcessViewRequest + */ + 'policyArguments': Array; +} + + /** * Set of transaction or state proofs and merkle tree root for verification * @export @@ -252,6 +353,78 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Merges multiple views according to a privacy policy + * @param {MergeViewsRequest} mergeViewsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mergeViewsV1: async (mergeViewsRequest: MergeViewsRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'mergeViewsRequest' is not null or undefined + assertParamExists('mergeViewsV1', 'mergeViewsRequest', mergeViewsRequest) + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/merge-views`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(mergeViewsRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Creates a Blockchain View. + * @param {ProcessViewRequest} processViewRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + processViewV1: async (processViewRequest: ProcessViewRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'processViewRequest' is not null or undefined + assertParamExists('processViewV1', 'processViewRequest', processViewRequest) + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/process-view`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(processViewRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Checks validity of merkle tree root given an input @@ -329,6 +502,28 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getPublicKey(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Merges multiple views according to a privacy policy + * @param {MergeViewsRequest} mergeViewsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async mergeViewsV1(mergeViewsRequest: MergeViewsRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.mergeViewsV1(mergeViewsRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary Creates a Blockchain View. + * @param {ProcessViewRequest} processViewRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async processViewV1(processViewRequest: ProcessViewRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.processViewV1(processViewRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Checks validity of merkle tree root given an input @@ -378,6 +573,26 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa getPublicKey(options?: any): AxiosPromise { return localVarFp.getPublicKey(options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Merges multiple views according to a privacy policy + * @param {MergeViewsRequest} mergeViewsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + mergeViewsV1(mergeViewsRequest: MergeViewsRequest, options?: any): AxiosPromise { + return localVarFp.mergeViewsV1(mergeViewsRequest, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Creates a Blockchain View. + * @param {ProcessViewRequest} processViewRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + processViewV1(processViewRequest: ProcessViewRequest, options?: any): AxiosPromise { + return localVarFp.processViewV1(processViewRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary Checks validity of merkle tree root given an input @@ -432,6 +647,30 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).getPublicKey(options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Merges multiple views according to a privacy policy + * @param {MergeViewsRequest} mergeViewsRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public mergeViewsV1(mergeViewsRequest: MergeViewsRequest, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).mergeViewsV1(mergeViewsRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Creates a Blockchain View. + * @param {ProcessViewRequest} processViewRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public processViewV1(processViewRequest: ProcessViewRequest, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).processViewV1(processViewRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Checks validity of merkle tree root given an input diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/plugin-bungee-hermes.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/plugin-bungee-hermes.ts index 899aae22d3..a660eab159 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/plugin-bungee-hermes.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/plugin-bungee-hermes.ts @@ -24,9 +24,15 @@ import { Server as SecureServer } from "https"; import { CreateViewRequest, CreateViewResponse, + MergePolicyOpts, + MergeViewsRequest, + MergeViewsResponse, + PrivacyPolicyOpts, + ProcessViewRequest, } from "./generated/openapi/typescript-axios"; import { Snapshot } from "./view-creation/snapshot"; import { View } from "./view-creation/view"; +import { IntegratedView } from "./view-merging/integrated-view"; import { NetworkDetails, ObtainLedgerStrategy, @@ -36,6 +42,12 @@ import { GetPublicKeyEndpointV1 } from "./web-services/get-public-key-endpoint"; import { GetAvailableStrategiesEndpointV1 } from "./web-services/get-available-strategies-endpoint"; import MerkleTree from "merkletreejs"; import { VerifyMerkleRootEndpointV1 } from "./web-services/verify-merkle-root-endpoint"; +import { MergePolicies } from "./view-merging/merge-policies"; +import { deserializeView } from "./utils"; +import { MergeViewsEndpointV1 } from "./web-services/merge-views-endpoint"; +import { ProcessViewEndpointV1 } from "./web-services/process-view-endpoint"; + +import { PrivacyPolicies } from "./view-creation/privacy-policies"; export interface IKeyPair { publicKey: Uint8Array; @@ -64,6 +76,10 @@ export class PluginBungeeHermes implements ICactusPlugin, IPluginWebService { private strategies: Map; + private mergePolicies: MergePolicies = new MergePolicies(); + + private viewPrivacyPolicies: PrivacyPolicies = new PrivacyPolicies(); + private level: LogLevelDesc; private endpoints: IWebServiceEndpoint[] | undefined; @@ -74,6 +90,7 @@ export class PluginBungeeHermes implements ICactusPlugin, IPluginWebService { this.level = options.logLevel || "INFO"; this.strategies = new Map(); + const label = this.className; const level = this.level; @@ -156,12 +173,20 @@ export class PluginBungeeHermes implements ICactusPlugin, IPluginWebService { const verifyMerkleProofEndpoint = new VerifyMerkleRootEndpointV1({ bungee: this, }); + const mergeViewsEndpoint = new MergeViewsEndpointV1({ + bungee: this, + }); + const processViewEndpoint = new ProcessViewEndpointV1({ + bungee: this, + }); this.endpoints = [ viewEndpoint, pubKeyEndpoint, availableStrategiesEndpoint, verifyMerkleProofEndpoint, + mergeViewsEndpoint, + processViewEndpoint, ]; return this.endpoints; } @@ -173,7 +198,77 @@ export class PluginBungeeHermes implements ICactusPlugin, IPluginWebService { public getInstanceId(): string { return this.instanceId; } - + onProcessView(request: ProcessViewRequest): CreateViewResponse { + const view = deserializeView(request.serializedView); + const signature = JSON.parse(request.serializedView).signature; + if (signature == undefined) { + throw Error("Provided view does not include signature."); + } + const parsed = JSON.parse(request.serializedView); + if ( + !this.verifyViewSignature( + signature, + parsed.view, + JSON.parse(parsed.view).creator, + ) + ) { + this.log.info("Some signature was deemed invalid"); + throw Error( + "At least one of they views does not include a valid signature", + ); + } + const prevVersionMetadata = { + viewId: view.getKey(), + creator: view.getCreator(), + viewProof: view.getViewProof(), + signature, + policy: view.getPolicy(), + }; + this.processView(view, request.policyId, request.policyArguments); + view.addPrevVersionMetadata(prevVersionMetadata); + view.setCreator(this.pubKeyBungee); + view.setKey(uuidV4()); + return { + view: JSON.stringify(view), + signature: this.sign(JSON.stringify(view)), + }; + } + onMergeViews(request: MergeViewsRequest): MergeViewsResponse { + const policy = request.mergePolicy; + const views: View[] = []; + const signatures: string[] = []; + if (request.serializedViews.length <= 1) { + this.log.info("less than 2 views were provided"); + throw Error("Must provide more than 1 view"); + } + request.serializedViews.forEach((view) => { + const parsed = JSON.parse(view); + if ( + !this.verifyViewSignature( + parsed.signature, + parsed.view, + JSON.parse(parsed.view).creator, + ) + ) { + this.log.info("Some signature was deemed invalid"); + throw Error( + "At least one of they views does not include a valid signature", + ); + } + signatures.push(parsed.signature); + views.push(deserializeView(view)); + }); + const integratedView = this.mergeViews( + views, + signatures, + policy, + request.policyArguments ? request.policyArguments : [], + ); + return { + integratedView: JSON.stringify(integratedView), + signature: integratedView.signature, + }; + } async onCreateView(request: CreateViewRequest): Promise { //ti and tf are unix timestamps, represented as strings const ti: string = request.tI ? request.tI : "0"; @@ -188,11 +283,10 @@ export class PluginBungeeHermes implements ICactusPlugin, IPluginWebService { request.networkDetails, ); this.logger.info("Generating view for request: ", request); - const response = JSON.stringify( - this.generateView(snapshot, ti, tf, request.viewID), - ); + const response = this.generateView(snapshot, ti, tf, request.viewID); return { - view: response, + view: JSON.stringify(response.view), + signature: response.signature, }; } @@ -203,13 +297,13 @@ export class PluginBungeeHermes implements ICactusPlugin, IPluginWebService { id: string | undefined, ): { view?: View; signature?: string } { if ( - parseInt(tI) > parseInt(snapshot.getTF()) || - parseInt(tF) < parseInt(snapshot.getTI()) || - parseInt(tI) > parseInt(tF) + BigInt(tI) > BigInt(snapshot.getTF()) || + BigInt(tF) < BigInt(snapshot.getTI()) || + BigInt(tI) > BigInt(tF) ) { return {}; } - const view = new View(tI, tF, snapshot, id); + const view = new View(this.pubKeyBungee, tI, tF, snapshot, id); snapshot.pruneStates(tI, tF); const signature = this.sign(JSON.stringify(view)); @@ -261,4 +355,108 @@ export class PluginBungeeHermes implements ICactusPlugin, IPluginWebService { }); return tree.getRoot().toString("hex") == root; } + + public mergeViews( + views: View[], + signatures: string[], + privacyPolicy: MergePolicyOpts, + args: string[], + ): { integratedView: IntegratedView; signature: string } { + const policy = this.mergePolicies.getMergePolicy(privacyPolicy); + + let integratedView = new IntegratedView( + privacyPolicy, + policy, + this.bungeeSigner, + ); + for (const view of views) { + if (!integratedView.isParticipant(view.getParticipant())) { + integratedView.addParticipant(view.getParticipant()); + } + integratedView.addIncludedViewMetadata({ + viewId: view.getKey(), + creator: view.getCreator(), + viewProof: view.getViewProof(), + signature: signatures[views.indexOf(view)], + policy: view.getPolicy(), + }); + for (const state of view.getSnapshot().getStateBins()) { + if (integratedView.getExtendedState(state.getId()) == undefined) { + integratedView.createExtendedState(state.getId()); + } + integratedView.addStateInExtendedState( + state.getId(), + view.getKey(), + state, + ); + if ( + BigInt(state.getInitialTime()) < BigInt(integratedView.getTI()) || + BigInt(state.getInitialTime()) < 0 + ) { + integratedView.setTI(state.getInitialTime()); + } + + if ( + BigInt(state.getFinalTime()) > BigInt(integratedView.getTF()) || + BigInt(state.getFinalTime()) < 0 + ) { + integratedView.setTF(state.getFinalTime()); + } + } + } + if (policy) { + integratedView = policy(integratedView, ...args); + } + integratedView.setIntegratedViewProof(); + return { + integratedView: integratedView, + //The paper specs suggest the integratedView should be jointly signed by all participants. + //That process is left to be addressed in the future + signature: this.sign(JSON.stringify(integratedView)), + }; + } + + public processView( + view: View, + policy: PrivacyPolicyOpts, + args: string[], + ): View { + const policyF = this.viewPrivacyPolicies.getPrivacyPolicy(policy); + if (policyF) { + view = policyF(view, ...args); + view.setPrivacyPolicy(policy, policyF, this.bungeeSigner); + view.updateViewProof(); + } + return view; + } + + verifyViewSignature( + signature: string, + view: string, + pubKey: string, + ): boolean { + const sourceSignature = new Uint8Array(Buffer.from(signature, "hex")); + const sourcePubkey = new Uint8Array(Buffer.from(pubKey, "hex")); + + return this.bungeeSigner.verify(view, sourceSignature, sourcePubkey); + } + + public isSafeToCallObjectMethod( + object: Record, + name: string, + ): boolean { + Checks.truthy( + object, + `${this.className}#isSafeToCallObjectMethod():contract`, + ); + Checks.nonBlankString( + name, + `${this.className}#isSafeToCallObjectMethod():name`, + ); + + return ( + Object.prototype.hasOwnProperty.call(object, name) && + typeof object[name] === "function" + ); + } } diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/utils.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/utils.ts new file mode 100644 index 0000000000..427cca87dc --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/utils.ts @@ -0,0 +1,89 @@ +import { Proof } from "./view-creation/proof"; +import { Snapshot } from "./view-creation/snapshot"; +import { State } from "./view-creation/state"; +import { Block, StateProof } from "./view-creation/state-proof"; +import { Transaction } from "./view-creation/transaction"; +import { TransactionProof } from "./view-creation/transaction-proof"; +import { View, IViewMetadata } from "./view-creation/view"; + +export function deserializeView(viewStr: string): View { + const view = JSON.parse(JSON.parse(viewStr).view); + const snapshot = view.snapshot; + const states = snapshot.stateBins; + const stateBin: State[] = []; + for (const state of states) { + const transactions = state.transactions; + const txs: Transaction[] = []; + for (const t of transactions) { + const txProof: TransactionProof = new TransactionProof( + new Proof({ + creator: t.proof.transactionCreator.creator, + mspid: t.proof.transactionCreator.mspid, + signature: t.proof.transactionCreator.signature, + }), + t.proof.hash, + ); + const tx = new Transaction(t.id, t.timeStamp, txProof); + + if (t.proof.endorsements == undefined) { + txs.push(tx); + continue; + } + + for (const endors of t.proof.endorsements) { + const endorsement = new Proof({ + creator: endors.creator, + mspid: endors.mspid, + signature: endors.signature, + }); + tx.addEndorser(endorsement); + } + txs.push(tx); + } + + const stateN = new State(state.id, state.values, txs); + const stateProofs: StateProof[] = []; + for (const proof of state.stateProof) { + const stateProof = new StateProof( + proof.value, + proof.version, + proof.stateID, + ); + + proof.blocks.forEach((block: unknown) => { + stateProof.addBlock(block as Block); + }); + stateProofs.push(stateProof); + } + stateN.setStateProof(stateProofs); + stateBin.push(stateN); + } + + const snapshotNew = new Snapshot(snapshot.id, snapshot.participant, stateBin); + snapshotNew.update_TI_TF(); + const viewNew = new View( + view.creator, + view.tI, + view.tF, + snapshotNew, + view.key, + ); + for (const metadata of view.oldVersionsMetadata) { + viewNew.addPrevVersionMetadata(metadata as IViewMetadata); + } + viewNew.setPrivacyPolicyValue(view.policy); + viewNew.setCreator(view.creator); + + if ( + viewNew.getViewProof().statesMerkleRoot != view.viewProof.statesMerkleRoot + ) { + throw Error("Error Parsing view. States root does not match"); + } + if ( + viewNew.getViewProof().transactionsMerkleRoot != + view.viewProof.transactionsMerkleRoot + ) { + throw Error("Error Parsing view. Transactions root does not match"); + } + return viewNew; +} diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/privacy-policies.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/privacy-policies.ts new file mode 100644 index 0000000000..a32c5314d5 --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/privacy-policies.ts @@ -0,0 +1,31 @@ +import { PrivacyPolicyOpts } from "../generated/openapi/typescript-axios"; +import { View } from "./view"; + +export interface IPrivacyPolicy { + (view: View, ...args: string[]): View; +} +export interface IPrivacyPolicyValue { + policy: PrivacyPolicyOpts; + policyHash: string; +} +export class PrivacyPolicies { + constructor() {} + + public pruneState(view: View, stateId: string): View { + const snapshot = view.getSnapshot(); + snapshot.removeState(stateId); + snapshot.update_TI_TF(); + return view; + } + + public getPrivacyPolicy(opts: PrivacyPolicyOpts): IPrivacyPolicy | undefined { + switch (opts) { + case PrivacyPolicyOpts.PruneState: + return this.pruneState; + break; + default: + return undefined; + break; + } + } +} diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/snapshot.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/snapshot.ts index 3ee8e1ce5d..3a22583fa8 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/snapshot.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/snapshot.ts @@ -61,13 +61,13 @@ export class Snapshot { } public filterStates(tI: string, tF: string): void { - const finalT = parseInt(tF); - const initialT = parseInt(tI); + const finalT = BigInt(tF); + const initialT = BigInt(tI); const stateBins: State[] = []; for (const state of this.stateBins) { if ( - parseInt(state.getInitialTime()) > finalT || - parseInt(state.getFinalTime()) < initialT + BigInt(state.getInitialTime()) > finalT || + BigInt(state.getFinalTime()) < initialT ) { continue; } @@ -85,4 +85,11 @@ export class Snapshot { return JSON.stringify(snapshotJson); } + + public removeState(stateId: string) { + this.stateBins = this.stateBins.filter((state) => { + return state.getId() !== stateId; + }); + this.update_TI_TF(); + } } diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/state.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/state.ts index ba05fd50c1..b390afa36c 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/state.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/state.ts @@ -76,13 +76,13 @@ export class State { } public pruneState(tI: string, tF: string): void { - const initialT = parseInt(tI); - const finalT = parseInt(tF); + const initialT = BigInt(tI); + const finalT = BigInt(tF); // eslint-disable-next-line prefer-const this.transactions.forEach((element, index) => { if ( - parseInt(element.getTimeStamp()) < initialT || - parseInt(element.getTimeStamp()) > finalT + BigInt(element.getTimeStamp()) < initialT || + BigInt(element.getTimeStamp()) > finalT ) { //this.version = this.version - 1; this.transactions.splice(index, 1); //Remove tx diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/view.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/view.ts index 8053996094..9e076131f5 100644 --- a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/view.ts +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-creation/view.ts @@ -2,23 +2,43 @@ import { v4 as uuidV4 } from "uuid"; import { Snapshot } from "./snapshot"; import MerkleTree from "merkletreejs"; import { Transaction } from "./transaction"; +import { IPrivacyPolicy, IPrivacyPolicyValue } from "./privacy-policies"; +import { PrivacyPolicyOpts } from "../generated/openapi/typescript-axios"; +import { JsObjectSigner } from "@hyperledger/cactus-common"; + +export interface IViewMetadata { + viewId: string; + viewProof: { + transactionsMerkleRoot: string; + statesMerkleRoot: string; + }; + policy?: IPrivacyPolicyValue; + creator: string; + signature: string; +} + export class View { private key: string; private snapshot: Snapshot; private tI: string; private tF: string; private participant: string; + private creator: string; + private oldVersionsMetadata: IViewMetadata[] = []; + private policy?: IPrivacyPolicyValue; private viewProof: { transactionsMerkleRoot: string; statesMerkleRoot: string; }; constructor( + creator: string, tI: string, tF: string, snapshot: Snapshot, id: string | undefined, ) { + this.creator = creator; this.key = id ? id : uuidV4(); // FIXME receive as input maybe this.tI = tI; this.tF = tF; @@ -27,12 +47,38 @@ export class View { snapshot.pruneStates(this.tI, this.tF); this.viewProof = this.generateViewProof(); } + + public setCreator(creator: string) { + this.creator = creator; + } + public getTI() { + return this.tI; + } + public getTF() { + return this.tF; + } + + public getCreator(): string { + return this.creator; + } + + public addPrevVersionMetadata(data: IViewMetadata) { + this.oldVersionsMetadata.push(data); + } + + public getPolicy() { + return this.policy; + } + public getKey() { return this.key; } public getSnapshot(): Snapshot { return this.snapshot; } + public updateViewProof() { + this.viewProof = this.generateViewProof(); + } private generateViewProof(): { transactionsMerkleRoot: string; statesMerkleRoot: string; @@ -76,6 +122,10 @@ export class View { return this.viewProof; } + public getParticipant(): string { + return this.participant; + } + public getAllTransactions(): Transaction[] { const transactions: Transaction[] = []; this.snapshot.getStateBins().forEach((state) => { @@ -85,4 +135,28 @@ export class View { }); return transactions; } + public setPrivacyPolicyValue(value: IPrivacyPolicyValue | undefined) { + this.policy = value; + } + public setPrivacyPolicy( + policy: PrivacyPolicyOpts, + func: IPrivacyPolicy, + signer: JsObjectSigner, + ) { + this.policy = { + policy, + policyHash: signer.dataHash(func.toString()), + }; + } + + public setParticipant(participant: string) { + this.participant = participant; + } + public setKey(key: string) { + this.key = key; + } + + public getOldVersionsMetadata(): IViewMetadata[] { + return this.oldVersionsMetadata; + } } diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/extended-state.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/extended-state.ts new file mode 100644 index 0000000000..3e094706a9 --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/extended-state.ts @@ -0,0 +1,19 @@ +import { State } from "../view-creation/state"; + +export class ExtendedState { + private states: Map; + constructor() { + this.states = new Map(); + } + public getState(viewId: string): State | undefined { + return this.states.get(viewId); + } + public setState(viewId: string, state: State) { + if (this.getState(viewId) == undefined) { + this.states.set(viewId, state); + } + } + public getStates(): Map { + return this.states; + } +} diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/integrated-view.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/integrated-view.ts new file mode 100644 index 0000000000..36e8607b59 --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/integrated-view.ts @@ -0,0 +1,167 @@ +import MerkleTree from "merkletreejs"; +import { v4 as uuidV4 } from "uuid"; +import { State } from "../view-creation/state"; +import { ExtendedState } from "./extended-state"; +import { IMergePolicy, IMergePolicyValue } from "./merge-policies"; + +import { Transaction } from "../view-creation/transaction"; +import { IViewMetadata } from "../view-creation/view"; +import { MergePolicyOpts } from "../generated/openapi/typescript-axios"; +import { JsObjectSigner } from "@hyperledger/cactus-common"; + +export class IntegratedView { + private id: string; + private stateList: Map; + private tI: string; + private tF: string; + private participants: string[]; + //metadata about the views included in the Integrated View + private viewsMetadata: IViewMetadata[] = []; + //id of the privacy policy in the plugin instance, and hash of the policy function code used. + private integratedViewProof: { + transactionsMerkleRoot: string; + statesMerkleRoot: string; + viewsMerkleRoot: string; + }; + private privacyPolicy: IMergePolicyValue; + constructor( + privacyPolicyId: MergePolicyOpts, + privacyPolicy: IMergePolicy | undefined, + signer: JsObjectSigner, + ) { + this.stateList = new Map(); + this.id = uuidV4(); + //these are invalid values, they are changed in the process of merging Views + this.tI = "-1"; + this.tF = "-1"; + + this.participants = []; + this.privacyPolicy = { + policy: privacyPolicyId, + policyHash: privacyPolicy + ? signer.dataHash(privacyPolicy.toString()) + : undefined, + }; + + this.integratedViewProof = this.generateIntegratedViewProof(); + } + public getTI(): string { + return this.tI; + } + + public addIncludedViewMetadata(data: IViewMetadata) { + this.viewsMetadata.push(data); + } + public setTI(tI: string) { + this.tI = tI; + } + public getTF(): string { + return this.tF; + } + public setTF(TF: string) { + this.tF = TF; + } + public addParticipant(participant: string) { + this.participants.push(participant); + } + public isParticipant(participant: string): boolean { + return this.participants.includes(participant); + } + public getExtendedState(stateId: string): ExtendedState | undefined { + return this.stateList.get(stateId); + } + + public setIntegratedViewProof() { + this.integratedViewProof = this.generateIntegratedViewProof(); + } + + public getIntegratedViewProof(): { + transactionsMerkleRoot: string; + statesMerkleRoot: string; + } { + return this.integratedViewProof; + } + + public getExtendedStates(): Map { + return this.stateList; + } + + public createExtendedState(stateId: string) { + if (this.getExtendedState(stateId) == undefined) { + this.stateList.set(stateId, new ExtendedState()); + } + } + public addStateInExtendedState( + stateId: string, + viewId: string, + state: State, + ) { + const extendedState = this.getExtendedState(stateId); + if (extendedState != undefined) { + extendedState.setState(viewId, state); + } + } + + public getState(stateId: string, viewId: string): State | undefined { + return this.getExtendedState(stateId)?.getState(viewId); + } + + private generateIntegratedViewProof(): { + transactionsMerkleRoot: string; + statesMerkleRoot: string; + viewsMerkleRoot: string; + } { + const states: string[] = []; + const transactions: string[] = []; + this.getAllTransactions().forEach((transaction) => { + transactions.push(JSON.stringify(transaction.getProof())); + }); + + this.getAllStates().forEach((state) => { + states.push(JSON.stringify(state.getStateProof())); + }); + const statesTree = new MerkleTree(states, undefined, { + sort: true, + hashLeaves: true, + }); + const transactionsTree = new MerkleTree(transactions, undefined, { + sort: true, + hashLeaves: true, + }); + const viewsTree = new MerkleTree( + this.viewsMetadata.map((x) => JSON.stringify(x)), + undefined, + { + sort: true, + hashLeaves: true, + }, + ); + return { + transactionsMerkleRoot: transactionsTree.getRoot().toString("hex"), + statesMerkleRoot: statesTree.getRoot().toString("hex"), + viewsMerkleRoot: viewsTree.getRoot().toString("hex"), + }; + } + + public getAllTransactions(): Transaction[] { + const transactions: Transaction[] = []; + Array.from(this.getExtendedStates().values()).forEach((extendedState) => { + for (const state of Array.from(extendedState.getStates().values())) { + for (const transaction of state.getTransactions()) { + transactions.push(transaction); + } + } + }); + return transactions; + } + + public getAllStates(): State[] { + const states: State[] = []; + Array.from(this.getExtendedStates().values()).forEach((extendedState) => { + for (const state of Array.from(extendedState.getStates().values())) { + states.push(state); + } + }); + return states; + } +} diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/merge-policies.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/merge-policies.ts new file mode 100644 index 0000000000..ec0efaa494 --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/view-merging/merge-policies.ts @@ -0,0 +1,44 @@ +import { MergePolicyOpts } from "../generated/openapi/typescript-axios"; +import { IntegratedView } from "./integrated-view"; +export interface IMergePolicy { + (view: IntegratedView, ...args: string[]): IntegratedView; +} + +export interface IMergePolicyValue { + policy: MergePolicyOpts; + policyHash?: string; //undefined if policy is NONE +} +export class MergePolicies { + constructor() {} + + public pruneState(view: IntegratedView, stateId: string): IntegratedView { + view.getExtendedStates().delete(stateId); + return view; + } + + public pruneStateFromView( + view: IntegratedView, + stateId: string, + viewId: string, + ): IntegratedView { + view.getExtendedState(stateId)?.getStates().delete(viewId); + return view; + } + + public getMergePolicy(opts: MergePolicyOpts): IMergePolicy | undefined { + switch (opts) { + case MergePolicyOpts.NONE: + return undefined; + break; + case MergePolicyOpts.PruneState: + return this.pruneState; + break; + case MergePolicyOpts.PruneStateFromView: + return this.pruneStateFromView; + break; + default: + return undefined; + break; + } + } +} diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/web-services/merge-views-endpoint.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/web-services/merge-views-endpoint.ts new file mode 100644 index 0000000000..4a697f9c98 --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/web-services/merge-views-endpoint.ts @@ -0,0 +1,104 @@ +import type { Express, Request, Response } from "express"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, + IEndpointAuthzOptions, +} from "@hyperledger/cactus-core-api/"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; + +import { + handleRestEndpointException, + registerWebServiceEndpoint, +} from "@hyperledger/cactus-core"; + +import OAS from "../../json/openapi.json"; +import { PluginBungeeHermes } from "../plugin-bungee-hermes"; + +export interface MergeViewsEndpointOptions { + logLevel?: LogLevelDesc; + bungee: PluginBungeeHermes; +} + +export class MergeViewsEndpointV1 implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "ClientEndpointV1"; + + private readonly log: Logger; + + public get className(): string { + return MergeViewsEndpointV1.CLASS_NAME; + } + + constructor(public readonly options: MergeViewsEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.bungee, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public getPath(): string { + const apiPath = + OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/merge-views" + ]; + return apiPath.get["x-hyperledger-cacti"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = + OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/merge-views" + ]; + return apiPath.get["x-hyperledger-cacti"].http.verbLowerCase; + } + + public getOperationId(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/merge-views" + ].get.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public async handleRequest(req: Request, res: Response): Promise { + const fnTag = `${this.className}#handleRequest()`; + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + try { + const view = this.options.bungee.onMergeViews(req.body); + res.status(200).json(view); + } catch (ex: unknown) { + const errorMsg = `${fnTag} request handler fn crashed for: ${reqTag}`; + handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + } + } +} diff --git a/packages/cactus-plugin-bungee-hermes/src/main/typescript/web-services/process-view-endpoint.ts b/packages/cactus-plugin-bungee-hermes/src/main/typescript/web-services/process-view-endpoint.ts new file mode 100644 index 0000000000..86c9a83416 --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/main/typescript/web-services/process-view-endpoint.ts @@ -0,0 +1,104 @@ +import type { Express, Request, Response } from "express"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, + IEndpointAuthzOptions, +} from "@hyperledger/cactus-core-api/"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; + +import { + handleRestEndpointException, + registerWebServiceEndpoint, +} from "@hyperledger/cactus-core"; + +import OAS from "../../json/openapi.json"; +import { PluginBungeeHermes } from "../plugin-bungee-hermes"; + +export interface ProcessViewEndpointOptions { + logLevel?: LogLevelDesc; + bungee: PluginBungeeHermes; +} + +export class ProcessViewEndpointV1 implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "ClientEndpointV1"; + + private readonly log: Logger; + + public get className(): string { + return ProcessViewEndpointV1.CLASS_NAME; + } + + constructor(public readonly options: ProcessViewEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.bungee, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public getPath(): string { + const apiPath = + OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/process-view" + ]; + return apiPath.get["x-hyperledger-cacti"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = + OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/process-view" + ]; + return apiPath.get["x-hyperledger-cacti"].http.verbLowerCase; + } + + public getOperationId(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-bungee-hermes/process-view" + ].get.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public async handleRequest(req: Request, res: Response): Promise { + const fnTag = `${this.className}#handleRequest()`; + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + try { + const view = this.options.bungee.onProcessView(req.body); + res.status(200).json(view); + } catch (ex: unknown) { + const errorMsg = `${fnTag} request handler fn crashed for: ${reqTag}`; + handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); + } + } +} diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-merge-views.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-merge-views.test.ts new file mode 100644 index 0000000000..bc0519c739 --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-merge-views.test.ts @@ -0,0 +1,419 @@ +import { + IListenOptions, + LogLevelDesc, + LoggerProvider, + Secp256k1Keys, + Servers, +} from "@hyperledger/cactus-common"; +import "jest-extended"; +import LockAssetContractJson from "../solidity/lock-asset-contract/LockAsset.json"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import bodyParser from "body-parser"; + +import http, { Server } from "http"; +import { Server as SocketIoServer } from "socket.io"; + +import express from "express"; +import { AddressInfo } from "net"; +import { v4 as uuidv4 } from "uuid"; +import { + BesuTestLedger, + pruneDockerAllIfGithubAction, + Containers, +} from "@hyperledger/cactus-test-tooling"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { + Web3SigningCredentialType, + PluginLedgerConnectorBesu, + EthContractInvocationType, + ReceiptType, + IPluginLedgerConnectorBesuOptions, + Web3SigningCredential, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; +import Web3 from "web3"; +import { Account } from "web3-core"; +import { + PluginBungeeHermes, + IPluginBungeeHermesOptions, +} from "../../../main/typescript/plugin-bungee-hermes"; +import { + DefaultApi as BungeeApi, + MergePolicyOpts, +} from "../../../main/typescript/generated/openapi/typescript-axios/api"; +import { + BesuNetworkDetails, + StrategyBesu, +} from "../../../main/typescript/strategy/strategy-besu"; +import { View } from "../../../main/typescript/view-creation/view"; +const logLevel: LogLevelDesc = "INFO"; + +let besuLedger: BesuTestLedger; +let contractName: string; + +let rpcApiHttpHost: string; +let rpcApiWsHost: string; +let web3: Web3; +let firstHighNetWorthAccount: string; +let connector: PluginLedgerConnectorBesu; +let besuKeyPair: { privateKey: string }; +let testEthAccount: Account; +const BESU_ASSET_ID = uuidv4(); + +const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: "BUNGEE - Hermes", +}); +let besuPath: string; +let pluginBungeeHermesOptions: IPluginBungeeHermesOptions; +let besuServer: Server; + +let bungeeSigningCredential: Web3SigningCredential; +let bungeeKeychainId: string; +let bungeeContractAddress: string; +let bungeeServer: Server; + +let keychainPlugin: PluginKeychainMemory; + +beforeAll(async () => { + pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); + + { + besuLedger = new BesuTestLedger({ + logLevel, + emitContainerLogs: true, + envVars: ["BESU_NETWORK=dev"], + }); + await besuLedger.start(); + + rpcApiHttpHost = await besuLedger.getRpcApiHttpHost(); + rpcApiWsHost = await besuLedger.getRpcApiWsHost(); + web3 = new Web3(rpcApiHttpHost); + firstHighNetWorthAccount = besuLedger.getGenesisAccountPubKey(); + + testEthAccount = await besuLedger.createEthTestAccount(); + + besuKeyPair = { + privateKey: besuLedger.getGenesisAccountPrivKey(), + }; + + contractName = "LockAsset"; + + const keychainEntryValue = besuKeyPair.privateKey; + const keychainEntryKey = uuidv4(); + keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + + backend: new Map([[keychainEntryKey, keychainEntryValue]]), + logLevel, + }); + keychainPlugin.set( + LockAssetContractJson.contractName, + JSON.stringify(LockAssetContractJson), + ); + + const pluginRegistry = new PluginRegistry({ + plugins: [keychainPlugin], + }); + + const options: IPluginLedgerConnectorBesuOptions = { + instanceId: uuidv4(), + rpcApiHttpHost, + rpcApiWsHost, + pluginRegistry, + logLevel, + }; + connector = new PluginLedgerConnectorBesu(options); + pluginRegistry.add(connector); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + besuServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 4000, + server: besuServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await connector.getOrCreateWebServices(); + const wsApi = new SocketIoServer(besuServer, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.registerWebServices(expressApp, wsApi); + besuPath = `http://${address}:${port}`; + + await connector.transact({ + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NodeTxPoolAck, + }, + transactionConfig: { + from: firstHighNetWorthAccount, + to: testEthAccount.address, + value: 10e9, + gas: 1000000, + }, + }); + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(parseInt(balance, 10)).toBeGreaterThan(10e9); + + log.info("Connector initialized"); + + const deployOut = await connector.deployContract({ + keychainId: keychainPlugin.getKeychainId(), + contractName: LockAssetContractJson.contractName, + contractAbi: LockAssetContractJson.abi, + constructorArgs: [], + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + bytecode: LockAssetContractJson.bytecode, + gas: 1000000, + }); + expect(deployOut).toBeTruthy(); + expect(deployOut.transactionReceipt).toBeTruthy(); + expect(deployOut.transactionReceipt.contractAddress).toBeTruthy(); + log.info("Contract Deployed successfully"); + + const res = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "createAsset", + params: [BESU_ASSET_ID, 19], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(res).toBeTruthy(); + expect(res.success).toBeTruthy(); + + const res3 = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Call, + methodName: "getAsset", + params: [BESU_ASSET_ID], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(res3).toBeTruthy(); + expect(res3.success).toBeTruthy(); + expect(res3.callOutput.toString()).toBeTruthy(); + + bungeeSigningCredential = { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }; + bungeeKeychainId = keychainPlugin.getKeychainId(); + + bungeeContractAddress = deployOut.transactionReceipt + .contractAddress as string; + + pluginBungeeHermesOptions = { + keyPair: Secp256k1Keys.generateKeyPairsBuffer(), + instanceId: uuidv4(), + logLevel, + }; + } +}); + +test("test merging views, and integrated view proofs", async () => { + const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); + const strategy = "BESU"; + bungee.addStrategy(strategy, new StrategyBesu("INFO")); + const networkDetails: BesuNetworkDetails = { + signingCredential: bungeeSigningCredential, + contractName, + connectorApiPath: besuPath, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + }; + + const snapshot = await bungee.generateSnapshot([], strategy, networkDetails); + const view = bungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + //changing BESU_ASSET_ID value + const lockAsset = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "lockAsset", + params: [BESU_ASSET_ID], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(lockAsset).not.toBeUndefined(); + expect(lockAsset.success).toBeTrue(); + + //creating new asset + const new_asset_id = uuidv4(); + const depNew = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "createAsset", + params: [new_asset_id, 10], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(depNew).not.toBeUndefined(); + expect(depNew.success).toBeTrue(); + + const snapshot1 = await bungee.generateSnapshot([], strategy, networkDetails); + const view2 = bungee.generateView( + snapshot1, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view2.view).toBeTruthy(); + expect(view2.signature).toBeTruthy(); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + bungeeServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 3000, + server: bungeeServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await bungee.getOrCreateWebServices(); + await bungee.registerWebServices(expressApp); + const bungeePath = `http://${address}:${port}`; + + const config = new Configuration({ basePath: bungeePath }); + const bungeeApi = new BungeeApi(config); + + const mergeViewsNoPolicyReq = await bungeeApi.mergeViewsV1({ + serializedViews: [ + JSON.stringify({ + view: JSON.stringify(view.view as View), + signature: view.signature, + }), + // eslint-disable-next-line prettier/prettier + JSON.stringify({ view: JSON.stringify(view2.view as View), signature: view2.signature }), + ], + mergePolicy: MergePolicyOpts.NONE, + }); + expect(mergeViewsNoPolicyReq.status).toBe(200); + + expect(mergeViewsNoPolicyReq.data.integratedView).toBeTruthy(); + expect(mergeViewsNoPolicyReq.data.signature).toBeTruthy(); + + const mergeViewsNoPolicy = bungee.mergeViews( + [view.view as View, view2.view as View], + [view.signature as string, view2.signature as string], + MergePolicyOpts.NONE, + [], + ); + //1 transaction captured in first view, and 3 in the second + expect(mergeViewsNoPolicy.integratedView.getAllTransactions().length).toBe(4); + //1 state captured in first view, and 2 in the second + expect(mergeViewsNoPolicy.integratedView.getAllStates().length).toBe(3); + + const transactionReceipts: string[] = []; + + mergeViewsNoPolicy.integratedView.getAllTransactions().forEach((t) => { + transactionReceipts.push(JSON.stringify(t.getProof())); + }); + expect( + ( + await bungeeApi.verifyMerkleRoot({ + input: transactionReceipts, + root: mergeViewsNoPolicy.integratedView.getIntegratedViewProof() + .transactionsMerkleRoot, + }) + ).data.result, + ).toBeTrue(); + + const mergeViewsWithPolicy = bungee.mergeViews( + [view.view as View, view2.view as View], + [view.signature as string, view2.signature as string], + MergePolicyOpts.PruneState, + [BESU_ASSET_ID], //should remove all states related to this asset + ); + + //0 transactions captured in first view, and 1 in the second (because of policy) + // eslint-disable-next-line prettier/prettier + expect(mergeViewsWithPolicy.integratedView.getAllTransactions().length).toBe(1); + //0 state captured in first view, and 1 in the second (because of policy) + expect(mergeViewsWithPolicy.integratedView.getAllStates().length).toBe(1); + + const mergeViewsWithPolicy2 = bungee.mergeViews( + [view.view as View, view2.view as View], + [view.signature as string, view2.signature as string], + MergePolicyOpts.PruneStateFromView, + [BESU_ASSET_ID, view2.view?.getKey() as string], //should remove all states related to this asset + ); + + //1 transactions captured in first view, and 1 in the second (because of policy) + // eslint-disable-next-line prettier/prettier + expect(mergeViewsWithPolicy2.integratedView.getAllTransactions().length).toBe(2); + //1 state captured in first view, and only 1 in the second (because of policy) + expect(mergeViewsWithPolicy2.integratedView.getAllStates().length).toBe(2); +}); + +afterAll(async () => { + await Servers.shutdown(besuServer); + await Servers.shutdown(bungeeServer); + await besuLedger.stop(); + await besuLedger.destroy(); + + await pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); +}); diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-process-views.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-process-views.test.ts new file mode 100644 index 0000000000..2daeba4518 --- /dev/null +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/bungee-process-views.test.ts @@ -0,0 +1,367 @@ +import { + IListenOptions, + LogLevelDesc, + LoggerProvider, + Secp256k1Keys, + Servers, +} from "@hyperledger/cactus-common"; +import "jest-extended"; +import LockAssetContractJson from "../solidity/lock-asset-contract/LockAsset.json"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import bodyParser from "body-parser"; + +import http, { Server } from "http"; +import { Server as SocketIoServer } from "socket.io"; + +import express from "express"; +import { AddressInfo } from "net"; +import { v4 as uuidv4 } from "uuid"; +import { + BesuTestLedger, + pruneDockerAllIfGithubAction, + Containers, +} from "@hyperledger/cactus-test-tooling"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { + Web3SigningCredentialType, + PluginLedgerConnectorBesu, + EthContractInvocationType, + ReceiptType, + IPluginLedgerConnectorBesuOptions, + Web3SigningCredential, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; +import Web3 from "web3"; +import { Account } from "web3-core"; +import { + PluginBungeeHermes, + IPluginBungeeHermesOptions, +} from "../../../main/typescript/plugin-bungee-hermes"; +import { + DefaultApi as BungeeApi, + PrivacyPolicyOpts, +} from "../../../main/typescript/generated/openapi/typescript-axios/api"; +import { + BesuNetworkDetails, + StrategyBesu, +} from "../../../main/typescript/strategy/strategy-besu"; +import { View } from "../../../main/typescript/view-creation/view"; +import { deserializeView } from "../../../main/typescript/utils"; + +const logLevel: LogLevelDesc = "INFO"; + +let besuLedger: BesuTestLedger; +let contractName: string; + +let rpcApiHttpHost: string; +let rpcApiWsHost: string; +let web3: Web3; +let firstHighNetWorthAccount: string; +let connector: PluginLedgerConnectorBesu; +let besuKeyPair: { privateKey: string }; +let testEthAccount: Account; +const BESU_ASSET_ID = uuidv4(); + +const log = LoggerProvider.getOrCreate({ + level: logLevel, + label: "BUNGEE - Hermes", +}); +let besuPath: string; +let pluginBungeeHermesOptions: IPluginBungeeHermesOptions; +let besuServer: Server; + +let bungeeSigningCredential: Web3SigningCredential; +let bungeeKeychainId: string; +let bungeeContractAddress: string; +let bungeeServer: Server; + +let keychainPlugin: PluginKeychainMemory; + +beforeAll(async () => { + pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); + + { + besuLedger = new BesuTestLedger({ + logLevel, + emitContainerLogs: true, + envVars: ["BESU_NETWORK=dev"], + }); + await besuLedger.start(); + + rpcApiHttpHost = await besuLedger.getRpcApiHttpHost(); + rpcApiWsHost = await besuLedger.getRpcApiWsHost(); + web3 = new Web3(rpcApiHttpHost); + firstHighNetWorthAccount = besuLedger.getGenesisAccountPubKey(); + + testEthAccount = await besuLedger.createEthTestAccount(); + + besuKeyPair = { + privateKey: besuLedger.getGenesisAccountPrivKey(), + }; + + contractName = "LockAsset"; + + const keychainEntryValue = besuKeyPair.privateKey; + const keychainEntryKey = uuidv4(); + keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + + backend: new Map([[keychainEntryKey, keychainEntryValue]]), + logLevel, + }); + keychainPlugin.set( + LockAssetContractJson.contractName, + JSON.stringify(LockAssetContractJson), + ); + + const pluginRegistry = new PluginRegistry({ + plugins: [keychainPlugin], + }); + + const options: IPluginLedgerConnectorBesuOptions = { + instanceId: uuidv4(), + rpcApiHttpHost, + rpcApiWsHost, + pluginRegistry, + logLevel, + }; + connector = new PluginLedgerConnectorBesu(options); + pluginRegistry.add(connector); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + besuServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 4000, + server: besuServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await connector.getOrCreateWebServices(); + const wsApi = new SocketIoServer(besuServer, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.registerWebServices(expressApp, wsApi); + besuPath = `http://${address}:${port}`; + + await connector.transact({ + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NodeTxPoolAck, + }, + transactionConfig: { + from: firstHighNetWorthAccount, + to: testEthAccount.address, + value: 10e9, + gas: 1000000, + }, + }); + const balance = await web3.eth.getBalance(testEthAccount.address); + expect(balance).toBeTruthy(); + expect(parseInt(balance, 10)).toBeGreaterThan(10e9); + + log.info("Connector initialized"); + + const deployOut = await connector.deployContract({ + keychainId: keychainPlugin.getKeychainId(), + contractName: LockAssetContractJson.contractName, + contractAbi: LockAssetContractJson.abi, + constructorArgs: [], + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + bytecode: LockAssetContractJson.bytecode, + gas: 1000000, + }); + expect(deployOut).toBeTruthy(); + expect(deployOut.transactionReceipt).toBeTruthy(); + expect(deployOut.transactionReceipt.contractAddress).toBeTruthy(); + log.info("Contract Deployed successfully"); + + const res = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "createAsset", + params: [BESU_ASSET_ID, 19], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(res).toBeTruthy(); + expect(res.success).toBeTruthy(); + + const res3 = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Call, + methodName: "getAsset", + params: [BESU_ASSET_ID], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(res3).toBeTruthy(); + expect(res3.success).toBeTruthy(); + expect(res3.callOutput.toString()).toBeTruthy(); + + //changing BESU_ASSET_ID value + const lockAsset = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "lockAsset", + params: [BESU_ASSET_ID], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(lockAsset).not.toBeUndefined(); + expect(lockAsset.success).toBeTrue(); + + //creating new asset + const new_asset_id = uuidv4(); + const depNew = await connector.invokeContract({ + contractName, + keychainId: keychainPlugin.getKeychainId(), + invocationType: EthContractInvocationType.Send, + methodName: "createAsset", + params: [new_asset_id, 10], + signingCredential: { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(depNew).not.toBeUndefined(); + expect(depNew.success).toBeTrue(); + + bungeeSigningCredential = { + ethAccount: firstHighNetWorthAccount, + secret: besuKeyPair.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }; + bungeeKeychainId = keychainPlugin.getKeychainId(); + + bungeeContractAddress = deployOut.transactionReceipt + .contractAddress as string; + + pluginBungeeHermesOptions = { + keyPair: Secp256k1Keys.generateKeyPairsBuffer(), + instanceId: uuidv4(), + logLevel, + }; + } +}); + +test("test merging views, and integrated view proofs", async () => { + const bungee = new PluginBungeeHermes(pluginBungeeHermesOptions); + const strategy = "BESU"; + bungee.addStrategy(strategy, new StrategyBesu("INFO")); + const networkDetails: BesuNetworkDetails = { + signingCredential: bungeeSigningCredential, + contractName, + connectorApiPath: besuPath, + keychainId: bungeeKeychainId, + contractAddress: bungeeContractAddress, + participant: firstHighNetWorthAccount, + }; + + const snapshot = await bungee.generateSnapshot([], strategy, networkDetails); + const view = bungee.generateView( + snapshot, + "0", + Number.MAX_SAFE_INTEGER.toString(), + undefined, + ); + //expect to return a view + expect(view.view).toBeTruthy(); + expect(view.signature).toBeTruthy(); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + bungeeServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 3000, + server: bungeeServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + await bungee.getOrCreateWebServices(); + await bungee.registerWebServices(expressApp); + const bungeePath = `http://${address}:${port}`; + + const config = new Configuration({ basePath: bungeePath }); + const bungeeApi = new BungeeApi(config); + + const processed = await bungeeApi.processViewV1({ + serializedView: JSON.stringify({ + view: JSON.stringify(view.view as View), + signature: view.signature, + }), + policyId: PrivacyPolicyOpts.PruneState, + policyArguments: [BESU_ASSET_ID], + }); + + expect(processed.status).toBe(200); + expect(processed.data.view).toBeTruthy(); + expect(processed.data.signature).toBeTruthy(); + + const processedView = deserializeView(JSON.stringify(processed.data)); + + //check view deserializer + expect(JSON.stringify(processedView)).toEqual(processed.data.view); + + expect(processedView.getPolicy()).toBeTruthy(); + expect(processedView.getOldVersionsMetadata().length).toBe(1); + expect(processedView.getOldVersionsMetadata()[0].signature).toBe( + view.signature, + ); + expect(processedView.getAllTransactions().length).toBe(1); +}); + +afterAll(async () => { + await Servers.shutdown(besuServer); + await Servers.shutdown(bungeeServer); + await besuLedger.stop(); + await besuLedger.destroy(); + + await pruneDockerAllIfGithubAction({ logLevel }) + .then(() => { + log.info("Pruning throw OK"); + }) + .catch(async () => { + await Containers.logDiagnostics({ logLevel }); + fail("Pruning didn't throw OK"); + }); +}); diff --git a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-pruning.test.ts b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-pruning.test.ts index 4bc26c4727..0a810c00ff 100644 --- a/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-pruning.test.ts +++ b/packages/cactus-plugin-bungee-hermes/src/test/typescript/integration/fabric-test-pruning.test.ts @@ -395,7 +395,8 @@ test("test creation of views for specific timeframes", async () => { //tI is the time of the first transaction +1 const tI = ( - parseInt(snapshot.getStateBins()[0].getTransactions()[0].getTimeStamp()) + 1 + BigInt(snapshot.getStateBins()[0].getTransactions()[0].getTimeStamp()) + + BigInt(1) ).toString(); expect(snapshot1.getStateBins().length).toEqual(1); @@ -410,7 +411,6 @@ test("test creation of views for specific timeframes", async () => { Number.MAX_SAFE_INTEGER.toString(), undefined, ); - //expect to return a view expect(view1.view).toBeTruthy(); expect(view1.signature).toBeTruthy();