Skip to content

Commit d7d6380

Browse files
authored
feat: implement Jumbo Transaction support and enhance pre-requisite validation for eth_sendRawTransaction (#3722)
Signed-off-by: Logan Nguyen <[email protected]>
1 parent 64a04c2 commit d7d6380

File tree

19 files changed

+774
-168
lines changed

19 files changed

+774
-168
lines changed

.github/workflows/acceptance.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
- { name: 'API Batch 3', testfilter: 'api_batch3' }
2525
- { name: 'ERC20', testfilter: 'erc20' }
2626
- { name: 'Rate Limiter', testfilter: 'ratelimiter', test_ws_server: true }
27+
- { name: 'SendRawTransaction Extension', testfilter: 'send_raw_transaction_extension' }
2728
- { name: 'HBar Limiter Batch 1', testfilter: 'hbarlimiter_batch1' }
2829
- { name: 'HBar Limiter Batch 2', testfilter: 'hbarlimiter_batch2' }
2930
- { name: 'HBar Limiter Batch 3', testfilter: 'hbarlimiter_batch3' }

docs/configuration.md

Lines changed: 76 additions & 73 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"acceptancetest:cache-service": "nyc ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@cache-service' --exit",
7070
"acceptancetest:rpc_api_schema_conformity": "nyc ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@api-conformity' --exit",
7171
"acceptancetest:serverconfig": "nyc ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@server-config' --exit",
72+
"acceptancetest:send_raw_transaction_extension": "nyc ts-mocha packages/server/tests/acceptance/index.spec.ts -g '@sendRawTransactionExtension' --exit",
7273
"build": "npx lerna run build",
7374
"build-and-test": "npx lerna run build && npx lerna run test",
7475
"build:docker": "docker build . -t ${npm_package_name}",

packages/config-service/src/services/globalConfig.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ const _CONFIG = {
117117
required: false,
118118
defaultValue: 3600000,
119119
},
120+
CALL_DATA_SIZE_LIMIT: {
121+
envName: 'CALL_DATA_SIZE_LIMIT',
122+
type: 'number',
123+
required: false,
124+
defaultValue: 131072, // 128KB
125+
},
120126
CHAIN_ID: {
121127
envName: 'CHAIN_ID',
122128
type: 'string',
@@ -141,6 +147,12 @@ const _CONFIG = {
141147
required: false,
142148
defaultValue: 50_000_000,
143149
},
150+
CONTRACT_CODE_SIZE_LIMIT: {
151+
envName: 'CONTRACT_CODE_SIZE_LIMIT',
152+
type: 'number',
153+
required: false,
154+
defaultValue: 24576, // 24KB
155+
},
144156
CONTRACT_QUERY_TIMEOUT_RETRIES: {
145157
envName: 'CONTRACT_QUERY_TIMEOUT_RETRIES',
146158
type: 'number',
@@ -405,6 +417,12 @@ const _CONFIG = {
405417
required: false,
406418
defaultValue: 1,
407419
},
420+
JUMBO_TX_ENABLED: {
421+
envName: 'JUMBO_TX_ENABLED',
422+
type: 'boolean',
423+
required: false,
424+
defaultValue: true,
425+
},
408426
LIMIT_DURATION: {
409427
envName: 'LIMIT_DURATION',
410428
type: 'number',
@@ -644,7 +662,7 @@ const _CONFIG = {
644662
envName: 'SEND_RAW_TRANSACTION_SIZE_LIMIT',
645663
type: 'number',
646664
required: false,
647-
defaultValue: 131072,
665+
defaultValue: 133120, // 130 KB
648666
},
649667
SERVER_HOST: {
650668
envName: 'SERVER_HOST',

packages/relay/src/lib/clients/sdkClient.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,17 @@ export class SDKClient {
139139
networkGasPriceInWeiBars: number,
140140
currentNetworkExchangeRateInCents: number,
141141
): Promise<{ txResponse: TransactionResponse; fileId: FileId | null }> {
142+
const jumboTxEnabled = ConfigService.get('JUMBO_TX_ENABLED');
142143
const ethereumTransactionData: EthereumTransactionData = EthereumTransactionData.fromBytes(transactionBuffer);
143144
const ethereumTransaction = new EthereumTransaction();
144145
const interactingEntity = ethereumTransactionData.toJSON()['to'].toString();
146+
145147
let fileId: FileId | null = null;
146148

147-
// if callData's size is greater than `fileAppendChunkSize` => employ HFS to create new file to carry the rest of the contents of callData
148-
if (ethereumTransactionData.callData.length <= this.fileAppendChunkSize) {
149+
if (jumboTxEnabled || ethereumTransactionData.callData.length <= this.fileAppendChunkSize) {
149150
ethereumTransaction.setEthereumData(ethereumTransactionData.toBytes());
150151
} else {
152+
// 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
151153
fileId = await this.createFile(
152154
ethereumTransactionData.callData,
153155
this.clientMain,
@@ -163,10 +165,11 @@ export class SDKClient {
163165
ethereumTransactionData.callData = new Uint8Array();
164166
ethereumTransaction.setEthereumData(ethereumTransactionData.toBytes()).setCallDataFileId(fileId);
165167
}
166-
const networkGasPriceInTinyBars = weibarHexToTinyBarInt(networkGasPriceInWeiBars);
167168

168169
ethereumTransaction.setMaxTransactionFee(
169-
Hbar.fromTinybars(Math.floor(networkGasPriceInTinyBars * constants.MAX_TRANSACTION_FEE_THRESHOLD)),
170+
Hbar.fromTinybars(
171+
Math.floor(weibarHexToTinyBarInt(networkGasPriceInWeiBars) * constants.MAX_TRANSACTION_FEE_THRESHOLD),
172+
),
170173
);
171174

172175
return {

packages/relay/src/lib/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ export default {
230230
},
231231

232232
MAX_TRANSACTION_FEE_THRESHOLD: ConfigService.get('MAX_TRANSACTION_FEE_THRESHOLD'),
233+
SEND_RAW_TRANSACTION_SIZE_LIMIT: ConfigService.get('SEND_RAW_TRANSACTION_SIZE_LIMIT'),
234+
CONTRACT_CODE_SIZE_LIMIT: ConfigService.get('CONTRACT_CODE_SIZE_LIMIT'),
235+
CALL_DATA_SIZE_LIMIT: ConfigService.get('CALL_DATA_SIZE_LIMIT'),
236+
233237
INVALID_EVM_INSTRUCTION: '0xfe',
234238
EMPTY_BLOOM: '0x' + '0'.repeat(512),
235239
ZERO_HEX: '0x0',

packages/relay/src/lib/errors/JsonRpcError.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,21 @@ export const predefined = {
279279
code: -32001,
280280
message: 'Filter not found',
281281
}),
282-
TRANSACTION_SIZE_TOO_BIG: (actualSize: string, expectedSize: string) =>
282+
TRANSACTION_SIZE_LIMIT_EXCEEDED: (actualSize: number, expectedSize: number) =>
283283
new JsonRpcError({
284284
code: -32201,
285285
message: `Oversized data: transaction size ${actualSize}, transaction limit ${expectedSize}`,
286286
}),
287+
CALL_DATA_SIZE_LIMIT_EXCEEDED: (actualSize: number, expectedSize: number) =>
288+
new JsonRpcError({
289+
code: -32201,
290+
message: `Oversized data: call data size ${actualSize}, call data size limit ${expectedSize}`,
291+
}),
292+
CONTRACT_CODE_SIZE_LIMIT_EXCEEDED: (actualSize: number, expectedSize: number) =>
293+
new JsonRpcError({
294+
code: -32201,
295+
message: `Oversized data: contract code size ${actualSize}, contract code size limit ${expectedSize}`,
296+
}),
287297
BATCH_REQUESTS_DISABLED: new JsonRpcError({
288298
code: -32202,
289299
message: 'Batch requests are disabled',

packages/relay/src/lib/precheck.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// SPDX-License-Identifier: Apache-2.0
22

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

@@ -35,7 +34,7 @@ export class Precheck {
3534
* @param {string | Transaction} transaction - The transaction to parse.
3635
* @returns {Transaction} The parsed transaction.
3736
*/
38-
public static parseTxIfNeeded(transaction: string | Transaction): Transaction {
37+
public static parseRawTransaction(transaction: string | Transaction): Transaction {
3938
return typeof transaction === 'string' ? Transaction.from(transaction) : transaction;
4039
}
4140

@@ -60,6 +59,9 @@ export class Precheck {
6059
networkGasPriceInWeiBars: number,
6160
requestDetails: RequestDetails,
6261
): Promise<void> {
62+
this.contractCodeSize(parsedTx);
63+
this.callDataSize(parsedTx);
64+
this.transactionSize(parsedTx);
6365
this.transactionType(parsedTx, requestDetails);
6466
this.gasLimit(parsedTx, requestDetails);
6567
const mirrorAccountInfo = await this.verifyAccount(parsedTx, requestDetails);
@@ -317,35 +319,34 @@ export class Precheck {
317319
}
318320

319321
/**
320-
* Converts hex string to bytes array
321-
* @param {string} hex - The hex string you want to convert.
322-
* @returns {Uint8Array} The bytes array.
322+
* Validates that the transaction size is within the allowed limit.
323+
* The serialized transaction length is converted from hex string length to byte count
324+
* by subtracting the '0x' prefix (2 characters) and dividing by 2 (since each byte is represented by 2 hex characters).
325+
*
326+
* @param {Transaction} tx - The transaction to validate.
327+
* @throws {JsonRpcError} If the transaction size exceeds the configured limit.
323328
*/
324-
hexToBytes(hex: string): Uint8Array {
325-
if (hex === '') {
326-
throw predefined.INTERNAL_ERROR('Passed hex an empty string');
329+
transactionSize(tx: Transaction): void {
330+
const totalRawTransactionSizeInBytes = tx.serialized.replace('0x', '').length / 2;
331+
const transactionSizeLimit = constants.SEND_RAW_TRANSACTION_SIZE_LIMIT;
332+
if (totalRawTransactionSizeInBytes > transactionSizeLimit) {
333+
throw predefined.TRANSACTION_SIZE_LIMIT_EXCEEDED(totalRawTransactionSizeInBytes, transactionSizeLimit);
327334
}
328-
329-
if (hex.startsWith('0x') && hex.length == 2) {
330-
throw predefined.INTERNAL_ERROR('Hex cannot be 0x');
331-
} else if (hex.startsWith('0x') && hex.length != 2) {
332-
hex = hex.slice(2);
333-
}
334-
335-
return Uint8Array.from(Buffer.from(hex, 'hex'));
336335
}
337336

338337
/**
339-
* Checks the size of the transaction.
340-
* @param {string} transaction - The transaction to check.
338+
* Validates that the call data size is within the allowed limit.
339+
* The data field length is converted from hex string length to byte count
340+
* by subtracting the '0x' prefix (2 characters) and dividing by 2 (since each byte is represented by 2 hex characters).
341+
*
342+
* @param {Transaction} tx - The transaction to validate.
343+
* @throws {JsonRpcError} If the call data size exceeds the configured limit.
341344
*/
342-
checkSize(transaction: string): void {
343-
const transactionToBytes: Uint8Array = this.hexToBytes(transaction);
344-
const transactionSize: number = transactionToBytes.length;
345-
const transactionSizeLimit: number = ConfigService.get('SEND_RAW_TRANSACTION_SIZE_LIMIT');
346-
347-
if (transactionSize > transactionSizeLimit) {
348-
throw predefined.TRANSACTION_SIZE_TOO_BIG(String(transactionSize), String(transactionSizeLimit));
345+
callDataSize(tx: Transaction): void {
346+
const totalCallDataSizeInBytes = tx.data.replace('0x', '').length / 2;
347+
const callDataSizeLimit = constants.CALL_DATA_SIZE_LIMIT;
348+
if (totalCallDataSizeInBytes > callDataSizeLimit) {
349+
throw predefined.CALL_DATA_SIZE_LIMIT_EXCEEDED(totalCallDataSizeInBytes, callDataSizeLimit);
349350
}
350351
}
351352

@@ -378,4 +379,21 @@ export class Precheck {
378379
}
379380
}
380381
}
382+
383+
/**
384+
* Validates that the contract code size is within the allowed limit.
385+
* This check is only performed for contract creation transactions (where tx.to is null).
386+
* This limits contract code size to prevent excessive gas consumption.
387+
*
388+
* @param {Transaction} tx - The transaction to validate.
389+
* @throws {JsonRpcError} If the contract code size exceeds the configured limit.
390+
*/
391+
contractCodeSize(tx: Transaction): void {
392+
if (!tx.to) {
393+
const contractCodeSize = tx.data.replace('0x', '').length / 2;
394+
if (contractCodeSize > constants.CONTRACT_CODE_SIZE_LIMIT) {
395+
throw predefined.CONTRACT_CODE_SIZE_LIMIT_EXCEEDED(contractCodeSize, constants.CONTRACT_CODE_SIZE_LIMIT);
396+
}
397+
}
398+
}
381399
}

packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,12 @@ export class TransactionService implements ITransactionService {
298298
*/
299299
async sendRawTransaction(transaction: string, requestDetails: RequestDetails): Promise<string | JsonRpcError> {
300300
const transactionBuffer = Buffer.from(this.prune0x(transaction), 'hex');
301-
301+
const parsedTx = Precheck.parseRawTransaction(transaction);
302302
const networkGasPriceInWeiBars = Utils.addPercentageBufferToGasPrice(
303303
await this.common.getGasPriceInWeibars(requestDetails),
304304
);
305-
const parsedTx = await this.parseRawTxAndPrecheck(transaction, networkGasPriceInWeiBars, requestDetails);
305+
306+
await this.validateRawTransaction(parsedTx, networkGasPriceInWeiBars, requestDetails);
306307

307308
/**
308309
* Note: If the USE_ASYNC_TX_PROCESSING feature flag is enabled,
@@ -536,18 +537,17 @@ export class TransactionService implements ITransactionService {
536537
}
537538

538539
/**
539-
* Parses a raw transaction and performs prechecks
540-
* @param transaction The raw transaction string
540+
* Validates a parsed transaction by performing prechecks
541+
* @param parsedTx The parsed Ethereum transaction to validate
541542
* @param networkGasPriceInWeiBars The current network gas price in wei bars
542543
* @param requestDetails The request details for logging and tracking
543-
* @returns {Promise<EthersTransaction>} A promise that resolves to the parsed Ethereum transaction
544+
* @throws {JsonRpcError} If validation fails
544545
*/
545-
private async parseRawTxAndPrecheck(
546-
transaction: string,
546+
private async validateRawTransaction(
547+
parsedTx: EthersTransaction,
547548
networkGasPriceInWeiBars: number,
548549
requestDetails: RequestDetails,
549-
): Promise<EthersTransaction> {
550-
const parsedTx = Precheck.parseTxIfNeeded(transaction);
550+
): Promise<void> {
551551
try {
552552
if (this.logger.isLevelEnabled('debug')) {
553553
this.logger.debug(
@@ -557,9 +557,7 @@ export class TransactionService implements ITransactionService {
557557
);
558558
}
559559

560-
this.precheck.checkSize(transaction);
561560
await this.precheck.sendRawTransactionCheck(parsedTx, networkGasPriceInWeiBars, requestDetails);
562-
return parsedTx;
563561
} catch (e: any) {
564562
this.logger.error(
565563
`${requestDetails.formattedRequestId} Precheck failed: transaction=${JSON.stringify(parsedTx)}`,

0 commit comments

Comments
 (0)