Skip to content

feat: implement Jumbo Transaction support and enhance pre-requisite validation for eth_sendRawTransaction #3722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
40e500f
chore: updated param types for TRANSACTION_SIZE_TOO_BIG JSONRpcError
quiet-node Apr 23, 2025
6fd0987
chore: renamed parseTxIfNeeded -> parseRawTransaction
quiet-node Apr 23, 2025
17a33fc
chore: reworked transactionSize precheck for simpler logic
quiet-node Apr 23, 2025
f629e62
chore: moved parsing logic out of validateRawTransaction()
quiet-node Apr 23, 2025
cb48238
chore: added contractCodeSize precheck
quiet-node Apr 23, 2025
d03a844
feat: allowed Relay to use Jumbo Transaction behind a feature flag
quiet-node Apr 23, 2025
67bfa2a
chore: updated SEND_RAW_TRANSACTION_SIZE_LIMIT to bump the limit to 1…
quiet-node Apr 24, 2025
6ff4dd8
feat: added callDataSize() precheck
quiet-node Apr 24, 2025
9835aae
chore: removed contractCodeSize acceptance test to mitigate flakyness
quiet-node Apr 24, 2025
3d9bab9
test: added new sendRawTransactionExtension acceptance test
quiet-node Apr 24, 2025
b2988f9
test: removed duplicated test
quiet-node Apr 24, 2025
e494a27
chore: rearranged data size prechecks
quiet-node Apr 24, 2025
c1b0779
fix: fixed test in batch1
quiet-node Apr 24, 2025
78d1b57
test: added e2e test for Jumbo
quiet-node May 7, 2025
2dc2ff5
chore: improved error handling for sdkClient
quiet-node May 8, 2025
148d608
backup eth.ts
quiet-node May 9, 2025
8d55464
chore: migrated to TransactionService
quiet-node May 9, 2025
0443062
chore: updated execute transaction sdk error handling
quiet-node May 9, 2025
d269f7d
fix: removed unused code
quiet-node May 12, 2025
2e8af99
Revert "chore: updated execute transaction sdk error handling"
quiet-node May 12, 2025
4c0e41d
Revert "chore: improved error handling for sdkClient"
quiet-node May 12, 2025
cb3985b
chore: turned JUMBO_TX_ENABLED's default value to true
quiet-node May 13, 2025
16de375
chore: turned test command for sendRawTransactionExtension to snake_case
quiet-node May 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
- { name: 'API Batch 3', testfilter: 'api_batch3' }
- { name: 'ERC20', testfilter: 'erc20' }
- { name: 'Rate Limiter', testfilter: 'ratelimiter', test_ws_server: true }
- { name: 'SendRawTransaction Extension', testfilter: 'sendRawTransactionExtension' }
- { name: 'HBar Limiter Batch 1', testfilter: 'hbarlimiter_batch1' }
- { name: 'HBar Limiter Batch 2', testfilter: 'hbarlimiter_batch2' }
- { name: 'HBar Limiter Batch 3', testfilter: 'hbarlimiter_batch3' }
Expand Down
149 changes: 76 additions & 73 deletions docs/configuration.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"acceptancetest:cache-service": "nyc ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@cache-service' --exit",
"acceptancetest:rpc_api_schema_conformity": "nyc ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@api-conformity' --exit",
"acceptancetest:serverconfig": "nyc ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@server-config' --exit",
"acceptancetest:sendRawTransactionExtension": "nyc ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@sendRawTransactionExtension' --exit",
"build": "npx lerna run build",
"build-and-test": "npx lerna run build && npx lerna run test",
"build:docker": "docker build . -t ${npm_package_name}",
Expand Down
20 changes: 19 additions & 1 deletion packages/config-service/src/services/globalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ const _CONFIG = {
required: false,
defaultValue: 3600000,
},
CALL_DATA_SIZE_LIMIT: {
envName: 'CALL_DATA_SIZE_LIMIT',
type: 'number',
required: false,
defaultValue: 131072, // 128KB
},
CHAIN_ID: {
envName: 'CHAIN_ID',
type: 'string',
Expand All @@ -141,6 +147,12 @@ const _CONFIG = {
required: false,
defaultValue: 50_000_000,
},
CONTRACT_CODE_SIZE_LIMIT: {
envName: 'CONTRACT_CODE_SIZE_LIMIT',
type: 'number',
required: false,
defaultValue: 24576, // 24KB
},
CONTRACT_QUERY_TIMEOUT_RETRIES: {
envName: 'CONTRACT_QUERY_TIMEOUT_RETRIES',
type: 'number',
Expand Down Expand Up @@ -405,6 +417,12 @@ const _CONFIG = {
required: false,
defaultValue: 1,
},
JUMBO_TX_ENABLED: {
envName: 'JUMBO_TX_ENABLED',
type: 'boolean',
required: false,
defaultValue: false,
},
LIMIT_DURATION: {
envName: 'LIMIT_DURATION',
type: 'number',
Expand Down Expand Up @@ -644,7 +662,7 @@ const _CONFIG = {
envName: 'SEND_RAW_TRANSACTION_SIZE_LIMIT',
type: 'number',
required: false,
defaultValue: 131072,
defaultValue: 133120, // 130 KB
},
SERVER_HOST: {
envName: 'SERVER_HOST',
Expand Down
11 changes: 7 additions & 4 deletions packages/relay/src/lib/clients/sdkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,17 @@ export class SDKClient {
networkGasPriceInWeiBars: number,
currentNetworkExchangeRateInCents: number,
): Promise<{ txResponse: TransactionResponse; fileId: FileId | null }> {
const jumboTxEnabled = ConfigService.get('JUMBO_TX_ENABLED');
const ethereumTransactionData: EthereumTransactionData = EthereumTransactionData.fromBytes(transactionBuffer);
const ethereumTransaction = new EthereumTransaction();
const interactingEntity = ethereumTransactionData.toJSON()['to'].toString();

let fileId: FileId | null = null;

// if callData's size is greater than `fileAppendChunkSize` => employ HFS to create new file to carry the rest of the contents of callData
if (ethereumTransactionData.callData.length <= this.fileAppendChunkSize) {
if (jumboTxEnabled || ethereumTransactionData.callData.length <= this.fileAppendChunkSize) {
ethereumTransaction.setEthereumData(ethereumTransactionData.toBytes());
} else {
// if JUMBO_TX_ENABLED is false and callData's size is greater than `fileAppendChunkSize` => employ HFS to create new file to carry the rest of the contents of callData
fileId = await this.createFile(
ethereumTransactionData.callData,
this.clientMain,
Expand All @@ -163,10 +165,11 @@ export class SDKClient {
ethereumTransactionData.callData = new Uint8Array();
ethereumTransaction.setEthereumData(ethereumTransactionData.toBytes()).setCallDataFileId(fileId);
}
const networkGasPriceInTinyBars = weibarHexToTinyBarInt(networkGasPriceInWeiBars);

ethereumTransaction.setMaxTransactionFee(
Hbar.fromTinybars(Math.floor(networkGasPriceInTinyBars * constants.MAX_TRANSACTION_FEE_THRESHOLD)),
Hbar.fromTinybars(
Math.floor(weibarHexToTinyBarInt(networkGasPriceInWeiBars) * constants.MAX_TRANSACTION_FEE_THRESHOLD),
),
);

return {
Expand Down
4 changes: 4 additions & 0 deletions packages/relay/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ export default {
},

MAX_TRANSACTION_FEE_THRESHOLD: ConfigService.get('MAX_TRANSACTION_FEE_THRESHOLD'),
SEND_RAW_TRANSACTION_SIZE_LIMIT: ConfigService.get('SEND_RAW_TRANSACTION_SIZE_LIMIT'),
CONTRACT_CODE_SIZE_LIMIT: ConfigService.get('CONTRACT_CODE_SIZE_LIMIT'),
CALL_DATA_SIZE_LIMIT: ConfigService.get('CALL_DATA_SIZE_LIMIT'),

INVALID_EVM_INSTRUCTION: '0xfe',
EMPTY_BLOOM: '0x' + '0'.repeat(512),
ZERO_HEX: '0x0',
Expand Down
12 changes: 11 additions & 1 deletion packages/relay/src/lib/errors/JsonRpcError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,21 @@ export const predefined = {
code: -32001,
message: 'Filter not found',
}),
TRANSACTION_SIZE_TOO_BIG: (actualSize: string, expectedSize: string) =>
TRANSACTION_SIZE_LIMIT_EXCEEDED: (actualSize: number, expectedSize: number) =>
new JsonRpcError({
code: -32201,
message: `Oversized data: transaction size ${actualSize}, transaction limit ${expectedSize}`,
}),
CALL_DATA_SIZE_LIMIT_EXCEEDED: (actualSize: number, expectedSize: number) =>
new JsonRpcError({
code: -32201,
message: `Oversized data: call data size ${actualSize}, call data size limit ${expectedSize}`,
}),
CONTRACT_CODE_SIZE_LIMIT_EXCEEDED: (actualSize: number, expectedSize: number) =>
new JsonRpcError({
code: -32201,
message: `Oversized data: contract code size ${actualSize}, contract code size limit ${expectedSize}`,
}),
BATCH_REQUESTS_DISABLED: new JsonRpcError({
code: -32202,
message: 'Batch requests are disabled',
Expand Down
68 changes: 43 additions & 25 deletions packages/relay/src/lib/precheck.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// SPDX-License-Identifier: Apache-2.0

import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
import { ethers, Transaction } from 'ethers';
import { Logger } from 'pino';

Expand Down Expand Up @@ -35,7 +34,7 @@ export class Precheck {
* @param {string | Transaction} transaction - The transaction to parse.
* @returns {Transaction} The parsed transaction.
*/
public static parseTxIfNeeded(transaction: string | Transaction): Transaction {
public static parseRawTransaction(transaction: string | Transaction): Transaction {
return typeof transaction === 'string' ? Transaction.from(transaction) : transaction;
}

Expand All @@ -60,6 +59,9 @@ export class Precheck {
networkGasPriceInWeiBars: number,
requestDetails: RequestDetails,
): Promise<void> {
this.contractCodeSize(parsedTx);
this.callDataSize(parsedTx);
this.transactionSize(parsedTx);
this.transactionType(parsedTx, requestDetails);
this.gasLimit(parsedTx, requestDetails);
const mirrorAccountInfo = await this.verifyAccount(parsedTx, requestDetails);
Expand Down Expand Up @@ -317,35 +319,34 @@ export class Precheck {
}

/**
* Converts hex string to bytes array
* @param {string} hex - The hex string you want to convert.
* @returns {Uint8Array} The bytes array.
* Validates that the transaction size is within the allowed limit.
* The serialized transaction length is converted from hex string length to byte count
* by subtracting the '0x' prefix (2 characters) and dividing by 2 (since each byte is represented by 2 hex characters).
*
* @param {Transaction} tx - The transaction to validate.
* @throws {JsonRpcError} If the transaction size exceeds the configured limit.
*/
hexToBytes(hex: string): Uint8Array {
if (hex === '') {
throw predefined.INTERNAL_ERROR('Passed hex an empty string');
transactionSize(tx: Transaction): void {
const totalRawTransactionSizeInBytes = tx.serialized.replace('0x', '').length / 2;
const transactionSizeLimit = constants.SEND_RAW_TRANSACTION_SIZE_LIMIT;
if (totalRawTransactionSizeInBytes > transactionSizeLimit) {
throw predefined.TRANSACTION_SIZE_LIMIT_EXCEEDED(totalRawTransactionSizeInBytes, transactionSizeLimit);
}

if (hex.startsWith('0x') && hex.length == 2) {
throw predefined.INTERNAL_ERROR('Hex cannot be 0x');
} else if (hex.startsWith('0x') && hex.length != 2) {
hex = hex.slice(2);
}

return Uint8Array.from(Buffer.from(hex, 'hex'));
}

/**
* Checks the size of the transaction.
* @param {string} transaction - The transaction to check.
* Validates that the call data size is within the allowed limit.
* The data field length is converted from hex string length to byte count
* by subtracting the '0x' prefix (2 characters) and dividing by 2 (since each byte is represented by 2 hex characters).
*
* @param {Transaction} tx - The transaction to validate.
* @throws {JsonRpcError} If the call data size exceeds the configured limit.
*/
checkSize(transaction: string): void {
const transactionToBytes: Uint8Array = this.hexToBytes(transaction);
const transactionSize: number = transactionToBytes.length;
const transactionSizeLimit: number = ConfigService.get('SEND_RAW_TRANSACTION_SIZE_LIMIT');

if (transactionSize > transactionSizeLimit) {
throw predefined.TRANSACTION_SIZE_TOO_BIG(String(transactionSize), String(transactionSizeLimit));
callDataSize(tx: Transaction): void {
const totalCallDataSizeInBytes = tx.data.replace('0x', '').length / 2;
const callDataSizeLimit = constants.CALL_DATA_SIZE_LIMIT;
if (totalCallDataSizeInBytes > callDataSizeLimit) {
throw predefined.CALL_DATA_SIZE_LIMIT_EXCEEDED(totalCallDataSizeInBytes, callDataSizeLimit);
}
}

Expand Down Expand Up @@ -378,4 +379,21 @@ export class Precheck {
}
}
}

/**
* Validates that the contract code size is within the allowed limit.
* This check is only performed for contract creation transactions (where tx.to is null).
* This limits contract code size to prevent excessive gas consumption.
*
* @param {Transaction} tx - The transaction to validate.
* @throws {JsonRpcError} If the contract code size exceeds the configured limit.
*/
contractCodeSize(tx: Transaction): void {
if (!tx.to) {
const contractCodeSize = tx.data.replace('0x', '').length / 2;
if (contractCodeSize > constants.CONTRACT_CODE_SIZE_LIMIT) {
throw predefined.CONTRACT_CODE_SIZE_LIMIT_EXCEEDED(contractCodeSize, constants.CONTRACT_CODE_SIZE_LIMIT);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,12 @@ export class TransactionService implements ITransactionService {
*/
async sendRawTransaction(transaction: string, requestDetails: RequestDetails): Promise<string | JsonRpcError> {
const transactionBuffer = Buffer.from(this.prune0x(transaction), 'hex');

const parsedTx = Precheck.parseRawTransaction(transaction);
const networkGasPriceInWeiBars = Utils.addPercentageBufferToGasPrice(
await this.common.getGasPriceInWeibars(requestDetails),
);
const parsedTx = await this.parseRawTxAndPrecheck(transaction, networkGasPriceInWeiBars, requestDetails);

await this.validateRawTransaction(parsedTx, networkGasPriceInWeiBars, requestDetails);

/**
* Note: If the USE_ASYNC_TX_PROCESSING feature flag is enabled,
Expand Down Expand Up @@ -536,18 +537,17 @@ export class TransactionService implements ITransactionService {
}

/**
* Parses a raw transaction and performs prechecks
* @param transaction The raw transaction string
* Validates a parsed transaction by performing prechecks
* @param parsedTx The parsed Ethereum transaction to validate
* @param networkGasPriceInWeiBars The current network gas price in wei bars
* @param requestDetails The request details for logging and tracking
* @returns {Promise<EthersTransaction>} A promise that resolves to the parsed Ethereum transaction
* @throws {JsonRpcError} If validation fails
*/
private async parseRawTxAndPrecheck(
transaction: string,
private async validateRawTransaction(
parsedTx: EthersTransaction,
networkGasPriceInWeiBars: number,
requestDetails: RequestDetails,
): Promise<EthersTransaction> {
const parsedTx = Precheck.parseTxIfNeeded(transaction);
): Promise<void> {
try {
if (this.logger.isLevelEnabled('debug')) {
this.logger.debug(
Expand All @@ -557,9 +557,7 @@ export class TransactionService implements ITransactionService {
);
}

this.precheck.checkSize(transaction);
await this.precheck.sendRawTransactionCheck(parsedTx, networkGasPriceInWeiBars, requestDetails);
return parsedTx;
} catch (e: any) {
this.logger.error(
`${requestDetails.formattedRequestId} Precheck failed: transaction=${JSON.stringify(parsedTx)}`,
Expand Down
Loading
Loading