Skip to content

fix(txpool-rpc): batch validity transaction insertion - #5064

Merged
BrianBland merged 1 commit into
mainfrom
brianbland/validity-insertion-sender
Sep 17, 2026
Merged

BrianBland merged 1 commit into
mainfrom
brianbland/validity-insertion-sender

Conversation

@BrianBland

Copy link
Copy Markdown
Contributor

Change

Route base_sendRawTransactionValidity through reth's shared transaction insertion sender instead of calling the pool directly.

The RPC keeps TransactionOrigin::Private, retains attached validity predicates, and returns the sender's RPC error. The extension injects the registered eth API sender; direct constructors retain the existing direct-pool path for focused tests.

Validation

  • cargo test --locked -p base-txpool-rpc --lib (33 passed)
  • cargo clippy --locked -p base-txpool-rpc --lib -- -D warnings
  • cargo +nightly fmt -p base-txpool-rpc -- --check
  • git diff --check

Generated with Toshi

@cb-heimdall

cb-heimdall commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

✅ Heimdall Review Status

Requirement Status More Info
Reviews 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@depot-code-access

depot-code-access Bot commented Sep 15, 2026

Copy link
Copy Markdown

✅ All benchmarks green — 14 within ±2% (deterministic instruction counts). View run

Benchmark details (14)
Benchmark Base (target) Head (this PR) Δ instructions
batch_queue/drain/drain_cached_span_batches 242,027 242,027 +0.0%
batch_transaction/encode_in_place/encode_in_place 4,199,759 4,199,759 +0.0%
batch_transaction/temporary_frame_buffers/temporary_frame_buffers 8,408,350 8,408,350 +0.0%
flashblock_decode/decode/brotli 3,296,484 3,296,484 +0.0%
flashblock_decode/decode/plain_json 2,280,194 2,280,194 +0.0%
flz/compress_len/real_contract_call 43,148 43,148 +0.0%
flz/compress_len/synthetic_0 38,205 38,205 +0.0%
flz/compress_len/synthetic_1 54,682 54,682 +0.0%
flz/compress_len/synthetic_2 147,976 147,976 +0.0%
flz/data_gas 43,059 43,059 +0.0%
flz/tx_estimated_size 43,056 43,056 +0.0%
frame_parse/decode/single_4kib 1,031 1,031 +0.0%
frame_parse/parse_frames/few_large 1,053,062 1,053,062 +0.0%
frame_parse/parse_frames/many_small 154,763 154,763 +0.0%

@depot-code-access

depot-code-access Bot commented Sep 15, 2026

Copy link
Copy Markdown

Base Std historical fork tests

Fork Result Passed Failed Skipped base/base base-anvil base-std
Beryl pass 616 0 13 a954429c 98e7839c 520d069b
Cobalt pass 725 0 14 a954429c 98e7839c 4571b325

View run

Comment thread crates/execution/txpool-rpc/src/rpc.rs Outdated
pool: Pool,
}

type ValidityTransactionSender = Arc<

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValidityTransactionSender is used in the public method with_validity_limits_and_sender (line 203) but is declared as a crate-private type alias. External consumers of this crate cannot name the type, which makes calling that constructor difficult and would trigger the workspace's unnameable-types = "warn" lint.

Per the project convention ("Make all structs, types, enums, and functions within modules pub, and re-export them from lib.rs"), this should be pub type ValidityTransactionSender = ... and re-exported from lib.rs.

Suggested change
type ValidityTransactionSender = Arc<
pub type ValidityTransactionSender = Arc<

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted: made ValidityTransactionSender public, re-exported it from the crate root, and kept the constructor public. Re-ran fmt, 33 unit tests, clippy with -D warnings, and git diff --check.

🤖 Reply generated with Toshi

@github-actions

Copy link
Copy Markdown
Contributor

<!-- CLAUDE_REVIEW_SUMMARY -->

Review Summary

Clean, well-scoped change that routes base_sendRawTransactionValidity through reth's shared add_pool_transaction sender while preserving TransactionOrigin::Private and attached validity predicates. The fallback to direct pool insertion for tests without the sender is a good design choice.

Findings

# Severity File Description
1 Low rpc.rs:120 ValidityTransactionSender type alias is crate-private but used in the pub fn with_validity_limits_and_sender signature. Should be pub type and re-exported from lib.rs per project conventions and to satisfy the workspace unnameable-types lint.

No block-production-critical issues identified. The change is in the RPC ingress path and add_pool_transaction is the same pool insertion path used by eth_sendRawTransaction, so no new block-production risk is introduced. Test coverage for the new sender path is adequate.

