Skip to content

Commit

Permalink
feat(connector-besu): request param: wait for ledger tx receipt #685
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
petermetz committed Mar 26, 2021
1 parent 18f5af7 commit dc8c564
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -343,7 +370,8 @@
"type": "object",
"required": [
"web3SigningCredential",
"transactionConfig"
"transactionConfig",
"consistencyStrategy"
],
"properties": {
"web3SigningCredential": {
Expand All @@ -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"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
/**
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ import {
import { DeployContractSolidityBytecodeEndpoint } from "./web-services/deploy-contract-solidity-bytecode-endpoint";

import {
ConsistencyStrategy,
DeployContractSolidityBytecodeV1Request,
DeployContractSolidityBytecodeV1Response,
EthContractInvocationType,
InvokeContractV1Request,
InvokeContractV2Request,
InvokeContractV1Response,
InvokeContractV2Response,
ReceiptType,
RunTransactionRequest,
RunTransactionResponse,
SignTransactionRequest,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -309,7 +315,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;
Expand Down Expand Up @@ -340,7 +350,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 ` +
Expand All @@ -360,17 +370,56 @@ export class PluginLedgerConnectorBesu
}

public async transactSigned(
rawTransaction: string,
req: RunTransactionRequest,
): Promise<RunTransactionResponse> {
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}`,
);
}
}

Expand All @@ -389,7 +438,8 @@ export class PluginLedgerConnectorBesu
);

if (signedTx.rawTransaction) {
return this.transactSigned(signedTx.rawTransaction);
req.transactionConfig.rawTransaction = signedTx.rawTransaction;
return this.transactSigned(req);
} else {
throw new Error(
`${fnTag} Failed to sign eth transaction. ` +
Expand Down Expand Up @@ -428,30 +478,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<TransactionReceipt> {
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(
Expand All @@ -472,6 +538,11 @@ export class PluginLedgerConnectorBesu
gas: req.gas,
gasPrice: req.gasPrice,
},
consistencyStrategy: {
blockConfirmations: 0,
receiptType: ReceiptType.NODETXPOOLACK,
timeoutMs: req.timeoutMs || 60000,
},
web3SigningCredential,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PluginLedgerConnectorBesu,
PluginFactoryLedgerConnector,
Web3SigningCredentialCactusKeychainRef,
ReceiptType,
} from "../../../../../main/typescript/public-api";
import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory";
import {
Expand Down Expand Up @@ -69,6 +70,7 @@ test(testCase, async (t: Test) => {
});
const connector: PluginLedgerConnectorBesu = await factory.create({
rpcApiHttpHost,
logLevel,
instanceId: uuidv4(),
pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }),
});
Expand All @@ -85,6 +87,11 @@ test(testCase, async (t: Test) => {
value: 10e9,
gas: 1000000,
},
consistencyStrategy: {
blockConfirmations: 0,
receiptType: ReceiptType.NODETXPOOLACK,
timeoutMs: 60000,
},
});

const balance = await web3.eth.getBalance(testEthAccount.address);
Expand Down Expand Up @@ -158,6 +165,11 @@ test(testCase, async (t: Test) => {
transactionConfig: {
rawTransaction,
},
consistencyStrategy: {
blockConfirmations: 0,
receiptType: ReceiptType.NODETXPOOLACK,
timeoutMs: 60000,
},
});

const balance2 = await web3.eth.getBalance(testEthAccount2.address);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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";
Expand Down Expand Up @@ -75,6 +76,10 @@ test("deploys contract via .json file", async (t: Test) => {
secret: besuKeyPair.privateKey,
type: Web3SigningCredentialType.PRIVATEKEYHEX,
},
consistencyStrategy: {
blockConfirmations: 0,
receiptType: ReceiptType.NODETXPOOLACK,
},
transactionConfig: {
from: firstHighNetWorthAccount,
to: testEthAccount.address,
Expand Down Expand Up @@ -150,6 +155,10 @@ test("deploys contract via .json file", async (t: Test) => {
web3SigningCredential: {
type: Web3SigningCredentialType.NONE,
},
consistencyStrategy: {
blockConfirmations: 0,
receiptType: ReceiptType.NODETXPOOLACK,
},
transactionConfig: {
rawTransaction,
},
Expand Down

0 comments on commit dc8c564

Please sign in to comment.