From 50787b550ad4c39eb9f74ece911429a08f00c921 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Sun, 21 Mar 2021 22:20:13 -0700 Subject: [PATCH] feat(connector-besu): request param: wait for ledger tx receipt #685 Adds new parameters to the run transaction endpoint's request object that allow the caller to specify a the consistency strategy parameters that can define if the connector should wait for either: 1. the transaction to be confirmed only by the node's transaction pool 2. if at least the block containing the transaction should be mined by the ledger 3. an N number of additional blocks should be confirmed by the ledger in **addition** to the block that contained the transaction. The parameters also allow to specify a timeout in milliseconds that if unspecified defaults to the maximum safe integer that Javascript can represent which for practical purposes we consider to be the same as waiting until the heat death of the universe or infinity. Fixes #685 Signed-off-by: Peter Somogyvari --- .../src/main/json/openapi.json | 38 +++++-- .../generated/openapi/typescript-axios/api.ts | 41 ++++++- .../plugin-ledger-connector-besu.ts | 102 +++++++++++++++--- .../deploy-contract-from-json.test.ts | 12 +++ 4 files changed, 167 insertions(+), 26 deletions(-) diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json index aeb63b5e2d9..d4d767a17db 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json @@ -31,6 +31,33 @@ ], "components": { "schemas": { + "ReceiptType": { + "description": "Enumerates the possible types of receipts that can be waited for by someone or something that has requested the execution of a transaction on a ledger.", + "type": "string", + "enum": [ + "NODE_TX_POOL_ACK", + "LEDGER_BLOCK_ACK" + ] + }, + "ConsistencyStrategy": { + "type": "object", + "required": ["receiptType", "blockConfirmations"], + "properties": { + "receiptType": { + "$ref": "#/components/schemas/ReceiptType" + }, + "timeoutMs": { + "type": "integer", + "description": "The amount of milliseconds to wait for the receipt to arrive to the connector. Defaults to 0 which means to wait for an unlimited amount of time. Note that this wait may be interrupted still by other parts of the infrastructure such as load balancers cutting of HTTP requests after some time even if they are the type that is supposed to be kept alive. The question of re-entrancy is a broader topic not in scope to discuss here, but it is important to mention it.", + "minimum": 0 + }, + "blockConfirmations": { + "type": "integer", + "minimum": 0, + "description": "The number of blocks to wait to be confirmed in addition to the block containing the transaction in question. Note that if the receipt type is set to only wait for node transaction pool ACK and this parameter is set to anything, but zero then the API will not accept the request due to conflicting parameters." + } + } + }, "Web3SigningCredential": { "type": "object", "required": [ @@ -343,7 +370,8 @@ "type": "object", "required": [ "web3SigningCredential", - "transactionConfig" + "transactionConfig", + "consistencyStrategy" ], "properties": { "web3SigningCredential": { @@ -354,12 +382,8 @@ "$ref": "#/components/schemas/BesuTransactionConfig", "nullable": false }, - "timeoutMs": { - "type": "number", - "description": "The amount of milliseconds to wait for a transaction receipt with thehash of the transaction(which indicates successful execution) beforegiving up and crashing.", - "minimum": 0, - "default": 60000, - "nullable": false + "consistencyStrategy": { + "$ref": "#/components/schemas/ConsistencyStrategy" } } }, diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts index c6b9fbb46bd..41b0ae10645 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -76,6 +76,31 @@ export interface BesuTransactionConfig { */ data?: string; } +/** + * + * @export + * @interface ConsistencyStrategy + */ +export interface ConsistencyStrategy { + /** + * + * @type {ReceiptType} + * @memberof ConsistencyStrategy + */ + receiptType: ReceiptType; + /** + * The amount of milliseconds to wait for the receipt to arrive to the connector. Defaults to 0 which means to wait for an unlimited amount of time. Note that this wait may be interrupted still by other parts of the infrastructure such as load balancers cutting of HTTP requests after some time even if they are the type that is supposed to be kept alive. The question of re-entrancy is a broader topic not in scope to discuss here, but it is important to mention it. + * @type {number} + * @memberof ConsistencyStrategy + */ + timeoutMs?: number; + /** + * The number of blocks to wait to be confirmed in addition to the block containing the transaction in question. Note that if the receipt type is set to only wait for node transaction pool ACK and this parameter is set to anything, but zero then the API will not accept the request due to conflicting parameters. + * @type {number} + * @memberof ConsistencyStrategy + */ + blockConfirmations: number; +} /** * * @export @@ -320,6 +345,16 @@ export interface InvokeContractV2Response { */ success: boolean; } +/** + * Enumerates the possible types of receipts that can be waited for by someone or something that has requested the execution of a transaction on a ledger. + * @export + * @enum {string} + */ +export enum ReceiptType { + NODETXPOOLACK = 'NODE_TX_POOL_ACK', + LEDGERBLOCKACK = 'LEDGER_BLOCK_ACK' +} + /** * * @export @@ -339,11 +374,11 @@ export interface RunTransactionRequest { */ transactionConfig: BesuTransactionConfig; /** - * The amount of milliseconds to wait for a transaction receipt with thehash of the transaction(which indicates successful execution) beforegiving up and crashing. - * @type {number} + * + * @type {ConsistencyStrategy} * @memberof RunTransactionRequest */ - timeoutMs?: number; + consistencyStrategy: ConsistencyStrategy; } /** * diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts index 6a95cb1ccf9..63095ba9e8e 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts @@ -38,6 +38,7 @@ import { import { DeployContractSolidityBytecodeEndpoint } from "./web-services/deploy-contract-solidity-bytecode-endpoint"; import { + ConsistencyStrategy, DeployContractSolidityBytecodeV1Request, DeployContractSolidityBytecodeV1Response, EthContractInvocationType, @@ -45,6 +46,7 @@ import { InvokeContractV1Response, InvokeContractV2Request, InvokeContractV2Response, + ReceiptType, RunTransactionRequest, RunTransactionResponse, SignTransactionRequest, @@ -238,7 +240,11 @@ export class PluginLedgerConnectorBesu const txReq: RunTransactionRequest = { transactionConfig, web3SigningCredential, - timeoutMs: req.timeoutMs || 60000, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: req.timeoutMs || 60000, + }, }; const out = await this.transact(txReq); @@ -305,7 +311,11 @@ export class PluginLedgerConnectorBesu const txReq: RunTransactionRequest = { transactionConfig, web3SigningCredential, - timeoutMs: req.timeoutMs || 60000, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: req.timeoutMs || 60000, + }, }; const out = await this.transact(txReq); const transactionReceipt = out.transactionReceipt; @@ -335,7 +345,7 @@ export class PluginLedgerConnectorBesu } case Web3SigningCredentialType.NONE: { if (req.transactionConfig.rawTransaction) { - return this.transactSigned(req.transactionConfig.rawTransaction); + return this.transactSigned(req); } else { throw new Error( `${fnTag} Expected pre-signed raw transaction ` + @@ -355,17 +365,56 @@ export class PluginLedgerConnectorBesu } public async transactSigned( - rawTransaction: string, + req: RunTransactionRequest, ): Promise { const fnTag = `${this.className}#transactSigned()`; - const receipt = await this.web3.eth.sendSignedTransaction(rawTransaction); + Checks.truthy(req.consistencyStrategy, `${fnTag}:req.consistencyStrategy`); + Checks.truthy( + req.transactionConfig.rawTransaction, + `${fnTag}:req.transactionConfig.rawTransaction`, + ); + const rawTx = req.transactionConfig.rawTransaction as string; + this.log.debug("Starting web3.eth.sendSignedTransaction(rawTransaction) "); + const txPoolReceipt = await this.web3.eth.sendSignedTransaction(rawTx); + this.log.debug("Received preliminary receipt from Besu node."); + + if (txPoolReceipt instanceof Error) { + this.log.debug(`${fnTag} sendSignedTransaction failed`, txPoolReceipt); + throw txPoolReceipt; + } - if (receipt instanceof Error) { - this.log.debug(`${fnTag} Web3 sendSignedTransaction failed`, receipt); - throw receipt; - } else { - return { transactionReceipt: receipt }; + if ( + req.consistencyStrategy.receiptType === ReceiptType.NODETXPOOLACK && + req.consistencyStrategy.blockConfirmations > 0 + ) { + throw new Error( + `${fnTag} Conflicting parameters for consistency` + + ` strategy: Cannot wait for >0 block confirmations AND only wait ` + + ` for the tx pool ACK at the same time.`, + ); + } + + switch (req.consistencyStrategy.receiptType) { + case ReceiptType.NODETXPOOLACK: + return { transactionReceipt: txPoolReceipt }; + case ReceiptType.LEDGERBLOCKACK: + this.log.debug("Starting poll for ledger TX receipt ..."); + const txHash = txPoolReceipt.transactionHash; + const { consistencyStrategy } = req; + const ledgerReceipt = await this.pollForTxReceipt( + txHash, + consistencyStrategy, + ); + this.log.debug( + "Finished poll for ledger TX receipt: %o", + ledgerReceipt, + ); + return { transactionReceipt: ledgerReceipt }; + default: + throw new Error( + `${fnTag} Unrecognized ReceiptType: ${req.consistencyStrategy.receiptType}`, + ); } } @@ -384,7 +433,7 @@ export class PluginLedgerConnectorBesu ); if (signedTx.rawTransaction) { - return this.transactSigned(signedTx.rawTransaction); + return this.transactSigned(req); } else { throw new Error( `${fnTag} Failed to sign eth transaction. ` + @@ -423,30 +472,46 @@ export class PluginLedgerConnectorBesu type: Web3SigningCredentialType.PRIVATEKEYHEX, secret: privateKeyHex, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: 60000, + }, }); } public async pollForTxReceipt( txHash: string, - timeoutMs = 60000, + consistencyStrategy: ConsistencyStrategy, ): Promise { const fnTag = `${this.className}#pollForTxReceipt()`; let txReceipt; let timedOut = false; let tries = 0; + let confirmationCount = 0; + const timeoutMs = consistencyStrategy.timeoutMs || Number.MAX_SAFE_INTEGER; const startedAt = new Date(); do { - txReceipt = await this.web3.eth.getTransactionReceipt(txHash); tries++; timedOut = Date.now() >= startedAt.getTime() + timeoutMs; - } while (!timedOut && !txReceipt); + if (timedOut) { + break; + } + + txReceipt = await this.web3.eth.getTransactionReceipt(txHash); + if (!txReceipt) { + continue; + } + + const latestBlockNo = await this.web3.eth.getBlockNumber(); + confirmationCount = latestBlockNo - txReceipt.blockNumber; + } while (confirmationCount >= consistencyStrategy.blockConfirmations); if (!txReceipt) { throw new Error(`${fnTag} Timed out ${timeoutMs}ms, polls=${tries}`); - } else { - return txReceipt; } + return txReceipt; } public async deployContract( @@ -467,6 +532,11 @@ export class PluginLedgerConnectorBesu gas: req.gas, gasPrice: req.gasPrice, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: req.timeoutMs || 60000, + }, web3SigningCredential, }); diff --git a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/deploy-contract-from-json.test.ts b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/deploy-contract-from-json.test.ts index f8c0919fdb8..9bd883c9519 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/deploy-contract-from-json.test.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/deploy-contract-from-json.test.ts @@ -7,6 +7,7 @@ import { PluginLedgerConnectorBesu, PluginFactoryLedgerConnector, Web3SigningCredentialCactusKeychainRef, + ReceiptType, } from "../../../../../main/typescript/public-api"; import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; import { BesuTestLedger } from "@hyperledger/cactus-test-tooling"; @@ -58,6 +59,7 @@ test("deploys contract via .json file", async (t: Test) => { }); const connector: PluginLedgerConnectorBesu = await factory.create({ rpcApiHttpHost, + logLevel, instanceId: uuidv4(), pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), }); @@ -74,6 +76,11 @@ test("deploys contract via .json file", async (t: Test) => { value: 10e9, gas: 1000000, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: 60000, + }, }); const balance = await web3.eth.getBalance(testEthAccount.address); @@ -147,6 +154,11 @@ test("deploys contract via .json file", async (t: Test) => { transactionConfig: { rawTransaction, }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NODETXPOOLACK, + timeoutMs: 60000, + }, }); const balance2 = await web3.eth.getBalance(testEthAccount2.address);