From 3c944d601d5824eaf3cc6a9a8af1f8a6e5fe6db3 Mon Sep 17 00:00:00 2001 From: aldousalvarez Date: Wed, 15 Feb 2023 19:28:10 +0800 Subject: [PATCH] feat(quorum): private transaction support ----------------------------------------- - Added v2.3.0-deploy-contract-from-json-private.test.ts that would support private transaction test for Quorum. - Added a QuorumPrivateTransactionConfig on openapi.json - Added Web3JsQuorum on plugin-ledger-connector-quorum and added a transact private method to be able to proceed with the private transaction. - Currently the privateUrl in the test is being truely optional and we need to address this in the future. - We are just passing privateUrl in all the instances even though we don't need to do private transactions everywhere. Fixes #951 Co-authored-by: Travis Payne Co-authored-by: johnhomantaring Co-authored-by: jagpreetsinghsasan Co-authored-by: aldousalvarez aldousss.alvarez@gmail.com Co-authored-by: Peter Somogyvari Signed-off-by: Peter Somogyvari --- .../src/main/json/openapi.json | 102 +++++- .../generated/openapi/typescript-axios/api.ts | 131 ++++++- .../plugin-ledger-connector-quorum.ts | 148 ++++++-- .../web-services/invoke-contract-endpoint.ts | 3 +- .../web-services/run-transaction-endpoint.ts | 3 +- ...-deploy-contract-from-json-private.test.ts | 325 ++++++++++++++++++ .../api-client-routing-node-to-node.test.ts | 1 + .../quorum/quorum-mp-test-ledger.ts | 7 + typings/web3js-quorum/index.d.ts | 2 +- 9 files changed, 692 insertions(+), 30 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-private.test.ts diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json index f237394336..b474ddd8a7 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json @@ -301,6 +301,46 @@ } } }, + "QuorumPrivateTransactionConfig" : { + "type": "object", + "required" : [ + "privateFor" + ], + "properties" : { + "privateFrom": { + "type": "string", + "nullable": false + }, + "privateFor": { + "type": "array", + "default": [], + "items": {}, + "nullable": false + }, + "isPrivate": { + "type": "boolean", + "default": false, + "nullable": false + }, + "gasPrice": { + "type": "number", + "nullable": false + }, + "gasLimit": { + "type": "number", + "nullable": false + }, + "privateKey": { + "type": "string", + "nullable": false + }, + "privacyGroupId": { + "type": "string", + "nullable": false + } + + } + }, "Web3TransactionReceipt": { "type": "object", "required": [ @@ -354,7 +394,33 @@ "to": { "type": "string", "nullable": false - } + }, + "logs": { + "type": "array", + "default": [], + "items": {}, + "nullable": false + }, + "logsBloom": { + "type": "string", + "nullable": false + }, + "revertReason": { + "type": "string", + "nullable": false + }, + "output": { + "type": "string", + "nullable": false + }, + "commitmentHash": { + "type": "string", + "nullable": false + }, + "cumulativeGasUSed": { + "type": "number", + "nullable": false + } } }, "ContractJSON": { @@ -436,6 +502,9 @@ "minimum": 0, "default": 60000, "nullable": false + }, + "privateTransactionConfig": { + "$ref": "#/components/schemas/QuorumPrivateTransactionConfig" } } }, @@ -448,7 +517,7 @@ "transactionReceipt": { "$ref": "#/components/schemas/Web3TransactionReceipt" } - } + } }, "DeployContractSolidityBytecodeV1Request": { "type": "object", @@ -466,10 +535,23 @@ "maxLength": 100, "nullable": false }, + "contractAbi": { + "description": "The application binary interface of the solidity contract", + "type": "array", + "items": {}, + "nullable": false + }, "web3SigningCredential": { "$ref": "#/components/schemas/Web3SigningCredential", "nullable": false }, + "bytecode": { + "type": "string", + "nullable": false, + "minLength": 1, + "maxLength": 24576, + "description": "See https://ethereum.stackexchange.com/a/47556 regarding the maximum length of the bytecode" + }, "keychainId": { "type": "string", "description": "The keychainId for retrieve the contracts json.", @@ -482,7 +564,15 @@ "nullable": false }, "gasPrice": { - "type": "string", + "type": "number", + "nullable": false + }, + "nonce": { + "type": "number", + "nullable": false + }, + "value": { + "type": "number", "nullable": false }, "timeoutMs": { @@ -502,6 +592,9 @@ "type": "array", "default": [], "items": {} + }, + "privateTransactionConfig": { + "$ref": "#/components/schemas/QuorumPrivateTransactionConfig" } } }, @@ -726,6 +819,9 @@ "$ref": "#/components/schemas/ContractJSON", "description": "For use when not using keychain, pass the contract in as this variable", "nullable": false + }, + "privateTransactionConfig": { + "$ref": "#/components/schemas/QuorumPrivateTransactionConfig" } } }, diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts index 9727493827..ad2deaf599 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -163,12 +163,24 @@ export interface DeployContractSolidityBytecodeV1Request { * @memberof DeployContractSolidityBytecodeV1Request */ contractName: string; + /** + * The application binary interface of the solidity contract + * @type {Array} + * @memberof DeployContractSolidityBytecodeV1Request + */ + contractAbi?: Array; /** * * @type {Web3SigningCredential} * @memberof DeployContractSolidityBytecodeV1Request */ web3SigningCredential: Web3SigningCredential; + /** + * See https://ethereum.stackexchange.com/a/47556 regarding the maximum length of the bytecode + * @type {string} + * @memberof DeployContractSolidityBytecodeV1Request + */ + bytecode?: string; /** * The keychainId for retrieve the contracts json. * @type {string} @@ -183,10 +195,22 @@ export interface DeployContractSolidityBytecodeV1Request { gas?: number; /** * - * @type {string} + * @type {number} * @memberof DeployContractSolidityBytecodeV1Request */ - gasPrice?: string; + gasPrice?: number; + /** + * + * @type {number} + * @memberof DeployContractSolidityBytecodeV1Request + */ + nonce?: number; + /** + * + * @type {number} + * @memberof DeployContractSolidityBytecodeV1Request + */ + value?: number; /** * The amount of milliseconds to wait for a transaction receipt with theaddress of the contract(which indicates successful deployment) beforegiving up and crashing. * @type {number} @@ -205,6 +229,12 @@ export interface DeployContractSolidityBytecodeV1Request { * @memberof DeployContractSolidityBytecodeV1Request */ constructorArgs?: Array; + /** + * + * @type {QuorumPrivateTransactionConfig} + * @memberof DeployContractSolidityBytecodeV1Request + */ + privateTransactionConfig?: QuorumPrivateTransactionConfig; } /** * @@ -315,6 +345,12 @@ export interface InvokeContractJsonObjectV1Request { * @memberof InvokeContractJsonObjectV1Request */ contractJSON: ContractJSON; + /** + * + * @type {QuorumPrivateTransactionConfig} + * @memberof InvokeContractJsonObjectV1Request + */ + privateTransactionConfig?: QuorumPrivateTransactionConfig; } /** * @@ -526,6 +562,55 @@ export interface InvokeRawWeb3EthMethodV1Response { */ errorDetail?: string; } +/** + * + * @export + * @interface QuorumPrivateTransactionConfig + */ +export interface QuorumPrivateTransactionConfig { + /** + * + * @type {string} + * @memberof QuorumPrivateTransactionConfig + */ + privateFrom?: string; + /** + * + * @type {Array} + * @memberof QuorumPrivateTransactionConfig + */ + privateFor: Array; + /** + * + * @type {boolean} + * @memberof QuorumPrivateTransactionConfig + */ + isPrivate?: boolean; + /** + * + * @type {number} + * @memberof QuorumPrivateTransactionConfig + */ + gasPrice?: number; + /** + * + * @type {number} + * @memberof QuorumPrivateTransactionConfig + */ + gasLimit?: number; + /** + * + * @type {string} + * @memberof QuorumPrivateTransactionConfig + */ + privateKey?: string; + /** + * + * @type {string} + * @memberof QuorumPrivateTransactionConfig + */ + privacyGroupId?: string; +} /** * * @export @@ -607,6 +692,12 @@ export interface RunTransactionRequest { * @memberof RunTransactionRequest */ timeoutMs?: number; + /** + * + * @type {QuorumPrivateTransactionConfig} + * @memberof RunTransactionRequest + */ + privateTransactionConfig?: QuorumPrivateTransactionConfig; } /** * @@ -1230,6 +1321,42 @@ export interface Web3TransactionReceipt { * @memberof Web3TransactionReceipt */ to: string; + /** + * + * @type {Array} + * @memberof Web3TransactionReceipt + */ + logs?: Array; + /** + * + * @type {string} + * @memberof Web3TransactionReceipt + */ + logsBloom?: string; + /** + * + * @type {string} + * @memberof Web3TransactionReceipt + */ + revertReason?: string; + /** + * + * @type {string} + * @memberof Web3TransactionReceipt + */ + output?: string; + /** + * + * @type {string} + * @memberof Web3TransactionReceipt + */ + commitmentHash?: string; + /** + * + * @type {number} + * @memberof Web3TransactionReceipt + */ + cumulativeGasUSed?: number; } /** diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts index 60945a86bf..5884e95770 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts @@ -7,6 +7,11 @@ import type { import { Express } from "express"; import Web3 from "web3"; +import Web3JsQuorum, { + IWeb3Quorum, + ISendRawTransaction, + IPrivateTransactionReceipt, +} from "web3js-quorum"; import { AbiItem } from "web3-utils"; import { Contract } from "web3-eth-contract"; import { ContractSendMethod } from "web3-eth-contract"; @@ -78,6 +83,7 @@ export interface IPluginLedgerConnectorQuorumOptions logLevel?: LogLevelDesc; prometheusExporter?: PrometheusExporter; pluginRegistry: PluginRegistry; + privateUrl?: string; } export class PluginLedgerConnectorQuorum @@ -93,10 +99,11 @@ export class PluginLedgerConnectorQuorum private readonly pluginRegistry: PluginRegistry; public prometheusExporter: PrometheusExporter; private readonly instanceId: string; + private readonly privateUrl: string | undefined; private readonly log: Logger; private readonly web3: Web3; + private readonly web3Quorum: IWeb3Quorum; private httpServer: Server | SecureServer | null = null; - private endpoints: IWebServiceEndpoint[] | undefined; public static readonly CLASS_NAME = "PluginLedgerConnectorQuorum"; @@ -108,7 +115,6 @@ export class PluginLedgerConnectorQuorum if (!this.options.rpcApiWsHost) { return new Web3.providers.HttpProvider(this.options.rpcApiHttpHost); } - return new Web3.providers.WebsocketProvider(this.options.rpcApiWsHost); } @@ -122,8 +128,14 @@ export class PluginLedgerConnectorQuorum const level = this.options.logLevel || "INFO"; const label = this.className; this.log = LoggerProvider.getOrCreate({ level, label }); - + this.privateUrl = options.privateUrl; this.web3 = new Web3(this.getWeb3Provider()); + this.web3Quorum = Web3JsQuorum( + this.web3, + { privateUrl: this.privateUrl } as any, + true, + ); + this.instanceId = options.instanceId; this.pluginRegistry = options.pluginRegistry as PluginRegistry; this.prometheusExporter = @@ -354,7 +366,7 @@ export class PluginLedgerConnectorQuorum const contractJSON = JSON.parse(contractStr); // if not exists a contract deployed, we deploy it - const networkId = await this.web3.eth.net.getId(); + const networkId = await this.web3Quorum.eth.net.getId(); if ( !contractJSON.networks || !contractJSON.networks[networkId] || @@ -448,7 +460,7 @@ export class PluginLedgerConnectorQuorum const { params } = payload; const [transactionConfig] = params; if (!req.gas) { - req.gas = await this.web3.eth.estimateGas(transactionConfig); + req.gas = await this.web3Quorum.eth.estimateGas(transactionConfig); } transactionConfig.from = web3SigningCredential.ethAccount; transactionConfig.gas = req.gas; @@ -459,6 +471,7 @@ export class PluginLedgerConnectorQuorum const txReq: RunTransactionRequest = { transactionConfig, web3SigningCredential, + privateTransactionConfig: req.privateTransactionConfig, timeoutMs: req.timeoutMs || 60000, }; const out = await this.transact(txReq); @@ -513,7 +526,9 @@ export class PluginLedgerConnectorQuorum ): Promise { const fnTag = `${this.className}#transactSigned()`; - const receipt = await this.web3.eth.sendSignedTransaction(rawTransaction); + const receipt = await this.web3Quorum.eth.sendSignedTransaction( + rawTransaction, + ); if (receipt instanceof Error) { this.log.debug(`${fnTag} Web3 sendSignedTransaction failed`, receipt); @@ -528,11 +543,16 @@ export class PluginLedgerConnectorQuorum txIn: RunTransactionRequest, ): Promise { const fnTag = `${this.className}#transactGethKeychain()`; - const { sendTransaction } = this.web3.eth.personal; + const { sendTransaction } = this.web3Quorum.eth.personal; const { transactionConfig, web3SigningCredential } = txIn; const { secret, } = web3SigningCredential as Web3SigningCredentialGethKeychainPassword; + + if (txIn.privateTransactionConfig) { + return this.transactPrivate(txIn); + } + try { const txHash = await sendTransaction(transactionConfig, secret); const transactionReceipt = await this.pollForTxReceipt(txHash); @@ -554,6 +574,10 @@ export class PluginLedgerConnectorQuorum secret, } = web3SigningCredential as Web3SigningCredentialPrivateKeyHex; + if (req.privateTransactionConfig) { + return this.transactPrivate(req); + } + const signedTx = await this.web3.eth.accounts.signTransaction( transactionConfig, secret, @@ -569,11 +593,67 @@ export class PluginLedgerConnectorQuorum } } + public async transactPrivate( + req: RunTransactionRequest, + ): Promise { + const { web3SigningCredential } = req; + const { + secret, + } = web3SigningCredential as Web3SigningCredentialPrivateKeyHex; + + const signingAccount = this.web3Quorum.eth.accounts.privateKeyToAccount( + secret, + ); + const txCount = await this.web3Quorum.eth.getTransactionCount( + signingAccount.address, + ); + const txn = { + gasLimit: req.transactionConfig.gas, //max number of gas units the tx is allowed to use + gasPrice: req.transactionConfig.gasPrice, //ETH per unit of gas + data: req.transactionConfig.data, + privateKey: secret, + privateFrom: req.privateTransactionConfig?.privateFrom, + privateFor: req.privateTransactionConfig?.privateFor, + from: signingAccount, + isPrivate: true, + nonce: txCount, + value: 0, + } as ISendRawTransaction; + + const block = (await this.web3Quorum.priv.generateAndSendRawTransaction( + txn, + )) as unknown; + + const { transactionHash } = block as IPrivateTransactionReceipt; + + const transactionReceipt = await this.web3Quorum.priv.waitForTransactionReceipt( + transactionHash, + ); + + return { transactionReceipt: transactionReceipt }; + } + + public async getPrivateTxReceipt( + privateFrom: string, + txHash: string, + ): Promise { + const txPoolReceipt = {} as any; + + console.log(privateFrom); + console.log(txHash); + + return { transactionReceipt: txPoolReceipt }; + } + public async transactCactusKeychainRef( req: RunTransactionRequest, ): Promise { const fnTag = `${this.className}#transactCactusKeychainRef()`; - const { transactionConfig, web3SigningCredential } = req; + const { + transactionConfig, + web3SigningCredential, + privateTransactionConfig, + } = req; const { ethAccount, keychainEntryKey, @@ -596,7 +676,7 @@ export class PluginLedgerConnectorQuorum this.log.debug( `${fnTag} Gas not specified in the transaction values. Using the estimate from web3`, ); - transactionConfig.gas = await this.web3.eth.estimateGas( + transactionConfig.gas = await this.web3Quorum.eth.estimateGas( transactionConfig, ); this.log.debug( @@ -606,6 +686,7 @@ export class PluginLedgerConnectorQuorum } return this.transactPrivateKey({ + privateTransactionConfig, transactionConfig, web3SigningCredential: { ethAccount, @@ -639,10 +720,10 @@ export class PluginLedgerConnectorQuorum } private async generateBytecode(req: any): Promise { - const tmpContract = new this.web3.eth.Contract( + const tmpContracts = new this.web3Quorum.eth.Contract( (req.contractJSON as any).abi, ); - const deployment = tmpContract.deploy({ + const deployment = tmpContracts.deploy({ data: req.contractJSON.bytecode, arguments: req.constructorArgs, }); @@ -655,16 +736,39 @@ export class PluginLedgerConnectorQuorum | Web3SigningCredentialGethKeychainPassword | Web3SigningCredentialPrivateKeyHex; - const receipt = await this.transact({ - transactionConfig: { - data: await this.generateBytecode(req), - from: web3SigningCredential.ethAccount, - gas: req.gas, - gasPrice: req.gasPrice, - }, - web3SigningCredential, - }); - return receipt; + const bytecode = await this.generateBytecode(req); + + if (req.privateTransactionConfig) { + const privacyGroupId = + req.privateTransactionConfig.privacyGroupId || + this.web3Quorum.utils.generatePrivacyGroup( + req.privateTransactionConfig, + ); + this.log.info("privacyGroupId=%o", privacyGroupId); + const receipt = await this.transactPrivate({ + privateTransactionConfig: req.privateTransactionConfig, + transactionConfig: { + data: bytecode, + from: web3SigningCredential.ethAccount, + gas: req.gas, + gasPrice: req.gasPrice, + }, + web3SigningCredential, + }); + return receipt; + } else { + const receipt = await this.transact({ + privateTransactionConfig: req.privateTransactionConfig, + transactionConfig: { + data: bytecode, + from: web3SigningCredential.ethAccount, + gas: req.gas, + gasPrice: req.gasPrice, + }, + web3SigningCredential, + }); + return receipt; + } } public async deployContract( @@ -708,7 +812,7 @@ export class PluginLedgerConnectorQuorum receipt.transactionReceipt.contractAddress && receipt.transactionReceipt.contractAddress != null ) { - const networkId = await this.web3.eth.net.getId(); + const networkId = await this.web3Quorum.eth.net.getId(); const address = { address: receipt.transactionReceipt.contractAddress }; const network = { [networkId]: address }; contractJSON.networks = network; diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-contract-endpoint.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-contract-endpoint.ts index a15ea9a8ef..a84a94af1b 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-contract-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-contract-endpoint.ts @@ -17,6 +17,7 @@ import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; import { PluginLedgerConnectorQuorum } from "../plugin-ledger-connector-quorum"; import OAS from "../../json/openapi.json"; +import { InvokeContractV1Request } from "../generated/openapi/typescript-axios/api"; export interface IInvokeContractEndpointOptions { logLevel?: LogLevelDesc; @@ -84,7 +85,7 @@ export class InvokeContractEndpoint implements IWebServiceEndpoint { public async handleRequest(req: Request, res: Response): Promise { const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); - const reqBody = req.body; + const reqBody: InvokeContractV1Request = req.body; try { const resBody = await this.options.connector.getContractInfoKeychain( reqBody, diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/run-transaction-endpoint.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/run-transaction-endpoint.ts index b26134087a..fe3f4ced0a 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/run-transaction-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/run-transaction-endpoint.ts @@ -17,6 +17,7 @@ import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; import { PluginLedgerConnectorQuorum } from "../plugin-ledger-connector-quorum"; import OAS from "../../json/openapi.json"; +import { RunTransactionRequest } from "../generated/openapi/typescript-axios/api"; export interface IRunTransactionEndpointOptions { logLevel?: LogLevelDesc; @@ -84,7 +85,7 @@ export class RunTransactionEndpoint implements IWebServiceEndpoint { public async handleRequest(req: Request, res: Response): Promise { const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); - const reqBody = req.body; + const reqBody: RunTransactionRequest = req.body; try { const resBody = await this.options.connector.transact(reqBody); res.json({ success: true, data: resBody }); diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-private.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-private.test.ts new file mode 100644 index 0000000000..f53c924547 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-private.test.ts @@ -0,0 +1,325 @@ +import "jest-extended"; +import Web3 from "web3"; +import { v4 as uuidV4 } from "uuid"; + +import { AbiItem } from "web3-utils"; + +import { LogLevelDesc } from "@hyperledger/cactus-common"; + +import HelloWorldContractJson from "../../../../solidity/hello-world-contract/HelloWorld.json"; + +import Web3JsQuorum, { IWeb3Quorum } from "web3js-quorum"; + +const keyStatic = { + tessera: { + member1: { + publicKey: "BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo=", + }, + member2: { + publicKey: "QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc=", + }, + member3: { + publicKey: "1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg=", + }, + }, + quorum: { + member1: { + name: "member1", + url: "http://127.0.0.1:20000", + wsUrl: "ws://127.0.0.1:20001", + privateUrl: "http://127.0.0.1:9081", + privateKey: + "b9a4bd1539c15bcc83fa9078fe89200b6e9e802ae992f13cd83c853f16e8bed4", + accountAddress: "f0e2db6c8dc6c681bb5d6ad121a107f300e9b2b5", + }, + member2: { + name: "member2", + url: `http://127.0.0.1:20002`, + wsUrl: `http://127.0.0.1:20003`, + privateUrl: "http://127.0.0.1:9082", + privateKey: + "f18166704e19b895c1e2698ebc82b4e007e6d2933f4b31be23662dd0ec602570", + accountAddress: "ca843569e3427144cead5e4d5999a3d0ccf92b8e", + }, + member3: { + name: "member3", + url: `http://127.0.0.1:20004`, + wsUrl: `http://127.0.0.1:20005`, + privateUrl: "http://127.0.0.1:9083", + privateKey: + "4107f0b6bf67a3bc679a15fe36f640415cf4da6a4820affaac89c8b280dfd1b3", + accountAddress: "0fbdc686b912d7722dc86510934589e0aaf3b55a", + }, + }, +}; + +import { QuorumMultiPartyTestLedger } from "@hyperledger/cactus-test-tooling"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + PluginFactoryLedgerConnector, + PluginLedgerConnectorQuorum, + Web3SigningCredentialType, +} from "../../../../../main/typescript/public-api"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginImportType } from "@hyperledger/cactus-core-api"; + +const logLevel: LogLevelDesc = "INFO"; + +describe("PluginLedgerConnectorQuorum", () => { + const preWarmedLedger = process.env.CACTUS_TEST_PRE_WARMED_LEDGER === "true"; + const keychainId1 = "keychain1_" + uuidV4(); + const keychainId2 = "keychain2_" + uuidV4(); + + let keys: typeof keyStatic; + let web3JsQuorumMember1: IWeb3Quorum; + let web3JsQuorumMember2: IWeb3Quorum; + let web3JsQuorumMember3: IWeb3Quorum; + let ledger: QuorumMultiPartyTestLedger; + let connector1: PluginLedgerConnectorQuorum; + let connector2: PluginLedgerConnectorQuorum; + let connector3: PluginLedgerConnectorQuorum; + + afterAll(async () => { + if (!preWarmedLedger) { + await ledger.stop(); + } + }); + + afterAll(async () => { + await connector1.shutdown(); + }); + + afterAll(async () => { + await connector2.shutdown(); + }); + + afterAll(async () => { + await connector3.shutdown(); + }); + + beforeAll(async () => { + ledger = new QuorumMultiPartyTestLedger({ logLevel }); + + if (preWarmedLedger) { + keys = keyStatic; + } else { + await ledger.start(); + keys = (await ledger.getKeys()) as typeof keyStatic; + } + + const rpcApiHttpHostMember1 = keys.quorum.member1.url; + const rpcApiHttpHostMember2 = keys.quorum.member2.url; + const rpcApiHttpHostMember3 = keys.quorum.member3.url; + const web3Member1 = new Web3(rpcApiHttpHostMember1); + const web3Member2 = new Web3(rpcApiHttpHostMember2); + const web3Member3 = new Web3(rpcApiHttpHostMember3); + + web3JsQuorumMember1 = Web3JsQuorum( + web3Member1, + { privateUrl: keys.quorum.member1.privateUrl } as any, + true, + ); + expect(web3JsQuorumMember1).toBeTruthy(); + + web3JsQuorumMember2 = Web3JsQuorum( + web3Member2, + { privateUrl: keys.quorum.member2.privateUrl } as any, + true, + ); + expect(web3JsQuorumMember2).toBeTruthy(); + + web3JsQuorumMember3 = Web3JsQuorum( + web3Member3, + { privateUrl: keys.quorum.member3.privateUrl } as any, + true, + ); + expect(web3JsQuorumMember3).toBeTruthy(); + + const pluginRegistry1 = new PluginRegistry(); + const pluginRegistry2 = new PluginRegistry(); + const pluginRegistry3 = new PluginRegistry(); + + const pluginFactoryLedgerConnector = new PluginFactoryLedgerConnector({ + pluginImportType: PluginImportType.Local, + }); + + const keychainInstanceId1 = "keychain_instance1_" + uuidV4(); + + const keychain1 = new PluginKeychainMemory({ + instanceId: keychainInstanceId1, + keychainId: keychainId1, + logLevel, + }); + + expect(keychain1).toBeTruthy(); + + await keychain1.set( + HelloWorldContractJson.contractName, + JSON.stringify(HelloWorldContractJson), + ); + pluginRegistry1.add(keychain1); + + const keychainInstanceId2 = "keychain_instance2_" + uuidV4(); + const keychain2 = new PluginKeychainMemory({ + instanceId: keychainInstanceId2, + keychainId: keychainId2, + logLevel, + }); + + expect(keychain2).toBeTruthy(); + + await keychain2.set( + HelloWorldContractJson.contractName, + JSON.stringify(HelloWorldContractJson), + ); + + pluginRegistry2.add(keychain2); + + const keychainInstanceId3 = "keychain_instance3_" + uuidV4(); + const keychainId3 = "keychain3_" + uuidV4(); + const keychain3 = new PluginKeychainMemory({ + instanceId: keychainInstanceId3, + keychainId: keychainId3, + logLevel, + }); + + expect(keychain3).toBeTruthy(); + + await keychain3.set( + HelloWorldContractJson.contractName, + JSON.stringify(HelloWorldContractJson), + ); + pluginRegistry3.add(keychain3); + + const connectorInstanceId1 = "quorum1_" + uuidV4(); + const connectorInstanceId2 = "quorum2_" + uuidV4(); + const connectorInstanceId3 = "quorum3_" + uuidV4(); + + connector1 = await pluginFactoryLedgerConnector.create({ + instanceId: connectorInstanceId1, + pluginRegistry: pluginRegistry1, + rpcApiHttpHost: rpcApiHttpHostMember1, + privateUrl: keys.quorum.member1.privateUrl, + logLevel, + }); + expect(connector1).toBeTruthy(); + pluginRegistry1.add(connector1); + + connector2 = await pluginFactoryLedgerConnector.create({ + instanceId: connectorInstanceId2, + pluginRegistry: pluginRegistry2, + rpcApiHttpHost: rpcApiHttpHostMember2, + privateUrl: keys.quorum.member3.privateUrl, + logLevel, + }); + expect(connector2).toBeTruthy(); + pluginRegistry2.add(connector2); + + connector3 = await pluginFactoryLedgerConnector.create({ + instanceId: connectorInstanceId3, + pluginRegistry: pluginRegistry3, + rpcApiHttpHost: rpcApiHttpHostMember3, + privateUrl: keys.quorum.member3.privateUrl, + logLevel, + }); + expect(connector3).toBeTruthy(); + pluginRegistry3.add(connector3); + + await connector1.onPluginInit(); + await connector2.onPluginInit(); + await connector3.onPluginInit(); + }); + + it("Can run private transactions", async () => { + const signingAddr = keys.quorum.member1.accountAddress; + + const txCount = await web3JsQuorumMember1.eth.getTransactionCount( + signingAddr, + ); + + const deployRes = await connector1.deployContract({ + bytecode: HelloWorldContractJson.bytecode, + contractAbi: HelloWorldContractJson.abi, + contractName: HelloWorldContractJson.contractName, + constructorArgs: [], + privateTransactionConfig: { + privateFrom: keys.tessera.member1.publicKey, + privateFor: [ + keys.tessera.member1.publicKey, + keys.tessera.member2.publicKey, + ], + isPrivate: true, + gasLimit: 10000000, + gasPrice: 0, + }, + web3SigningCredential: { + secret: keys.quorum.member1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + ethAccount: signingAddr, + }, + keychainId: keychainId1, + gas: 3000000, + gasPrice: 0, + nonce: txCount, + }); + + const contractDeployReceipt = await web3JsQuorumMember1.eth.getTransactionReceipt( + deployRes.transactionReceipt.transactionHash, + ); + + expect(contractDeployReceipt).toBeTruthy(); + const receipt = contractDeployReceipt; + + const { contractAddress } = receipt; + expect(contractAddress).toBeTruthy(); + + const member1Contract = new web3JsQuorumMember1.eth.Contract( + HelloWorldContractJson.abi as AbiItem[], + contractAddress, + ); + const mem1Response = await member1Contract.methods.getName().call(); + expect(mem1Response).toStrictEqual("CaptainCactus"); + + const member2Contract = new web3JsQuorumMember2.eth.Contract( + HelloWorldContractJson.abi as AbiItem[], + contractAddress, + ); + const mem2Response = await member2Contract.methods.getName().call(); + expect(mem2Response).toStrictEqual("CaptainCactus"); + + const member3Contract = new web3JsQuorumMember3.eth.Contract( + HelloWorldContractJson.abi as AbiItem[], + contractAddress, + ); + + await expect(member3Contract.methods.getName().call()).rejects.toThrow(); + + const newName = "Captain Cacti " + uuidV4; + await member1Contract.methods.setName(newName).send({ + from: signingAddr, + privateFrom: keys.tessera.member1.publicKey, + privateFor: [ + keys.tessera.member1.publicKey, + keys.tessera.member2.publicKey, + ], + isPrivate: true, + gasLimit: 10000000, + gasPrice: 0, + }); + + // Verify that member 1 can read the updated name + const member1NewNameResponse = await member1Contract.methods + .getName() + .call(); + expect(member1NewNameResponse).toBe(newName); + + // Verify that member 2 can read the updated name + const member2NewNameResponse = await member2Contract.methods + .getName() + .call(); + expect(member2NewNameResponse).toBe(newName); + + // Verify that member 3 cannot access the updated name + await expect(member3Contract.methods.getName().call()).rejects.toThrow(); + }); +}); diff --git a/packages/cactus-test-api-client/src/test/typescript/integration/api-client-routing-node-to-node.test.ts b/packages/cactus-test-api-client/src/test/typescript/integration/api-client-routing-node-to-node.test.ts index 82beb7b6ae..e4f96492cb 100644 --- a/packages/cactus-test-api-client/src/test/typescript/integration/api-client-routing-node-to-node.test.ts +++ b/packages/cactus-test-api-client/src/test/typescript/integration/api-client-routing-node-to-node.test.ts @@ -224,6 +224,7 @@ describe(testCase, () => { const pluginRegistry = new PluginRegistry({ plugins: [] }); const pluginQuorumConnector = new PluginLedgerConnectorQuorum({ + privateUrl: rpcApiHttpHost2, instanceId: uuidV4(), rpcApiHttpHost: rpcApiHttpHost2, logLevel, diff --git a/packages/cactus-test-tooling/src/main/typescript/quorum/quorum-mp-test-ledger.ts b/packages/cactus-test-tooling/src/main/typescript/quorum/quorum-mp-test-ledger.ts index 6571ebb5dc..ec0bb04dc2 100644 --- a/packages/cactus-test-tooling/src/main/typescript/quorum/quorum-mp-test-ledger.ts +++ b/packages/cactus-test-tooling/src/main/typescript/quorum/quorum-mp-test-ledger.ts @@ -158,6 +158,13 @@ export class QuorumMultiPartyTestLedger implements ITestLedger { }); } + public async pullFile(filePath: string): Promise { + const docker = new Docker(); + this.container = docker.getContainer(this.containerId as string); + + return await Containers.pullFile(this.container, filePath); + } + public stop(): Promise { if (this.useRunningLedger) { this.log.info("Ignore stop request because useRunningLedger is enabled."); diff --git a/typings/web3js-quorum/index.d.ts b/typings/web3js-quorum/index.d.ts index cdff6f164b..6117bc5067 100644 --- a/typings/web3js-quorum/index.d.ts +++ b/typings/web3js-quorum/index.d.ts @@ -311,7 +311,7 @@ declare module "web3js-quorum" { readonly privateFrom: string; readonly privateFor: string[]; readonly privacyGroupId?: string; - readonly nonce?: string; + readonly nonce?: number; readonly to?: string; readonly data: string; }