|
| 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 | + |
0 commit comments