@BrianBland
BrianBland force-pushed the brianbland/validity-insertion-sender branch 2 times, most recently from d24e4ec to 769e5c7 Compare September 15, 2026 23:53
Comment thread crates/execution/txpool-rpc/src/rpc.rs Outdated
Comment on lines 199 to 213
pub fn with_validity_limits_and_sender(
pool: Pool,
provider: Provider,
max_validity_predicates: usize,
max_validity_expiry_secs: u64,
transaction_sender: ValidityTransactionSender,
) -> Self {
Self {
pool,
provider,
max_validity_predicates,
max_validity_expiry_secs,
transaction_sender: Some(transaction_sender),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: When transaction_sender is Some (the production path via SendRawTransactionValidityExtension), the pool field is never read — it's only used in the else fallback for tests that construct the impl directly.

This means callers of with_validity_limits_and_sender must pass a pool that's never used in production. Consider making the pool Option<Pool> or removing it from the sender-backed constructor — though this may require trait-bound gymnastics. Alternatively, a brief doc comment on with_validity_limits_and_sender noting that pool is unused when the sender is present would clarify the intent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3d77731: the RPC now stores the concrete reth BatchTxRequest sender, has no direct-pool fallback, and uses that one route for all submissions. The obsolete callback/API surface is removed.

🤖 Reply generated with Toshi

Comment thread crates/execution/txpool-rpc/src/rpc.rs Outdated
Comment on lines +382 to +390
let transaction = transaction.with_validity_predicates(options.validity);
if let Some(transaction_sender) = &self.transaction_sender {
transaction_sender(TransactionOrigin::Private, transaction).await?;
} else {
self.pool
.add_transaction(TransactionOrigin::Private, transaction)
.await
.map_err(|error| ErrorObjectOwned::from(RpcPoolError::from(error)))?;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error-code divergence between the two insertion paths.

The direct-pool fallback converts via RpcPoolError::from(error), which produces pool-specific JSON-RPC error codes (e.g., ALREADY_KNOWN, UNDERPRICED). The transaction_sender path converts via ErrorObjectOwned::from(BaseEthApiError), which can produce different error codes/messages for the same pool rejection.

RPC callers that pattern-match on error codes will see different responses depending on whether the shared sender is wired in. If the error contract matters for consumers of base_sendRawTransactionValidity, consider normalizing the error conversion so both paths produce the same RpcPoolError-style codes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3d77731: the RPC now stores the concrete reth BatchTxRequest sender, has no direct-pool fallback, and uses that one route for all submissions. The obsolete callback/API surface is removed.

🤖 Reply generated with Toshi

@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

This PR routes base_sendRawTransactionValidity through reth's shared eth_api().add_pool_transaction() sender instead of calling the pool directly, while preserving TransactionOrigin::Private and validity predicates. The change is clean and well-tested.

Findings

  1. Unused pool field in production path (Low) — When transaction_sender is Some (the production wiring in SendRawTransactionValidityExtension), the pool field is never read. Callers of with_validity_limits_and_sender must pass a pool that goes unused. Consider documenting this or restructuring the constructors.

  2. Error-code divergence between insertion paths (Medium) — The direct-pool fallback converts errors via RpcPoolError, producing pool-specific JSON-RPC error codes (ALREADY_KNOWN, UNDERPRICED, etc.). The shared-sender path converts via ErrorObjectOwned::from(BaseEthApiError), which may produce different codes/messages for the same rejection. RPC consumers that match on error codes will see different responses depending on whether the shared sender is wired in.

No block-production-critical findings. This PR touches the RPC ingress path only — transaction pool insertion via add_pool_transaction is the same mechanism used by eth_sendRawTransaction.

@BrianBland
BrianBland force-pushed the brianbland/validity-insertion-sender branch from 769e5c7 to 3d77731 Compare September 16, 2026 00:04
@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

Clean refactor that replaces direct pool insertion with the shared BatchTxRequest channel for base_sendRawTransactionValidity. The Pool generic is fully removed, simplifying the type signatures.

Correctness: The change is sound. The validation pipeline (predicate limits, expiry bounds, fork gates, transaction decoding) runs entirely before the channel send, so validation behavior is preserved. The channel send + oneshot response pattern correctly propagates pool insertion errors back to the RPC caller.

Test coverage: Tests are well-structured with two helpers — test_transaction_sender() (dropped receiver, for validation-only tests that never reach pool insertion) and validity_rpc() (real BatchTxProcessor + NoopTransactionPool, for end-to-end admission tests). All usages are appropriate for their test scenarios.

Block production sensitivity: Not on the critical path. This is purely an RPC ingress endpoint; payload builders read directly from the pool via best_transactions() iterators and have no dependency on this code. The unbounded channel cannot backpressure block production.

No new findings beyond existing inline comments. The prior review comments about error-code divergence and type alias verbosity remain relevant to this iteration, though the specific code references in those comments are stale (they reference with_validity_limits_and_sender, ValidityTransactionSender, and dual pool/sender paths that no longer exist in this version).

@BrianBland
BrianBland added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 17e85d2 Sep 17, 2026
26 checks passed
@BrianBland
BrianBland deleted the brianbland/validity-insertion-sender branch September 17, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants