feat(l1): add --mempool.private flag for non-propagating local txs#6576
feat(l1): add --mempool.private flag for non-propagating local txs#6576ilitteri wants to merge 9 commits into
Conversation
Mirrors reth's --txpool.no-local-transactions-propagation. When set, transactions submitted via this node's eth_sendRawTransaction enter the mempool and may be included in blocks built locally, but their hashes are NOT inserted into the broadcast pool — peers never see them via Transactions / NewPooledTransactionHashes. Transactions received from peers continue to propagate as before. Plumbing -------- - BlockchainOptions gains a `private_mempool: bool` (default false). - Mempool gets a sibling `add_transaction_no_broadcast` that skips the broadcast_pool insertion. Both methods share an internal `add_transaction_inner(broadcast: bool)`. - Blockchain exposes `add_local_transaction_to_pool` and `add_local_blob_transaction_to_pool` for the RPC entry point. These check `options.private_mempool` and route to the no-broadcast variant when set; the existing `add_transaction_to_pool` (used by P2P/sync paths) keeps current behavior and always broadcasts. - The eth_sendRawTransaction handler now calls the local variants. - CLI flag: `--mempool.private` (env: ETHREX_MEMPOOL_PRIVATE), matching the existing `--mempool.maxsize` prefix. Test ---- `add_transaction_no_broadcast_keeps_tx_out_of_broadcast_pool` shows that a tx routed via the no-broadcast path is queryable from the mempool but absent from `get_txs_for_broadcast`.
🤖 Kimi Code ReviewThis PR implements a private mempool feature (mirroring Reth's Summary of ChangesThe PR cleanly separates transaction ingestion paths:
Code Quality & Correctness
Security & Consensus
Minor Suggestions
VerdictApprove. The implementation is correct, secure, and follows Rust best practices. The separation of concerns between P2P and RPC ingestion paths is architecturally sound and prevents accidental leakage of private transactions to the broadcast pool. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
Lines of code reportTotal lines added: Detailed view |
🤖 Claude Code ReviewPR Review:
|
| Area | Verdict |
|---|---|
| Core correctness | Correct |
| Security | No issues |
| CLI flag | Minor: redundant default_value_t |
| Docs | Minor: "EIP-equivalent" phrasing |
| Test coverage | Moderate gap: Blockchain-level routing and blob-tx private path untested |
The implementation is solid. The main ask before merging is a test that exercises Blockchain::add_local_transaction_to_pool with private_mempool: true end-to-end — everything else is polish.
Automated review by Claude (Anthropic) · sonnet · custom prompt
Greptile SummaryAdds a
Confidence Score: 3/5Safe to merge for the happy path, but the interaction between a privately-submitted transaction and a subsequent P2P receipt of the same tx breaks the documented invariant and should be resolved first. The core machinery is correct, but both inner helpers share a deduplication path that can permanently suppress a tx from broadcast_pool when the private RPC path races ahead of a P2P receipt of the same transaction. crates/blockchain/blockchain.rs — both
|
| Filename | Overview |
|---|---|
| crates/blockchain/blockchain.rs | Adds add_local_transaction_to_pool, add_local_blob_transaction_to_pool, and shared _inner helpers; the contains_tx early-return in both inner helpers prevents the P2P path from promoting a privately-submitted tx into broadcast_pool. |
| crates/blockchain/mempool.rs | Adds add_transaction_no_broadcast and shared add_transaction_inner helper; correctly gates broadcast_pool insertion on the broadcast flag. Logic is clean. |
| crates/networking/rpc/eth/transaction.rs | Routes eth_sendRawTransaction through add_local_blob_transaction_to_pool / add_local_transaction_to_pool; correct — no other RPC entry points call pool methods. |
| cmd/ethrex/cli.rs | Adds --mempool.private / ETHREX_MEMPOOL_PRIVATE CLI flag with SetTrue action; default false, docs and help text are accurate. |
| cmd/ethrex/initializers.rs | Correctly wires opts.mempool_private → BlockchainOptions::private_mempool in the L1 initializer. |
| cmd/ethrex/l2/initializers.rs | Correctly wires opts.node_opts.mempool_private → BlockchainOptions::private_mempool for the L2 initializer. |
| test/tests/blockchain/mempool_tests.rs | New test directly exercises add_transaction_no_broadcast and confirms the tx stays in transaction_pool but is absent from get_txs_for_broadcast; thorough and self-contained. |
Sequence Diagram
sequenceDiagram
participant RPC as eth_sendRawTransaction
participant BC as Blockchain
participant MP as Mempool
participant P2P as P2P Network
Note over RPC,P2P: private_mempool = true
RPC->>BC: add_local_transaction_to_pool(tx)
BC->>BC: add_transaction_to_pool_inner(broadcast=false)
BC->>MP: add_transaction_no_broadcast(hash, sender, tx)
MP->>MP: transaction_pool.insert(hash, tx)
Note right of MP: broadcast_pool NOT updated
MP-->>BC: Ok(())
BC-->>RPC: Ok(hash)
P2P->>BC: add_transaction_to_pool(tx)
BC->>BC: add_transaction_to_pool_inner(broadcast=true)
BC->>MP: contains_tx(hash)?
alt tx already in pool (private submission race)
MP-->>BC: true - early return Ok(hash)
Note right of BC: broadcast_pool still missing hash
else tx not yet seen
BC->>MP: add_transaction(hash, sender, tx)
MP->>MP: transaction_pool.insert + broadcast_pool.insert
end
Comments Outside Diff (1)
-
crates/blockchain/blockchain.rs, line 2421-2423 (link)Silent de-duplication hides broadcast-pool omission for P2P path
Both
add_transaction_to_pool_innerandadd_blob_transaction_to_pool_innerreturn early oncontains_tx, which checks onlytransaction_pool— notbroadcast_pool. If a transaction is first submitted via private RPC (broadcast = false), it lands intransaction_poolbut never entersbroadcast_pool. When the same tx later arrives through P2P (broadcast = true),contains_txreturnstrueand the function returnsOk(hash)without inserting the hash intobroadcast_pool. The tx is silently never relayed to peers despite the documented guarantee that "P2P-received transactions are unaffected." The same window exists inadd_blob_transaction_to_pool_innerat line 2356.Prompt To Fix With AI
This is a comment left during a code review. Path: crates/blockchain/blockchain.rs Line: 2421-2423 Comment: **Silent de-duplication hides broadcast-pool omission for P2P path** Both `add_transaction_to_pool_inner` and `add_blob_transaction_to_pool_inner` return early on `contains_tx`, which checks only `transaction_pool` — not `broadcast_pool`. If a transaction is first submitted via private RPC (`broadcast = false`), it lands in `transaction_pool` but never enters `broadcast_pool`. When the same tx later arrives through P2P (`broadcast = true`), `contains_tx` returns `true` and the function returns `Ok(hash)` without inserting the hash into `broadcast_pool`. The tx is silently never relayed to peers despite the documented guarantee that "P2P-received transactions are unaffected." The same window exists in `add_blob_transaction_to_pool_inner` at line 2356. How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
crates/blockchain/blockchain.rs:2421-2423
**Silent de-duplication hides broadcast-pool omission for P2P path**
Both `add_transaction_to_pool_inner` and `add_blob_transaction_to_pool_inner` return early on `contains_tx`, which checks only `transaction_pool` — not `broadcast_pool`. If a transaction is first submitted via private RPC (`broadcast = false`), it lands in `transaction_pool` but never enters `broadcast_pool`. When the same tx later arrives through P2P (`broadcast = true`), `contains_tx` returns `true` and the function returns `Ok(hash)` without inserting the hash into `broadcast_pool`. The tx is silently never relayed to peers despite the documented guarantee that "P2P-received transactions are unaffected." The same window exists in `add_blob_transaction_to_pool_inner` at line 2356.
Reviews (1): Last reviewed commit: "feat(l1): add --mempool.private flag for..." | Re-trigger Greptile
There was a problem hiding this comment.
Pull request overview
This PR adds a node-level “private local mempool” mode so transactions submitted via RPC are kept local (eligible for locally-built blocks) but are not queued for P2P propagation, mirroring reth’s --txpool.no-local-transactions-propagation.
Changes:
- Introduces
--mempool.private/ETHREX_MEMPOOL_PRIVATEand plumbs it intoBlockchainOptions::private_mempool. - Splits mempool insertion into broadcast vs. no-broadcast paths (
add_transactionvs.add_transaction_no_broadcast) with shared internal logic. - Routes
eth_sendRawTransactionthrough new “local” blockchain entry points that honor the flag.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
cmd/ethrex/cli.rs |
Adds the --mempool.private CLI/env flag and defaults. |
cmd/ethrex/initializers.rs |
Wires the CLI flag into L1 BlockchainOptions. |
cmd/ethrex/l2/initializers.rs |
Wires the CLI flag into L2 BlockchainOptions. |
crates/blockchain/blockchain.rs |
Adds private_mempool option and introduces local vs. P2P mempool insertion APIs (broadcast vs. no-broadcast). |
crates/blockchain/mempool.rs |
Implements add_transaction_no_broadcast via a shared internal helper toggling broadcast-pool insertion. |
crates/networking/rpc/eth/transaction.rs |
Switches eth_sendRawTransaction to use the local insertion APIs so the flag is enforced for RPC submissions. |
test/tests/blockchain/mempool_tests.rs |
Adds a unit test asserting no-broadcast transactions never appear in get_txs_for_broadcast(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
🤖 Codex Code ReviewFindings
Open Question
Summary I could not run tests in this environment: Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
iovoid
left a comment
There was a problem hiding this comment.
We leak the private transactions through send_all_pooled_tx_hashes, which is used to send a list of pooled transactions to new peers. We also send them if asked by txhash.
| .await | ||
| } | ||
|
|
||
| /// EIP-equivalent of `add_blob_transaction_to_pool` for transactions |
…sactions PR #6576 review (@iovoid): the original `--mempool.private` change only filtered the *broadcast* pool, leaving two P2P paths that still leaked the locally-submitted txs to peers: 1. `send_all_pooled_tx_hashes` (in p2p/rlpx/connection/server.rs) sends every mempool tx hash to a freshly-connected peer. 2. `GetPooledTransactions::handle` calls `Blockchain::get_p2p_transaction_by_hash`, which served any tx in the mempool to whoever asked by hash. Fix: track admission as private explicitly. - New `Mempool::private_pool` (`FxHashSet<H256>`) populated by `add_transaction_no_broadcast`, cleared on tx removal alongside `broadcast_pool`. - New `Mempool::is_private(hash) -> Result<bool, StoreError>` for the P2P paths to consult. - `send_all_pooled_tx_hashes` skips any tx whose hash is private before forwarding to the new peer. - `Blockchain::get_p2p_transaction_by_hash` now returns the same `not found`-shaped error for private hashes (the eth/68 spec explicitly allows skipping unavailable txs, so peers handle this cleanly). Test `add_transaction_no_broadcast_marks_tx_as_private_for_p2p_filters` exercises the flag round-trip (add → is_private → remove → !is_private) and covers the unknown-hash path. Existing `add_transaction_no_broadcast_keeps_tx_out_of_broadcast_pool` is unchanged. Drive-by: rename "EIP-equivalent of …" doc on `add_local_blob_transaction_to_pool` to "Local-RPC counterpart of …" per @iovoid's inline comment.
|
Pushed 1. P2P leak paths (@iovoid CHANGES_REQUESTED) — fixed both leaks you identified:
New test 2. "Equivalent to which EIP?" inline ( Ready for re-review. |
Address review findings from the cross-check pass: 1) Mempool::get_txs_for_new_peer_dump: build the broadcast-eligible tx list under a single read lock. The previous send_all_pooled_tx_hashes loop took a separate is_private read lock per tx, which under a hot pool can serialize peer-handshake throughput against any mempool write. The new helper filters out privileged + private txs in one pass and returns the snapshot the P2P dump needs. 2) add_transaction_to_pool_inner (and the EIP-4844 counterpart) now log a warning when contains_tx short-circuits with broadcast=false. If a tx has already been seen via gossip, the local --mempool.private request cannot retroactively un-broadcast it; surfacing this to the operator avoids silent confusion. State is intentionally unchanged (mutating the existing entry would itself violate the original submitter's intent). Integration tests for the P2P/RPC race ordering are not added here: exercising both paths through Blockchain::add_*_transaction_to_pool requires sender-account fixtures we don't have in mempool_tests yet.
Cross-client audit flagged that geth (`core/types/transaction.go::Size`), nethermind (`MaxBlobTxSize`), and erigon (`ValidateSerializedTxn`) all compare their 1 MiB blob-tx cap against the wire form that **includes the sidecar** (blobs + commitments + proofs). The previous ethrex check in `validate_transaction` compared `Transaction::encode_canonical_to_vec` which only covers the core tx — the sidecar lives in the adjacent `BlobsBundle`. With 6 blobs (~786 KB blob data + ~100 KB commitments/proofs ≈ 900 KB) the worst-case wire wrapper can reach ~1.9 MiB while still passing the 1 MiB core-only check. Peers reject that on the wire so ethrex would be admitting txs nobody else will relay. Changes: - `validate_transaction` now only enforces `MAX_TX_SIZE` for non-blob txs. - `add_blob_transaction_to_pool_inner` runs a new wire-wrapper check before bundle validation: `core_tx_encoded + bundle_encoded <= MAX_BLOB_TX_SIZE`. Summing the two encoded sizes matches geth's `tx.Size()` semantic to within the ±few bytes of outer list framing, which is rounding error at this scale. - Drop `validate_transaction_rejects_oversize_blob_core` — the function it covered no longer applies to blob txs. Integration test for the new wrapper check deferred (same pattern as PRs #6603/#6576: the `c-kzg`-gated `add_blob_transaction_to_pool` isn't currently exercised by `mempool_tests`).
…vel config Addresses @iovoid's "Equivalent to which EIP?" question — the flag is not based on an EIP, it's a node-level config. Help text and docs/CLI.md reworded to make that explicit and drop the reference to reth's flag name (consistent with the no-peer-client-name-drops style established elsewhere in this repo's CLI help text).
ElFantasma
left a comment
There was a problem hiding this comment.
Two small observability concerns. Both follow-ups, not blocking.
| let hash = transaction.hash(); | ||
| if self.mempool.contains_tx(hash)? { | ||
| if !broadcast { | ||
| warn!(%hash, "tx already public; --mempool.private cannot retroactively un-broadcast"); |
There was a problem hiding this comment.
The warn! correctly surfaces "this private submission lost the race to a public gossip" — but a log line is easy to miss for operators running with INFO+ and impossible to alert on. Consider adding a counter (METRICS_MEMPOOL.inc_private_collisions() or similar) so the rate of these races is visible in Grafana. Two reasons it matters:
- An operator using
--mempool.privatefor MEV-protected ordering needs to know how often their privacy was already breached by peer gossip before their RPC submission arrived. The warn gives one-time anecdotal data, a metric gives the rate. - The same warn-path exists in the blob path at
:2357, so two log lines for the same condition — a single counter would collapse them.
Not blocking — happy to defer to a follow-up if you'd rather keep the PR scoped to the flag itself.
| // somehow learned the hash. The spec for `GetPooledTransactions` | ||
| // explicitly allows skipping unavailable transactions, so we mirror | ||
| // the "not found" path the caller already handles. | ||
| if self.mempool.is_private(*hash)? { |
There was a problem hiding this comment.
Consistency observation with the get_txs_for_new_peer_dump optimization called out in the PR description ("replaces the per-tx is_private lock the previous loop took"): the same concern still applies to GetPooledTransactions::handle (networking/p2p/rlpx/eth/transactions.rs:228-237), which calls get_p2p_transaction_by_hash in a loop. Each call takes two read locks here (one for is_private, one for get_transaction_by_hash), and a GetPooledTransactions request can carry up to 256 hashes per the eth spec.
A batched get_p2p_transactions_by_hashes(&[H256]) -> Vec<P2PTransaction> that takes a single read lock and applies the is_private filter inline would mirror the dump-path fix and remove a similar peer-handshake-vs-mempool-write contention. Same direction as the existing TODO at transactions.rs:222 ("TODO(#1615): get transactions in batch instead of iterating over them") — this would dovetail with that.
Not blocking for this PR — the flag itself is correctly implemented and tested. Flagging because the description's lock-contention point applies broader than the one path.
…ambdaclass#6599) **Motivation** Every major Ethereum execution client (geth `txMaxSize`, reth `DEFAULT_MAX_TX_INPUT_BYTES`, nethermind `MaxTxSize` / `MaxBlobTxSize`, erigon size enforcement) ships a per-transaction wire-size cap at admission. Without one a single oversized transaction can chew bandwidth and pool capacity at near-zero attacker cost. **Description** - New constants `MAX_TX_SIZE = 128 KiB` and `MAX_BLOB_TX_SIZE = 1 MiB` in `crates/common/types/constants.rs`. - `Blockchain::validate_transaction` enforces `MAX_TX_SIZE` on the canonical RLP encoding of non-blob transactions. - `Blockchain::add_blob_transaction_to_pool` enforces `MAX_BLOB_TX_SIZE` on the **wire wrapper** — `Transaction::encode_canonical_to_vec().len() + BlobsBundle::encode_to_vec().len()` — to match geth (`tx.Size()` includes the attached sidecar), nethermind (`MaxBlobTxSize` is checked on the wrapper form), and erigon (`ValidateSerializedTxn` works on the raw wire bytes which for blob txs is the wrapper-with-sidecar). The two encoded sizes are summed because ethrex stores the core tx and the bundle in separate structs; the ±few bytes of outer list framing are rounding error at the 1 MiB scale. - New error `MempoolError::TxSizeExceeded { actual, limit }`. - Drops the redundant `MAX_TRANSACTION_DATA_SIZE` calldata-only check from `validate_transaction` — the new encoded-size cap is strictly tighter (a 128 KiB calldata payload always encodes to > 128 KiB). - Unit test in `mempool_tests.rs` covering the non-blob path. Integration test for the wrapper check deferred (the `c-kzg`-gated `add_blob_transaction_to_pool` isn't currently exercised by `mempool_tests`, same pattern as PRs lambdaclass#6603/lambdaclass#6576).
# Conflicts: # cmd/ethrex/initializers.rs # cmd/ethrex/l2/initializers.rs # crates/blockchain/blockchain.rs
|
| EIP | Bucket | Count |
|---|---|---|
| EIP-7702 | set_code_txs |
24 |
| EIP-7702 | set_code_txs_2 |
15 |
| EIP-7702 | gas |
1 |
| EIP-8037 | state_gas_set_code |
17 |
| EIP-8037 | state_gas_pricing |
1 |
| EIP-8037 | state_gas_sstore |
1 |
| EIP-7928 | block_access_lists_eip7702 |
8 |
| EIP-7928 | block_access_lists |
1 |
| EIP-7778 | gas_accounting |
3 |
| EIP-7708 | transfer_logs |
1 |
| EIP-7976 | refunds |
1 |
| EIP-1344 | chainid (Amsterdam fork-transition fixture) |
1 |
| Total | 74 |
Re-enable once we either:
- (a) bump fixtures to a snobal-devnet-7 release that locks in the new
accounting; or - (b) revert the bal-devnet-7-prep subtraction for bal-devnet-6
compatibility.
Full test list (74)
EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/set_code_txs/
delegation_clearingdelegation_clearing_and_setdelegation_clearing_failing_txdelegation_clearing_tx_toeoa_tx_after_set_codeext_code_on_chain_delegating_set_codeext_code_on_self_delegating_set_codeext_code_on_self_set_codeext_code_on_set_codemany_delegationsnonce_overflow_after_first_authorizationnonce_validityreset_codeself_code_on_set_codeself_sponsored_set_codeset_code_multiple_valid_authorization_tuples_same_signer_increasing_nonceset_code_multiple_valid_authorization_tuples_same_signer_increasing_nonce_self_sponsoredset_code_to_logset_code_to_non_empty_storage_non_zero_nonceset_code_to_self_destructset_code_to_self_destructing_account_deployed_in_same_txset_code_to_sstoreset_code_to_sstore_then_sloadset_code_to_system_contract
EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/set_code_txs_2/
call_pointer_to_created_from_create_after_oog_call_againcall_to_precompile_in_pointer_contextcontract_storage_to_pointer_with_storagedelegation_replacement_call_previous_contractdouble_authpointer_measurementspointer_normalpointer_reentrypointer_resets_an_empty_code_account_with_storagepointer_revertspointer_to_pointerpointer_to_precompilepointer_to_staticpointer_to_static_reentrystatic_to_pointer
EIP-7702 — for_amsterdam/prague/eip7702_set_code_tx/gas/
account_warming
EIP-8037 — for_amsterdam/amsterdam/eip8037_state_creation_gas_cost_increase/state_gas_set_code/
auth_refund_block_gas_accountingauth_refund_bypasses_one_fifth_capauth_with_calldata_and_access_listauth_with_multiple_sstoresauthorization_exact_state_gas_boundaryauthorization_to_precompile_addressauthorization_with_sstoreduplicate_signer_authorizationsexisting_account_auth_header_gas_used_uses_worst_caseexisting_account_refundexisting_account_refund_enables_sstoreexisting_auth_with_reverted_execution_preserves_intrinsicmany_authorizations_state_gasmixed_auths_header_gas_used_uses_worst_casemixed_new_and_existing_authsmixed_valid_and_invalid_authsmulti_tx_block_auth_refund_and_sstore
EIP-8037 — state_gas_pricing/
auth_state_gas_scales_with_cpsb
EIP-8037 — state_gas_sstore/
sstore_state_gas_all_tx_types
EIP-7928 — for_amsterdam/amsterdam/eip7928_block_level_access_lists/block_access_lists_eip7702/
bal_7702_delegation_clearbal_7702_delegation_createbal_7702_delegation_updatebal_7702_double_auth_resetbal_7702_double_auth_swapbal_7702_null_address_delegation_no_code_changebal_selfdestruct_to_7702_delegationbal_withdrawal_to_7702_delegation
EIP-7928 — block_access_lists/
bal_all_transaction_types
EIP-7778 — for_amsterdam/amsterdam/eip7778_block_gas_accounting_without_refunds/gas_accounting/
multiple_refund_types_in_one_txsimple_gas_accountingvarying_calldata_costs
EIP-7708 — for_amsterdam/amsterdam/eip7708_eth_transfer_logs/transfer_logs/
transfer_with_all_tx_types
EIP-7976 — for_amsterdam/amsterdam/eip7976_increase_calldata_floor_cost/refunds/
gas_refunds_from_data_floor
EIP-1344 — for_amsterdam/istanbul/eip1344_chainid/chainid/
chainid(Amsterdam fork-transition fixture)
Rust 1.91 clippy::redundant_clone (now -D warnings in CI) flags the clone because public_tx is not used after the call. Drop the clone.
Motivation
Mirror reth's
--txpool.no-local-transactions-propagation: a flag to keep RPC-submitted transactions private to this node so they're only included in blocks built locally — no peer gossip. Use cases:Description
New flag
--mempool.private(envETHREX_MEMPOOL_PRIVATE, off by default), placed underNode optionsnext to the existing--mempool.maxsize. When enabled, transactions submitted via this node'seth_sendRawTransactionenter the mempool and may be included in locally-built blocks but are NOT inserted into the broadcast pool. P2P-received transactions are unaffected.Plumbing follows the no-feature-gated-args pattern (separate methods + shared private helper):
BlockchainOptions::private_mempool: bool(defaultfalse).Mempoolgets a siblingadd_transaction_no_broadcastnext toadd_transaction; both share a privateadd_transaction_inner(broadcast: bool)so the broadcast-pool insertion is the only behavioral difference.Blockchain::add_local_transaction_to_poolandadd_local_blob_transaction_to_poolare the RPC entry points — they consultoptions.private_mempooland route to the no-broadcast variant when set. The existingadd_transaction_to_pool(used by the P2P / sync paths) keeps current behavior and always broadcasts.eth_sendRawTransactionhandler now calls the local variants.P2P-gossip race + perf hardening
The new-peer pooled-hash dump (
send_all_pooled_tx_hashes) and the local-submission early-return both have to handle the case where a tx is already in the public pool via gossip:Mempool::get_txs_for_new_peer_dumpreturns the broadcast-eligible snapshot under a single read lock — replaces the per-txis_privatelock the previous loop took, which under a hot pool would serialize peer handshakes against any mempool write.add_transaction_to_pool_inner(+ EIP-4844 path) logs awarn!whencontains_txshort-circuits withbroadcast=false. If the tx has already been seen via gossip, the local--mempool.privaterequest cannot retroactively un-broadcast it; the log surfaces this to the operator. State is intentionally unchanged — mutating the existing entry would itself violate the original submitter's intent.Checklist
STORE_SCHEMA_VERSION— N/A