Skip to content

Commit 4d097c8

Browse files
konstantinablnatanasowsimzzz
authored
feat: Adds transaction pool funcitonality (#4454)
Signed-off-by: nikolay <[email protected]> Signed-off-by: Simeon Nakov <[email protected]> Signed-off-by: Konstantina Blazhukova <[email protected]> Co-authored-by: Nikolay Atanasow <[email protected]> Co-authored-by: Simeon Nakov <[email protected]>
1 parent b7c40e7 commit 4d097c8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2231
-633
lines changed

.github/workflows/image-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ jobs:
5151
-e OPERATOR_ID_MAIN='0.0.1002' \
5252
-e OPERATOR_KEY_MAIN='302e020100300506032b65700422042077d69b53642d0000000000000000000000000000000000000000000000000000' \
5353
-e READ_ONLY='true' \
54+
-e REDIS_ENABLED='false' \
5455
-d -p 7546:7546 --name relay relay:latest
5556
5657
- name: Test server

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Unless you need to set a non-default value, it is recommended to only populate o
3737
| `CONTRACT_QUERY_TIMEOUT_RETRIES` | "3" | Maximum retries for failed contract call query with timeout exceeded error |
3838
| `DEBUG_API_ENABLED` | "false" | Enables all debug related methods: `debug_traceTransaction` |
3939
| `DEFAULT_RATE_LIMIT` | "200" | default fallback rate limit, if no other is configured. |
40+
| `ENABLE_TX_POOL` | "false" | Flag to determine if the system should use a tx pool or not. |
4041
| `ESTIMATE_GAS_THROWS` | "true" | Flag to determine if the system should throw an error with the actual reason during contract reverts instead of returning a predefined gas value. |
4142
| `ETH_BLOCK_NUMBER_CACHE_TTL_MS` | "1000" | Time in ms to cache response from mirror node |
4243
| `ETH_CALL_ACCEPTED_ERRORS` | "[]" | A list of acceptable error codes for eth_call requests. If an error code in this list is returned, the request will be retried. |
@@ -89,6 +90,7 @@ Unless you need to set a non-default value, it is recommended to only populate o
8990
| `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. |
9091
| `PAYMASTER_ENABLED` | "false" | Flag to enable or disable the Paymaster functionally |
9192
| `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
9294
| `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. |
9395
| `REDIS_ENABLED` | "true" | Enable usage of Redis as shared cache |
9496
| `REDIS_RECONNECT_DELAY_MS` | "1000" | Sets the delay between reconnect retries from the Redis client in ms |
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
> [!NOTE]
2+
> This is an experimental feature hidden behind a flag `ENABLE_TX_POOL`
3+
4+
## Nonce management and transaction pool
5+
6+
This document explains how a per-address pending pool adds a pending count to Mirror Node nonces for rapid multi-transaction sends.
7+
8+
It covers the background and motivation, configuration, storage backends, request flows, failure modes, and how this impacts `eth_getTransactionCount` and `eth_sendRawTransaction`.
9+
10+
---
11+
12+
### Background and motivation
13+
14+
Hedera does not maintain an Ethereum-style mempool. Mirror Node (MN) imports account state (including `ethereum_nonce`) with a slight delay. If clients fire multiple transactions rapidly and compute nonces from MN only, nonces won't be correct and this can lead to errors.
15+
16+
To reduce these failures, the relay can maintain a per-address set of “pending” transactions it has seen and accepted, and expose that state to:
17+
18+
- Adjust the nonce precheck in `eth_sendRawTransaction`.
19+
- Serve `eth_getTransactionCount(address, "pending")` as MN nonce + pending count for that address.
20+
21+
The feature is disabled by default and gated by configuration.
22+
23+
---
24+
25+
### High-level behavior
26+
27+
- When enabled, the relay records a transaction hash in a per-address pending set just before submitting it to a consensus node, and removes it after the transaction is observed as processed (success or failure) via Mirror Node polling.
28+
- `eth_getTransactionCount(address, "latest")` returns the MN nonce only.
29+
- `eth_getTransactionCount(address, "pending")` returns MN nonce + current pending count for that address (only when the feature flag is enabled).
30+
- `eth_sendRawTransaction` precheck treats the acceptable signer nonce as MN nonce (+ pending count if enabled). If the transaction nonce is lower, the relay would throw an error.
31+
32+
Limitations (by design): Hedera services do not buffer transactions by nonce; users sending out-of-order nonces must resubmit later nonces after gaps are filled.
33+
34+
---
35+
36+
### Configuration
37+
38+
- `ENABLE_TX_POOL` (boolean; default: false)
39+
- Enables nonce management via the per-address pending pool.
40+
- Affects both precheck behavior in `eth_sendRawTransaction` and `eth_getTransactionCount(..., "pending")` responses.
41+
42+
- `REDIS_ENABLED` (boolean) and `REDIS_URL` (string)
43+
- If enabled and a valid URL is provided, the relay will attempt to connect to Redis and use it for the pending pool backend.
44+
- If disabled or unavailable, an in-memory local backend is used.
45+
46+
- `USE_ASYNC_TX_PROCESSING` (boolean)
47+
- If true, the relay returns the computed transaction hash immediately after prechecks. Pool bookkeeping still happens; MN polling and cleanup run in the background.
48+
49+
Caching notes for `eth_getTransactionCount`:
50+
- The implementation skips cache whenever block param is a non-cachable value (e.g., `latest`/`pending`). Historical queries may be cached; `pending` relies on live MN data and in-process pending counts.
51+
52+
---
53+
54+
### Storage backends
55+
56+
The pool is implemented behind a small interface so operators can choose a backend.
57+
58+
- Local in-memory storage (default fallback)
59+
- Per-process `Map<string, Set<string>>` keyed by lowercase address.
60+
- Operations: add/remove a tx hash; get set size for count; clear all.
61+
- Duplicates are naturally prevented by `Set` semantics.
62+
- Resets on process restart; state is not shared across multiple relay instances.
63+
64+
- Redis storage
65+
- Uses Redis `SET` per address with key prefix (e.g., `pending:<address>`).
66+
- Operations: `SADD` (add), `SCARD` (count), `SREM` (remove), plus a SCAN-based `removeAll` for startup/maintenance.
67+
- Single Redis commands are atomic; this is sufficient for per-address count consistency in this design.
68+
- If Redis is not reachable, the relay falls back to local storage automatically (see Failure modes).
69+
70+
Key design choices in the current implementation:
71+
- A per-address set is the source of truth for pending count.
72+
- The backend stores only hashes for counting purposes; raw RLP bodies are not currently stored.
73+
74+
---
75+
76+
### Request flows
77+
78+
#### eth_getTransactionCount
79+
80+
- latest: return MN `ethereum_nonce`.
81+
- pending: if `ENABLE_TX_POOL` is true, return `MN_nonce + pending_count(address)`; otherwise, return `MN_nonce`.
82+
83+
This lets users compute the next usable nonce even while MN has not yet reflected recent submissions.
84+
85+
#### eth_sendRawTransaction
86+
87+
1) Prechecks include:
88+
- Size/type/gas checks as before.
89+
- Account verification via MN.
90+
- Nonce precheck: define `signerNonce = MN_nonce` and, when `ENABLE_TX_POOL` is true, treat the acceptable minimum as `MN_nonce + pending_count(address)`. If `tx.nonce < signerNonce`, it fails.
91+
92+
2) Pool bookkeeping and submission:
93+
- Before submission, add the tx hash to the sender’s pending set.
94+
- Submit to consensus and poll Mirror Node to obtain the resulting Ethereum hash.
95+
- On success, remove the pending entry using the observed transaction hash.
96+
- On SDK timeout/connection drop, poll MN; if a record is found, remove and return its hash; if not, remove using the computed hash and return the computed hash.
97+
- On any terminal error, remove the pending entry and surface the error.
98+
99+
These rules ensure the pool reflects only transactions that the relay has accepted for submission and is robust to partial failures.
100+
101+
---
102+
103+
### Acceptance criteria mapping (abridged)
104+
105+
- After a tx is processed (success or failure), `pending` equals `latest` for that signer because the pending entry is removed.
106+
- While one or more transactions are pending for a signer, `pending` is greater than `latest` when `ENABLE_TX_POOL = true`.
107+
- With the feature disabled, behavior matches today’s MN-only semantics.
108+
109+
110+
---
111+
112+
### FAQ
113+
114+
- Does this guarantee out-of-order nonce execution without resubmission?
115+
- No. Hedera does not maintain an execution buffer by nonce; users must resubmit later nonces if gaps existed when they were first sent.
116+
117+
- Is `eth_getTransactionCount` cached?
118+
- The method skips cache for `latest`/`pending` style requests to keep results fresh; historical queries may be cached.
119+
120+
- Why use a set instead of a list?
121+
- Sets prevent duplicates by construction and make counting O(1). The use case only needs a per-address count and membership for removal by hash.
122+
123+
124+

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

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ type ExtractTypeStringFromKey<K extends string> = K extends keyof typeof _CONFIG
2323
type StringTypeToActualType<Tstr extends string> = Tstr extends 'string'
2424
? string
2525
: Tstr extends 'boolean'
26-
? boolean
27-
: Tstr extends 'number'
28-
? number
29-
: Tstr extends 'strArray'
30-
? string[]
31-
: Tstr extends 'numArray'
32-
? number[]
33-
: never;
26+
? boolean
27+
: Tstr extends 'number'
28+
? number
29+
: Tstr extends 'strArray'
30+
? string[]
31+
: Tstr extends 'numArray'
32+
? number[]
33+
: never;
3434

3535
/**
3636
* Determines if a configuration value can be `undefined` based on two conditions:
@@ -46,8 +46,8 @@ type CanBeUndefined<K extends string> = K extends keyof typeof _CONFIG
4646
? (typeof _CONFIG)[K]['required'] extends true
4747
? false
4848
: (typeof _CONFIG)[K]['defaultValue'] extends null
49-
? true
50-
: false
49+
? true
50+
: false
5151
: never;
5252

5353
/**
@@ -59,9 +59,10 @@ type CanBeUndefined<K extends string> = K extends keyof typeof _CONFIG
5959
* - `'WEB_SOCKET_PORT'` (`type: 'number'`, `required: false`, `defaultValue: 8546`) → `number`
6060
* - `'GITHUB_PR_NUMBER'` (`type: 'string'`, `required: false`, `defaultValue: null`) → `string | undefined`
6161
*/
62-
export type GetTypeOfConfigKey<K extends string> = CanBeUndefined<K> extends true
63-
? StringTypeToActualType<ExtractTypeStringFromKey<K>> | undefined
64-
: StringTypeToActualType<ExtractTypeStringFromKey<K>>;
62+
export type GetTypeOfConfigKey<K extends string> =
63+
CanBeUndefined<K> extends true
64+
? StringTypeToActualType<ExtractTypeStringFromKey<K>> | undefined
65+
: StringTypeToActualType<ExtractTypeStringFromKey<K>>;
6566

6667
/**
6768
* Interface defining the structure of a configuration property.
@@ -167,6 +168,11 @@ const _CONFIG = {
167168
required: false,
168169
defaultValue: 7546,
169170
},
171+
ENABLE_TX_POOL: {
172+
type: 'boolean',
173+
required: false,
174+
defaultValue: false,
175+
},
170176
ESTIMATE_GAS_THROWS: {
171177
type: 'boolean',
172178
required: false,
@@ -513,6 +519,11 @@ const _CONFIG = {
513519
required: false,
514520
defaultValue: [],
515521
},
522+
PENDING_TRANSACTION_STORAGE_TTL: {
523+
type: 'number',
524+
required: false,
525+
defaultValue: 30,
526+
},
516527
RATE_LIMIT_DISABLED: {
517528
type: 'boolean',
518529
required: false,

packages/relay/src/lib/clients/cache/IRedisCacheClient.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
// SPDX-License-Identifier: Apache-2.0
22

3-
import type { ICacheClient } from './ICacheClient';
43
import { RequestDetails } from '../../types';
4+
import type { ICacheClient } from './ICacheClient';
55

66
export interface IRedisCacheClient extends ICacheClient {
7-
disconnect: () => Promise<void>;
87
incrBy(key: string, amount: number, callingMethod: string, requestDetails: RequestDetails): Promise<number>;
98
rPush(key: string, value: any, callingMethod: string, requestDetails: RequestDetails): Promise<number>;
109
lRange(

0 commit comments

Comments
 (0)