feat(l1): add TxOrigin enum to thread RPC vs P2P submission through admission#6608
feat(l1): add TxOrigin enum to thread RPC vs P2P submission through admission#6608ilitteri wants to merge 7 commits into
Conversation
Introduce a `TxOrigin::{Local, External}` enum that tags transactions
entering the mempool by their provenance (RPC vs P2P). Thread the
origin through `Blockchain::validate_transaction` and the four pool
entry points: `add_transaction_to_pool` and
`add_blob_transaction_to_pool` (P2P, `External`) plus new
`add_local_transaction_to_pool` and `add_local_blob_transaction_to_pool`
helpers (RPC, `Local`). `eth_sendRawTransaction` now routes through
the local variants.
Add `BlockchainOptions::nolocals` (default `false`) and a
`--mempool.nolocals` CLI flag (env `ETHREX_MEMPOOL_NOLOCALS`,
documented in `docs/CLI.md`) plumbed through both the L1 and L2
initializers. When set, locally-submitted txs are treated as
external for admission purposes.
`validate_transaction` computes `locals_exempt = !nolocals && origin
== Local` and carries a `TODO(#6604)` noting where the min-tip floor
exemption is wired once PR #6604 lands. Until then, the structure is
in place and behavior is unchanged for any check present on `main`.
Tests in `test/tests/blockchain/mempool_tests.rs` exercise the
threading: both origins reach the same downstream errors today, and
`add_local_transaction_to_pool` is shown to route through validation.
🤖 Kimi Code ReviewThis PR introduces infrastructure for origin-aware mempool admission policies (distinguishing RPC-submitted vs P2P-received transactions). The implementation is clean and well-documented. Minor observations:
Security/Logic verification:
Code quality:
Approval: LGTM. The preparatory structure is sound and ready for the min-tip floor logic in the follow-up PR. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Claude Code ReviewNow I have everything I need to write a thorough review. PR Review:
|
Lines of code reportTotal lines added: Detailed view |
Greptile SummaryThis PR introduces
Confidence Score: 4/5Safe to merge; the change is behavior-neutral on main and the exemption only activates after PR #6604 lands. The plumbing is clean, all P2P and RPC call sites are correctly updated, and both L1/L2 initializers thread the new flag. The only open items are that _locals_exempt is computed but never read (intentional scaffolding), and the architecture-doc snippet for validate_transaction is now further out of date than before the PR. docs/l1/architecture/crate_map.md — the validate_transaction public API snippet is stale and now diverges more from reality.
|
| Filename | Overview |
|---|---|
| crates/blockchain/mempool.rs | Adds the TxOrigin enum with Local / External variants; clean, well-documented addition with no logic changes. |
| crates/blockchain/blockchain.rs | Adds BlockchainOptions::nolocals, add_local_transaction_to_pool, add_local_blob_transaction_to_pool, and the origin: TxOrigin parameter to validate_transaction; _locals_exempt is computed but intentionally unused (scaffolding for #6604). |
| crates/networking/rpc/eth/transaction.rs | Routes eth_sendRawTransaction through add_local_*_to_pool helpers, correctly tagging RPC-submitted transactions as TxOrigin::Local. |
| cmd/ethrex/cli.rs | Adds --mempool.nolocals / ETHREX_MEMPOOL_NOLOCALS CLI flag with correct default (false) and proper help_heading. |
| cmd/ethrex/initializers.rs | Threads nolocals: opts.mempool_nolocals into BlockchainOptions during L1 init; one-line plumbing change. |
| cmd/ethrex/l2/initializers.rs | Same nolocals plumbing for L2 init path. |
| test/tests/blockchain/mempool_tests.rs | Updates existing test calls to pass TxOrigin::External; adds three new scaffolding tests. The add_local_transaction_to_pool_routes_through_validation assertion is result.is_err() only (no specific error variant check), which is intentionally loose. |
| docs/CLI.md | Adds --mempool.nolocals entry in both the L1 and L2 sections of the CLI reference; consistent with the arg definition. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[eth_sendRawTransaction] -->|EIP-4844| B[add_local_blob_transaction_to_pool]
A -->|other tx types| C[add_local_transaction_to_pool]
B --> D[add_blob_transaction_to_pool_with_origin\norigin = Local]
C --> E[add_transaction_to_pool_with_origin\norigin = Local]
F[P2P peer] -->|rlpx/transactions| G[add_transaction_to_pool\norigin = External]
F -->|rlpx/server| G
G --> E
D --> H[validate_transaction\ntx, sender, origin]
E --> H
H --> I{locals_exempt?\n!nolocals and origin==Local}
I -->|true - future: skip min-tip floor| J[TODO #6604: if !locals_exempt\ncheck min-tip floor]
I -->|false - always enforce| J
J --> K[remaining admission checks\nnonce, gas, balance, ...]
Comments Outside Diff (1)
-
docs/l1/architecture/crate_map.md, line 108 (link)Stale public-API snippet in architecture docs
The snippet still shows the old two-argument, synchronous
validate_transactionsignature (txonly, returningResult<(), MempoolError>). After this PR the real signature ispub async fn validate_transaction(&self, tx: &Transaction, sender: Address, origin: TxOrigin) -> Result<Option<H256>, MempoolError>. The signature had already drifted (missingsender) before this PR landed, but the neworiginparameter widens the gap further. Any reader relying on this for integration or review will be misled.Prompt To Fix With AI
This is a comment left during a code review. Path: docs/l1/architecture/crate_map.md Line: 108 Comment: **Stale public-API snippet in architecture docs** The snippet still shows the old two-argument, synchronous `validate_transaction` signature (`tx` only, returning `Result<(), MempoolError>`). After this PR the real signature is `pub async fn validate_transaction(&self, tx: &Transaction, sender: Address, origin: TxOrigin) -> Result<Option<H256>, MempoolError>`. The signature had already drifted (missing `sender`) before this PR landed, but the new `origin` parameter widens the gap further. Any reader relying on this for integration or review will be misled. How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
docs/l1/architecture/crate_map.md:108
**Stale public-API snippet in architecture docs**
The snippet still shows the old two-argument, synchronous `validate_transaction` signature (`tx` only, returning `Result<(), MempoolError>`). After this PR the real signature is `pub async fn validate_transaction(&self, tx: &Transaction, sender: Address, origin: TxOrigin) -> Result<Option<H256>, MempoolError>`. The signature had already drifted (missing `sender`) before this PR landed, but the new `origin` parameter widens the gap further. Any reader relying on this for integration or review will be misled.
### Issue 2 of 2
crates/blockchain/blockchain.rs:2493
**Unused variable `_locals_exempt` silences dead-code lint**
`_locals_exempt` is computed but never read; the leading underscore is the only thing preventing a compiler warning. While the PR description explains this is intentional scaffolding for PR #6604, the dead binding means the expression `!self.options.nolocals && origin == TxOrigin::Local` is never exercised in any code path today. If the follow-up PR forgets to remove the underscore when wiring the `if !_locals_exempt` guard, the exemption will silently remain dead. Prefer an explicit `#[allow(unused_variables)]` with an `// until #6604` annotation, or a `let _ = ...;` assignment, to make the intentional deferral even more visible to reviewers.
Reviews (1): Last reviewed commit: "feat(l1): add TxOrigin enum and local-tx..." | Re-trigger Greptile
| // `locals_exempt` gates origin-aware exemptions. When the operator sets | ||
| // `--mempool.nolocals`, locally-submitted txs are treated like external | ||
| // ones for admission purposes. | ||
| let _locals_exempt = !self.options.nolocals && origin == TxOrigin::Local; |
There was a problem hiding this comment.
Unused variable
_locals_exempt silences dead-code lint
_locals_exempt is computed but never read; the leading underscore is the only thing preventing a compiler warning. While the PR description explains this is intentional scaffolding for PR #6604, the dead binding means the expression !self.options.nolocals && origin == TxOrigin::Local is never exercised in any code path today. If the follow-up PR forgets to remove the underscore when wiring the if !_locals_exempt guard, the exemption will silently remain dead. Prefer an explicit #[allow(unused_variables)] with an // until #6604 annotation, or a let _ = ...; assignment, to make the intentional deferral even more visible to reviewers.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/blockchain.rs
Line: 2493
Comment:
**Unused variable `_locals_exempt` silences dead-code lint**
`_locals_exempt` is computed but never read; the leading underscore is the only thing preventing a compiler warning. While the PR description explains this is intentional scaffolding for PR #6604, the dead binding means the expression `!self.options.nolocals && origin == TxOrigin::Local` is never exercised in any code path today. If the follow-up PR forgets to remove the underscore when wiring the `if !_locals_exempt` guard, the exemption will silently remain dead. Prefer an explicit `#[allow(unused_variables)]` with an `// until #6604` annotation, or a `let _ = ...;` assignment, to make the intentional deferral even more visible to reviewers.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Pull request overview
This PR introduces transaction-origin plumbing (Local vs External) to support origin-aware mempool admission policies (notably a future “local tx” exemption from the min-tip floor), plus an operator flag (--mempool.nolocals) to disable those exemptions.
Changes:
- Add
TxOrigin { Local, External }and thread it throughBlockchain::validate_transactionand mempool admission entry points. - Route
eth_sendRawTransactionthrough new “local” add-to-pool APIs so RPC submissions are taggedLocal. - Add
BlockchainOptions::nolocals, CLI plumbing, docs updates, and a few tests covering the new origin parameter and local entry point.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/tests/blockchain/mempool_tests.rs | Updates validation calls to pass TxOrigin and adds new tests for origin plumbing/local entry point. |
| docs/CLI.md | Documents new --mempool.nolocals flag (L1 and L2 sections). |
| crates/networking/rpc/eth/transaction.rs | Routes eth_sendRawTransaction through local mempool entry points (including blob tx path). |
| crates/blockchain/mempool.rs | Introduces the TxOrigin enum. |
| crates/blockchain/blockchain.rs | Adds nolocals option, introduces local/external add-to-pool wrappers, and extends validate_transaction with origin. |
| cmd/ethrex/l2/initializers.rs | Plumbs CLI mempool_nolocals into L2 blockchain options. |
| cmd/ethrex/initializers.rs | Plumbs CLI mempool_nolocals into L1 blockchain options. |
| cmd/ethrex/cli.rs | Adds --mempool.nolocals / ETHREX_MEMPOOL_NOLOCALS option. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // RPC-submitted transactions are tagged as `TxOrigin::Local` so they may | ||
| // bypass admission gates (such as the min-tip floor) intended to protect | ||
| // against P2P spam. See `Blockchain::add_local_transaction_to_pool`. | ||
| let hash = if let SendRawTransactionRequest::EIP4844(wrapped_blob_tx) = self { | ||
| context | ||
| .blockchain | ||
| .add_blob_transaction_to_pool( | ||
| .add_local_blob_transaction_to_pool( | ||
| wrapped_blob_tx.tx.clone(), | ||
| wrapped_blob_tx.blobs_bundle.clone(), | ||
| ) |
| // Threading check: the RPC entry point must route through validation. A | ||
| // transaction whose gas limit exceeds the block's must be rejected with | ||
| // `TxGasLimitExceededError`, proving that we did not accidentally bypass | ||
| // `validate_transaction` in `add_local_transaction_to_pool`. | ||
| let (config, header) = build_basic_config_and_header(false, false); | ||
| let store = setup_storage(config, header).await.expect("Storage setup"); | ||
| let blockchain = Blockchain::default_with_store(store); | ||
|
|
||
| // A canonical legacy tx (sender derivable from signature) with `gas_limit` | ||
| // beyond the test block's limit (100_000_000). Reused from the existing | ||
| // mempool fixtures (`test_filter_mempool_transactions`). | ||
| let tx = Transaction::decode_canonical(&hex::decode("f86d80843baa0c4082f618946177843db3138ae69679a54b95cf345ed759450d870aa87bee538000808360306ba0151ccc02146b9b11adf516e6787b59acae3e76544fdcd75e77e67c6b598ce65da064c5dd5aae2fbb535830ebbdad0234975cd7ece3562013b63ea18cc0df6c97d4").unwrap()).unwrap(); | ||
|
|
||
| let result = blockchain.add_local_transaction_to_pool(tx).await; | ||
| // The fixture tx has chain_id None, so it should hit NotEnoughBalance | ||
| // (sender not in storage) — same outcome as `add_transaction_to_pool`. | ||
| // The point of the assertion is that the call returns an error from | ||
| // `validate_transaction` rather than silently inserting. | ||
| assert!(result.is_err(), "local tx must be rejected by validation"); |
| [default: 10000] | ||
|
|
||
| --mempool.nolocals | ||
| Disable admission exemptions for locally-submitted transactions. When set, RPC-submitted txs are subject to the same admission gates (e.g. min-tip floor) as P2P-received ones. |
| #[arg( | ||
| help = "Disable admission exemptions for locally-submitted transactions. \ | ||
| When set, RPC-submitted txs are subject to the same admission gates \ | ||
| (e.g. min-tip floor) as P2P-received ones.", | ||
| long = "mempool.nolocals", | ||
| default_value_t = false, | ||
| help_heading = "Node options", | ||
| env = "ETHREX_MEMPOOL_NOLOCALS" | ||
| )] | ||
| pub mempool_nolocals: bool, |
🤖 Codex Code Review
No EVM opcode, gas-accounting, consensus, trie, or RLP issues stood out in this diff beyond the mempool admission boundary above. I couldn’t run the Rust tests here because Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Phase 2 review (greptile + Copilot) flagged three related issues: - `_locals_exempt` was a dead binding silenced by an underscore prefix; if PR #6604 forgot to remove the underscore when wiring the gate, the exemption would silently never fire. - `--mempool.nolocals` CLI flag and `docs/CLI.md` entry implied the flag changes behavior, but with no origin-gated rule on main it's a no-op — an operator-visible knob that does nothing. - `BlockchainOptions::nolocals` field had no consumers. Strip the scaffold: keep the `TxOrigin` enum and the `origin` parameter threading through `validate_transaction` (those are real plumbing, not dead code), but drop the dead variable, the unused option, the no-op CLI flag, and the two docs entries. The `TODO(#6604)` comment in `validate_transaction` records exactly how the exemption should be wired (and that the flag should land in the same PR).
Phase 2 review (Copilot): `eth_sendRawTransaction` called `Blockchain::add_local_blob_transaction_to_pool` unconditionally for EIP-4844 requests, but that method is `#[cfg(feature = "c-kzg")]` in `ethrex-blockchain`. Building `ethrex-rpc` without enabling the c-kzg feature failed to compile because the called method didn't exist. Add a forwarding `c-kzg` feature on `ethrex-rpc` and `#[cfg]`-gate the EIP-4844 arm. When the feature is disabled, the handler returns a clear `RpcErr::Internal` instead of failing to build. Verified both `cargo check -p ethrex-rpc --no-default-features` and `cargo check -p ethrex-rpc --features c-kzg` succeed.
Phase 2 review (Copilot): - The previous test comment claimed the legacy fixture's `gas_limit` exceeded the block limit, but the RLP carries `0xF618` (63K gas), well below the 100M block cap. The fixture never exercised `TxGasLimitExceededError`. - `assert!(result.is_err())` was too loose — it would pass on any error, including ones unrelated to `validate_transaction`. Tighten the assertion to `NotEnoughBalance` OR `StoreError(_)` so the test specifically proves admission reached the validation path. The minimal test store doesn't seed account state, so the lookup may surface as either variant; what we need is for the call to NOT return `Ok(_)`. Updated the comment to match the actual fixture. Also drop the companion `blockchain_options_default_keeps_local_exemption` test that referenced the removed `nolocals` field.
|
| 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)
When this branch added TxOrigin to validate_transaction, a tx-size test carried over from main was left on the old 2-arg form. The test is unrelated to origin-specific behavior, so use TxOrigin::External to match the P2P-default path other tests in this file use.
Motivation
PR #6604 introduces an admission-time minimum tip floor that rejects P2P-received transactions whose effective gas tip is below an operator-configured threshold. The floor is the correct behavior for spam-sensitive ingress but is too restrictive for operator-controlled workflows (sequencers, internal services, key-rotation tooling) that submit transactions directly via RPC.
This PR adds the language and plumbing to distinguish those origins so the floor (and other future operator-friendly exemptions) can opt locally-submitted transactions out by default.
Description
pub enum TxOrigin { Local, External }incrates/blockchain/mempool.rs. Orthogonal to PR feat(l1): add --mempool.private flag for non-propagating local txs #6576'sis_privatepropagation flag.Blockchain::validate_transactiongains anorigin: TxOriginparameter (marked_originuntil feat(l1): enforce minimum priority-fee floor at mempool admission #6604 wires the exemption). Existing P2P entry points (add_transaction_to_pool,add_blob_transaction_to_pool) passExternal; newadd_local_transaction_to_poolandadd_local_blob_transaction_to_poolhelpers passLocal. Both are thin wrappers around shared private routes.eth_sendRawTransactionnow routes through the local variants.validate_transactioncarries aTODO(#6604)block marking exactly where the min-tip floor check should be gated once PR feat(l1): enforce minimum priority-fee floor at mempool admission #6604 merges. The opt-out operator flag (e.g.--mempool.nolocals) is intentionally NOT added here — exposing a no-op operator knob would be confusing. The flag should land in the same PR that wires the exemption.Behavioral change
This PR is behavior-neutral on
mainby itself — it only adds the type and threading. The min-tip floor exemption activates when PR #6604 merges and theTODO(#6604)is wired (a one-lineifwrap), at which point the companion operator flag also lands.Deferred
if origin != TxOrigin::Local { ... }gating around the min-tip floor is deferred to a follow-up that lands after PR feat(l1): enforce minimum priority-fee floor at mempool admission #6604.TxOrigin::Privatefor non-propagation) is intentionally out of scope; it stays orthogonal to PR feat(l1): add --mempool.private flag for non-propagating local txs #6576'sis_private.