Skip to content

Commit 4ea1929

Browse files
authored
feat: adds RLP hex txn storage to tx pool service (#4521)
Signed-off-by: Simeon Nakov <[email protected]>
1 parent 65c1a72 commit 4ea1929

File tree

13 files changed

+589
-170
lines changed

13 files changed

+589
-170
lines changed

.env.http.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ OPERATOR_KEY_MAIN= # Operator private key used to sign transaction
5858
# TIER_3_RATE_LIMIT=1600 # Max request limit for static return endpoints
5959
# LIMIT_DURATION=60000 # Duration in ms for rate limits
6060

61+
# ========== TRANSACTION POOL ========
62+
# PENDING_TRANSACTION_STORAGE_TTL=30 # Time-to-live (TTL) in seconds for transaction payloads stored in Redis
63+
6164
# ========== HBAR RATE LIMITING ==========
6265
# HBAR_RATE_LIMIT_TINYBAR=25000000000 # Total HBAR budget (250 HBARs)
6366
# HBAR_RATE_LIMIT_DURATION=86400000 # HBAR budget limit duration (1 day)

.env.ws.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ SUBSCRIPTIONS_ENABLED=true # Must be true for the WebSocket server to func
5656
# REDIS_RECONNECT_DELAY_MS=1000 # Delay between reconnect retries
5757
# MULTI_SET=false # Implementation for setting multiple K/V pairs
5858

59+
# ========== TRANSACTION POOL ========
60+
# PENDING_TRANSACTION_STORAGE_TTL=30 # Time-to-live (TTL) in seconds for transaction payloads stored in Redis
61+
5962
# ========== OTHER SETTINGS ==========
6063
# CLIENT_TRANSPORT_SECURITY=false # Enable or disable TLS for both networks
6164
# USE_ASYNC_TX_PROCESSING=true # If true, returns tx hash immediately after prechecks

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ Unless you need to set a non-default value, it is recommended to only populate o
9090
| `OPCODELOGGER_ENABLED` | "true" | Whether the `opcodeLogger` tracer is enabled for a call to `debug_traceTransaction`. This setting should match the value `hiero.mirror.web3.opcode.tracer.enabled` in the Mirror Node used. See [HIP-801](https://hips.hedera.com/hip/hip-801) for more information. |
9191
| `PAYMASTER_ENABLED` | "false" | Flag to enable or disable the Paymaster functionally |
9292
| `PAYMASTER_WHITELIST` | "[]" | List of "to" addresses that will have fully subsidized transactions if the gasPrice was set to 0 by the signer |
93-
| `PENDING_TRANSACTION_STORAGE_TTL` | 30 | Sets the ttl for the pending transactions in the transaction pool
9493
| `READ_ONLY` | "false" | Starts the JSON-RPC Relay in Read-Only mode. In Read-Only mode, write operations, _e.g._, `eth_sendRawTransaction` or query operations to the Consensus Node will trigger an `Unsupported operation` error. |
9594
| `REDIS_ENABLED` | "true" | Enable usage of Redis as shared cache |
9695
| `REDIS_RECONNECT_DELAY_MS` | "1000" | Sets the delay between reconnect retries from the Redis client in ms |
@@ -102,6 +101,7 @@ Unless you need to set a non-default value, it is recommended to only populate o
102101
| `TIER_1_RATE_LIMIT` | "100" | Maximum restrictive request count limit used for expensive endpoints rate limiting. |
103102
| `TIER_2_RATE_LIMIT` | "800" | Maximum moderate request count limit used for non expensive endpoints. |
104103
| `TIER_3_RATE_LIMIT` | "1600" | Maximum relaxed request count limit used for static return endpoints. |
104+
| `PENDING_TRANSACTION_STORAGE_TTL` | "30" | Time-to-live (TTL) in seconds for transaction payloads stored in Redis. After this period, transaction data expires and is automatically cleaned up by Redis and the Lua-based orphan cleanup mechanism. |
105105
| `TX_DEFAULT_GAS` | "400000" | Default gas for transactions that do not specify gas. |
106106
| `USE_ASYNC_TX_PROCESSING` | "true" | Set to `true` to enable `eth_sendRawTransaction` to return the transaction hash immediately after passing all prechecks, while processing the transaction asynchronously in the background. |
107107
| `USE_MIRROR_NODE_MODULARIZED_SERVICES` | null | Controls routing of Mirror Node traffic through modularized services. When set to `true`, enables routing a percentage of traffic to modularized services. When set to `false`, ensures traffic follows the traditional non-modularized flow. When not set (i.e. `null` by default), no specific routing preference is applied. As Mirror Node gradually transitions to a fully modularized architecture across all networks, this setting will eventually default to `true`. |

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -519,11 +519,6 @@ const _CONFIG = {
519519
required: false,
520520
defaultValue: [],
521521
},
522-
PENDING_TRANSACTION_STORAGE_TTL: {
523-
type: 'number',
524-
required: false,
525-
defaultValue: 30,
526-
},
527522
RATE_LIMIT_DISABLED: {
528523
type: 'boolean',
529524
required: false,
@@ -614,6 +609,11 @@ const _CONFIG = {
614609
required: false,
615610
defaultValue: false,
616611
},
612+
PENDING_TRANSACTION_STORAGE_TTL: {
613+
type: 'number',
614+
required: false,
615+
defaultValue: 30,
616+
},
617617
TIER_1_RATE_LIMIT: {
618618
type: 'number',
619619
required: false,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ export class TransactionService implements ITransactionService {
499499

500500
// Remove the transaction from the transaction pool after submission
501501
if (ConfigService.get('ENABLE_TX_POOL')) {
502-
await this.transactionPoolService.removeTransaction(originalCallerAddress, parsedTx.hash!);
502+
await this.transactionPoolService.removeTransaction(originalCallerAddress, parsedTx.serialized);
503503
}
504504

505505
sendRawTransactionError = error;

packages/relay/src/lib/services/transactionPoolService/LocalPendingTransactionStorage.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ import { PendingTransactionStorage } from '../../types/transactionPool';
1010
* atomicity across multiple process instances.
1111
*/
1212
export class LocalPendingTransactionStorage implements PendingTransactionStorage {
13-
// Maps address to a Set of transaction hashes for that address
13+
// Maps address to a Set of RLP hex payloads for that address
1414
private readonly pendingTransactions: Map<string, Set<string>>;
1515

16+
// Global set of all pending RLP hex payloads
17+
private readonly globalTransactionIndex: Set<string>;
18+
1619
constructor() {
1720
this.pendingTransactions = new Map();
21+
this.globalTransactionIndex = new Set();
1822
}
1923

2024
/**
@@ -30,37 +34,44 @@ export class LocalPendingTransactionStorage implements PendingTransactionStorage
3034

3135
/**
3236
* Adds a pending transaction entry for the given address.
37+
* Atomically indexes the transaction (per-address + global) and persists its payload.
3338
*
3439
* @param addr - The account address
35-
* @param txHash - The transaction hash to add to the pending list
40+
* @param rlpHex - The RLP-encoded transaction as a hex string
3641
*/
37-
async addToList(addr: string, txHash: string): Promise<void> {
42+
async addToList(addr: string, rlpHex: string): Promise<void> {
3843
// Initialize the set if it doesn't exist
3944
if (!this.pendingTransactions.has(addr)) {
4045
this.pendingTransactions.set(addr, new Set());
4146
}
4247

4348
const addressTransactions = this.pendingTransactions.get(addr)!;
44-
addressTransactions.add(txHash);
49+
addressTransactions.add(rlpHex);
50+
51+
// Add to global index
52+
this.globalTransactionIndex.add(rlpHex);
4553
}
4654

4755
/**
4856
* Removes a transaction from the pending list of the given address.
4957
*
5058
* @param address - The account address whose transaction should be removed
51-
* @param txHash - The transaction hash to remove
59+
* @param rlpHex - The RLP-encoded transaction as a hex string
5260
*/
53-
async removeFromList(address: string, txHash: string): Promise<void> {
61+
async removeFromList(address: string, rlpHex: string): Promise<void> {
5462
const addressTransactions = this.pendingTransactions.get(address);
5563

5664
if (addressTransactions) {
57-
addressTransactions.delete(txHash);
65+
addressTransactions.delete(rlpHex);
5866

5967
// Clean up empty sets to prevent memory leaks
6068
if (addressTransactions.size === 0) {
6169
this.pendingTransactions.delete(address);
6270
}
6371
}
72+
73+
// Remove from global index
74+
this.globalTransactionIndex.delete(rlpHex);
6475
}
6576

6677
/**
@@ -70,5 +81,26 @@ export class LocalPendingTransactionStorage implements PendingTransactionStorage
7081
*/
7182
async removeAll(): Promise<void> {
7283
this.pendingTransactions.clear();
84+
this.globalTransactionIndex.clear();
85+
}
86+
87+
/**
88+
* Retrieves all pending transaction payloads (RLP hex) across all addresses.
89+
*
90+
* @returns Set of all pending transaction RLP hex strings
91+
*/
92+
async getAllTransactionPayloads(): Promise<Set<string>> {
93+
return this.globalTransactionIndex;
94+
}
95+
96+
/**
97+
* Retrieves pending transaction payloads (RLP hex) for a specific address.
98+
*
99+
* @param address - The account address to query
100+
* @returns Set of transaction RLP hex strings for the address
101+
*/
102+
async getTransactionPayloads(address: string): Promise<Set<string>> {
103+
const addressTransactions = this.pendingTransactions.get(address);
104+
return addressTransactions ?? new Set();
73105
}
74106
}

packages/relay/src/lib/services/transactionPoolService/RedisPendingTransactionStorage.ts

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,29 @@ export class RedisPendingTransactionStorage implements PendingTransactionStorage
1515
*/
1616
private readonly keyPrefix = 'txpool:pending:';
1717

18+
/**
19+
* Key for storing all pending transactions across all addresses.
20+
*
21+
* @remarks
22+
* This global set contains all pending transaction RLPs from all addresses,
23+
* allowing efficient retrieval of the entire pending pool without scanning
24+
* individual address keys.
25+
*/
26+
private readonly globalPendingTxsKey = `${this.keyPrefix}global`;
27+
1828
/**
1929
* The time-to-live (TTL) for the pending transaction storage in seconds.
2030
*/
21-
private readonly storageTtl = ConfigService.get('PENDING_TRANSACTION_STORAGE_TTL');
31+
private readonly storageTtl: number;
2232

2333
/**
2434
* Creates a new Redis-backed pending transaction storage.
2535
*
2636
* @param redisClient - A connected {@link RedisClientType} instance.
2737
*/
28-
constructor(private readonly redisClient: RedisClientType) {}
38+
constructor(private readonly redisClient: RedisClientType) {
39+
this.storageTtl = ConfigService.get('PENDING_TRANSACTION_STORAGE_TTL');
40+
}
2941

3042
/**
3143
* Resolves the Redis key for a given address.
@@ -38,46 +50,43 @@ export class RedisPendingTransactionStorage implements PendingTransactionStorage
3850
}
3951

4052
/**
41-
* Appends a transaction hash to the pending list for the provided address.
42-
*
43-
* @remarks
44-
* This uses Redis `SADD`, which is atomic. The integer result from Redis is
45-
* the new length of the list after the append.
53+
* Adds a pending transaction for the given address.
54+
* Atomically indexes the transaction (per-address + global) using MULTI/EXEC.
4655
*
47-
* @param addr - Account address whose pending list will be appended to.
48-
* @param txHash - Transaction hash to append.
49-
* @returns The new pending transaction count after the addition.
56+
* @param address - Account address whose pending list will be appended to.
57+
* @param rlpHex - The RLP-encoded transaction as a hex string.
5058
*/
51-
async addToList(address: string, txHash: string): Promise<void> {
52-
const key = this.keyFor(address);
59+
async addToList(address: string, rlpHex: string): Promise<void> {
60+
const addressKey = this.keyFor(address);
5361

54-
// doing this to be able to atomically add the transaction hash
55-
// and set the expiration time
56-
await this.redisClient.multi().sAdd(key, txHash).expire(key, this.storageTtl).execAsPipeline();
62+
await this.redisClient
63+
.multi()
64+
.sAdd(addressKey, rlpHex)
65+
.expire(addressKey, this.storageTtl)
66+
.sAdd(this.globalPendingTxsKey, rlpHex)
67+
.expire(this.globalPendingTxsKey, this.storageTtl)
68+
.exec();
5769
}
5870

5971
/**
60-
* Removes a transaction hash from the pending list of the given address.
72+
* Removes a transaction from the pending list of the given address.
6173
*
6274
* @param address - Account address whose pending list should be modified.
63-
* @param txHash - Transaction hash to remove from the list.
75+
* @param rlpHex - The RLP-encoded transaction as a hex string.
6476
*/
65-
async removeFromList(address: string, txHash: string): Promise<void> {
77+
async removeFromList(address: string, rlpHex: string): Promise<void> {
6678
const key = this.keyFor(address);
67-
68-
await this.redisClient.sRem(key, txHash);
79+
await this.redisClient.multi().sRem(key, rlpHex).sRem(this.globalPendingTxsKey, rlpHex).exec();
6980
}
7081

7182
/**
7283
* Removes all keys managed by this storage (all `txpool:pending:*`).
7384
*/
7485
async removeAll(): Promise<void> {
75-
const keys = await this.redisClient.keys(`${this.keyPrefix}*`);
86+
const pendingKeys = await this.redisClient.keys(`${this.keyPrefix}*`);
7687

77-
if (keys.length > 0) {
78-
for (const key of keys) {
79-
this.redisClient.unlink(key);
80-
}
88+
if (pendingKeys.length > 0) {
89+
await this.redisClient.unlink(pendingKeys);
8190
}
8291
}
8392

@@ -92,4 +101,26 @@ export class RedisPendingTransactionStorage implements PendingTransactionStorage
92101

93102
return await this.redisClient.sCard(key);
94103
}
104+
105+
/**
106+
* Retrieves all pending transaction payloads (RLP hex) across all addresses.
107+
*
108+
* @returns Set of all pending transaction RLP hex strings
109+
*/
110+
async getAllTransactionPayloads(): Promise<Set<string>> {
111+
const members = await this.redisClient.sMembers(this.globalPendingTxsKey);
112+
return new Set(members);
113+
}
114+
115+
/**
116+
* Retrieves pending transaction payloads (RLP hex) for a specific address.
117+
*
118+
* @param address - The account address to query
119+
* @returns Set of transaction RLP hex strings for the address
120+
*/
121+
async getTransactionPayloads(address: string): Promise<Set<string>> {
122+
const addressKey = this.keyFor(address);
123+
const members = await this.redisClient.sMembers(addressKey);
124+
return new Set(members);
125+
}
95126
}

0 commit comments

Comments
 (0)