Skip to content

feat: add ev-dev local chain and txpool transaction support#145

Open
randygrok wants to merge 1 commit intomainfrom
randygrok/ev-dev-local-chain
Open

feat: add ev-dev local chain and txpool transaction support#145
randygrok wants to merge 1 commit intomainfrom
randygrok/ev-dev-local-chain

Conversation

@randygrok
Copy link
Contributor

@randygrok randygrok commented Feb 27, 2026

Description

Adds ev-dev binary providing a one-command local development chain with pre-funded Hardhat accounts, similar to Anvil or Hardhat Node. Also fixes the payload builder to pull pending transactions from the txpool when Engine API attributes are empty, enabling cast send and other RPC-submitted transactions to be included in blocks during --dev mode. Includes DebugNode implementation for EvolveNode and dynamic chain ID parsing in the banner.

Summary by CodeRabbit

Release Notes

  • New Features

    • One-command local development chain (ev-dev) with pre-funded test accounts for rapid local development
    • Transaction sponsor service for signing EvNode transactions on behalf of users via JSON-RPC proxy
  • Bug Fixes

    • RPC-submitted transactions now properly included in blocks during development
    • Improved transaction pool ordering and balance validation for sponsored transactions

…mode

Add ev-dev binary providing a one-command local development chain with
pre-funded Hardhat accounts, similar to Anvil or Hardhat Node. Includes
genesis configuration and Makefile targets for convenience.

Fix payload builder to pull pending transactions from the txpool when
Engine API attributes are empty, enabling cast send and other RPC-submitted
transactions to be included in blocks during --dev mode.

Add DebugNode implementation for EvolveNode supporting local payload
attributes builder.
@randygrok randygrok requested a review from a team as a code owner February 27, 2026 18:55
@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @randygrok's task —— View job


PR Review Complete ✅

I've thoroughly analyzed this PR that adds ev-dev local development chain and txpool transaction support. Here's my comprehensive review:

Review Checklist:

  • Read and understand the changed files
  • Review code quality and best practices
  • Identify potential bugs or issues
  • Assess performance considerations
  • Check security concerns
  • Evaluate test coverage
  • Provide comprehensive feedback

🎯 Overview

This is a well-implemented PR that adds a valuable development tool (ev-dev) and fixes an important gap in the payload builder. The implementation is clean and follows existing patterns.

Code Quality & Best Practices

  • Excellent: Consistent with existing codebase patterns
  • Good: Proper error handling with expect() calls and context
  • Good: Comprehensive CLI argument parsing with clap
  • Good: Proper use of Arc for shared ownership
  • Good: Follows Rust naming conventions

🐛 Potential Issues

  1. Genesis Path Validation (bin/ev-dev/src/main.rs:143-147):

    • The temp file path conversion could theoretically fail on some platforms
    • Consider adding better error handling for the to_str() conversion
  2. Account Limit Enforcement (bin/ev-dev/src/main.rs:47,79):

    • CLI allows max 20 accounts but derive_keys() doesn't enforce this limit
    • Could allocate unnecessary memory if user passes large numbers

🚀 Performance Considerations

  • Good: Uses block_in_place() appropriately for async/sync boundaries
  • Good: Temporary directories are cleaned up automatically
  • Minor: Key derivation happens twice (banner + actual use) - could be optimized

🔐 Security Analysis

  • ✅ Safe: Hard-coded Hardhat mnemonic is well-known test keys (appropriate for dev use)
  • ✅ Safe: Clear warnings about fund security in banner
  • ✅ Safe: Uses temp directories with proper cleanup
  • ✅ Safe: CORS set to * only for dev environment (appropriate)
  • Minor: Consider rate limiting or connection limits for dev server

🧪 Test Coverage

  • Good: Existing test in payload_service.rs covers the core changes
  • Suggestion: Consider adding integration tests for ev-dev binary specifically
  • Suggestion: Test the txpool transaction pulling functionality in dev mode

📋 Detailed Code Review

New ev-dev Binary

  • Excellent implementation following Hardhat/Anvil patterns
  • Clean argument parsing and banner display
  • Proper temporary file management

Payload Service Enhancement

Key improvement in crates/node/src/payload_service.rs:182-197:

let transactions = if attributes.transactions.is_empty() {
    // Pull from pool when Engine API attributes are empty
    let pool_txs: Vec<TransactionSigned> = self
        .pool
        .pending_transactions()
        .into_iter()
        .map(|tx| tx.transaction.clone_into_consensus().into_inner())
        .collect();
    // ... logging and return
} else {
    attributes.transactions.clone()
};

This elegantly solves the dev mode transaction inclusion issue.

Node.rs DebugNode Implementation

Proper implementation of DebugNode trait enabling debug capabilities.

Genesis Configuration

  • Valid Ethereum genesis with appropriate EIP activations
  • Pre-funded accounts match Hardhat's standard setup
  • Evolve-specific configuration looks correct

🔧 Recommendations

  1. Add account limit validation:

    // In derive_keys()
    let count = count.min(20); // Enforce max limit
  2. Consider caching derived keys:

    // Derive once and reuse for banner and CLI
    let accounts = derive_keys(args.accounts.min(20));
  3. Add integration test for dev mode:

    • Test that RPC-submitted transactions are included in blocks
    • Verify txpool integration works correctly
  4. Consider adding transaction timeout/retry logic in dev mode for better UX

🎉 Positive Highlights

  • Excellent UX: Banner design matches professional dev tools
  • Smart fallback logic: Engine API → txpool transaction sourcing
  • Proper documentation: Clear comments explaining behavior
  • Makefile integration: Good developer experience with make dev-chain
  • Clean architecture: Maintains separation of concerns

📊 Summary

This is a high-quality PR that significantly improves the developer experience. The implementation is solid, follows best practices, and addresses a real need in the development workflow. The few minor issues identified are non-critical and could be addressed in follow-up PRs.

Recommendation: ✅ Approve with minor suggestions


@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

This PR introduces an ev-dev binary for local development with pre-funded Hardhat accounts, adds supporting configuration files and build targets, and modifies the payload builder to pull pending transactions from the txpool while fixing sponsored transaction handling in the network layer.

Changes

Cohort / File(s) Summary
Build Configuration
CHANGELOG.md, Cargo.toml, Makefile
Added documentation of new ev-dev and sponsor service features; registered ev-dev as a workspace binary; defined build and execution targets for the development chain.
ev-dev Binary
bin/ev-dev/Cargo.toml, bin/ev-dev/assets/devnet-genesis.json, bin/ev-dev/src/main.rs
Implemented a one-command local development chain with CLI argument parsing, pre-funded test account generation from a fixed mnemonic, genesis file loading, banner display, and node execution with custom txpool RPC module injection.
Payload Builder & Node Integration
crates/node/src/payload_service.rs, crates/node/src/node.rs
Added generic Pool parameter to EvolveEnginePayloadBuilder to pull pending transactions from txpool; introduced DebugNode trait implementation for EvolveNode with block conversion and custom payload attributes builder support.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 A dev chain blooms with ev-dev's call,
Pre-funded accounts for testing all,
Payload builder drinks from txpool's spring,
Sponsored transactions dance and swing,
Local development—simplified bliss! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description provides a clear summary of changes but does not follow the provided template structure with required sections like Type of Change, Checklist, Testing, and Related Issues. Restructure the description to match the template by adding Type of Change (mark New feature), Checklist items, Testing section, and Related Issues. The summary content is good but needs proper template formatting.
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately summarizes the main changes: adding the ev-dev local chain binary and enabling txpool transaction support in the payload builder.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch randygrok/ev-dev-local-chain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
Makefile (1)

58-64: Add new command targets to .PHONY to prevent accidental skipped execution.

Without .PHONY, make dev-chain / make build-ev-dev can be treated as up-to-date if same-named files exist.

♻️ Proposed Makefile update
-.PHONY: all build test clean fmt lint run help
+.PHONY: all build build-dev build-maxperf build-ev-dev build-all \
+        test test-verbose test-unit test-integration test-node test-evolve test-common \
+        run run-dev dev-chain fmt fmt-check lint check clean update audit doc doc-all check-all help
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 58 - 64, Add the new make targets to the .PHONY list
so they are always executed: update the existing .PHONY definition to include
build-ev-dev and dev-chain (the targets defined by the rules for "build-ev-dev:"
and "dev-chain:") so make won't treat them as up-to-date when files with those
names exist.
crates/node/src/payload_service.rs (3)

325-413: Add a regression test for attributes empty -> txpool fallback transaction inclusion.

This PR’s core behavior change isn’t directly asserted yet; please add an integration test that seeds pool txs and verifies they are included when attributes are empty.

Based on learnings: Write integration tests in crates/tests/ covering Engine API interactions, payload building with transactions, state execution validation, and Evolve-specific scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/node/src/payload_service.rs` around lines 325 - 413, Add an
integration test under crates/tests/ that seeds a real/test transaction pool and
verifies EvolveEnginePayloadBuilder.try_build includes pool transactions when
RpcPayloadAttributes.withdrawals and other attributes are empty: replace
NoopTransactionPool in the setup (EvolveEnginePayloadBuilder.pool) with a seeded
test pool implementation (or a helper that implements TransactionPool and
inserts known EvPooledTransaction(s)), call
engine_builder.try_build(BuildArguments::new(...)) with
EthPayloadBuilderAttributes built from empty RpcPayloadAttributes, then assert
the built payload contains those transactions (check payload.transaction list or
tx_count field and that known tx hashes are present). Use the same fixtures from
the test (EvolvePayloadBuilder, EvolveEnginePayloadBuilder, BuildArguments,
RpcPayloadAttributes, PayloadConfig) and name the test to reflect "attributes
empty -> txpool fallback".

139-143: tx_count span field is inaccurate on txpool fallback path.

When attributes.transactions is empty, the span still reports 0 even if pool transactions are used.

🧭 Suggested instrumentation adjustment
 #[instrument(skip(self, args), fields(
-    tx_count = args.config.attributes.transactions.len(),
+    tx_count = tracing::field::Empty,
     payload_id = %args.config.attributes.payload_id(),
     duration_ms = tracing::field::Empty,
 ))]
 fn try_build(
@@
         let transactions = if attributes.transactions.is_empty() {
@@
         } else {
             attributes.transactions.clone()
         };
+
+        tracing::Span::current().record("tx_count", transactions.len() as u64);

Also applies to: 182-197

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/node/src/payload_service.rs` around lines 139 - 143, The span field
tx_count set in the #[instrument(..., fields(tx_count =
args.config.attributes.transactions.len(), ...))] attribute is stale on the
txpool fallback path; change to compute the actual transaction count at runtime
inside the function (determine whether to use
args.config.attributes.transactions or pool transactions), then update the span
after that decision by recording the real count into the span (e.g.,
Span::current().record or equivalent) using the same field name "tx_count";
apply the same fix for the other instrumented block referenced around lines
182-197 so both places reflect the actual tx count rather than
args.config.attributes.transactions.len().

185-190: Avoid unbounded materialization of the entire pending pool each build.

Collecting all pending txs into a Vec per payload build can degrade latency/memory under larger pools; prefer a bounded selection strategy for fallback mode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/node/src/payload_service.rs` around lines 185 - 190, The current
fallback path in payload_service.rs materializes the entire pending pool into
pool_txs by collecting pending_transactions() into a Vec, which can blow
memory/latency; change the fallback selection to a bounded strategy: iterate
pending_transactions() without collecting all, apply a deterministic or
priority-based filter and .take(MAX_FALLBACK_TXS) (or reservoir/sample if
needed), and build the payload from that bounded iterator instead of the full
Vec; update references to pool_txs and the place that expects
Vec<TransactionSigned> (e.g., where TransactionSigned instances are consumed) to
accept the streamed/limited selection and define a MAX_FALLBACK_TXS constant for
clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/ev-dev/src/main.rs`:
- Around line 46-49: The documented max for the CLI flag 'accounts' is not
enforced at parse time; update the 'accounts' argument declaration (the struct
field named accounts) to validate the range at parse-time (e.g., add a Clap
value/validator that restricts values to 1..=20) so invalid inputs produce an
error instead of being silently clamped, or alternatively adjust the
help/default text to reflect the actual behavior; ensure the change is applied
on the 'accounts' field annotation used by clap.
- Line 148: Run rustfmt/cargo fmt to fix formatting in main.rs: reformat the
file (or run `cargo fmt`) to resolve the lint error reported at line 148; ensure
the `main` function and surrounding declarations in bin/ev-dev/src/main.rs are
formatted to match rustfmt rules and commit the resulting changes.
- Around line 192-194: Replace the .expect("valid CLI args") panic by handling
the result of Cli::<EvolveChainSpecParser, EvolveArgs>::try_parse_from(args)
explicitly: match or map the Err case and convert the clap parse error into a
user-facing error (e.g., print the clap error via eprintln!("{}", err) or return
Err(err.into()) from main) so the launcher fails gracefully instead of
panicking; then pass the successful parsed Cli value into .run(|builder,
_evolve_args| async move { ... }) as before.

In `@crates/node/src/payload_service.rs`:
- Line 189: The file crates/node/src/payload_service.rs has formatting
mismatches flagged by CI; run rustfmt (cargo fmt) and reformat the file so the
closure/map expressions like the one that returns
tx.transaction.clone_into_consensus().into_inner() and the nearby expressions
are formatted according to rustfmt rules (fix spacing, line breaks and trailing
commas) — simply run cargo fmt and commit the resulting changes to resolve the
lint failures at the locations currently flagged.

---

Nitpick comments:
In `@crates/node/src/payload_service.rs`:
- Around line 325-413: Add an integration test under crates/tests/ that seeds a
real/test transaction pool and verifies EvolveEnginePayloadBuilder.try_build
includes pool transactions when RpcPayloadAttributes.withdrawals and other
attributes are empty: replace NoopTransactionPool in the setup
(EvolveEnginePayloadBuilder.pool) with a seeded test pool implementation (or a
helper that implements TransactionPool and inserts known
EvPooledTransaction(s)), call engine_builder.try_build(BuildArguments::new(...))
with EthPayloadBuilderAttributes built from empty RpcPayloadAttributes, then
assert the built payload contains those transactions (check payload.transaction
list or tx_count field and that known tx hashes are present). Use the same
fixtures from the test (EvolvePayloadBuilder, EvolveEnginePayloadBuilder,
BuildArguments, RpcPayloadAttributes, PayloadConfig) and name the test to
reflect "attributes empty -> txpool fallback".
- Around line 139-143: The span field tx_count set in the #[instrument(...,
fields(tx_count = args.config.attributes.transactions.len(), ...))] attribute is
stale on the txpool fallback path; change to compute the actual transaction
count at runtime inside the function (determine whether to use
args.config.attributes.transactions or pool transactions), then update the span
after that decision by recording the real count into the span (e.g.,
Span::current().record or equivalent) using the same field name "tx_count";
apply the same fix for the other instrumented block referenced around lines
182-197 so both places reflect the actual tx count rather than
args.config.attributes.transactions.len().
- Around line 185-190: The current fallback path in payload_service.rs
materializes the entire pending pool into pool_txs by collecting
pending_transactions() into a Vec, which can blow memory/latency; change the
fallback selection to a bounded strategy: iterate pending_transactions() without
collecting all, apply a deterministic or priority-based filter and
.take(MAX_FALLBACK_TXS) (or reservoir/sample if needed), and build the payload
from that bounded iterator instead of the full Vec; update references to
pool_txs and the place that expects Vec<TransactionSigned> (e.g., where
TransactionSigned instances are consumed) to accept the streamed/limited
selection and define a MAX_FALLBACK_TXS constant for clarity.

In `@Makefile`:
- Around line 58-64: Add the new make targets to the .PHONY list so they are
always executed: update the existing .PHONY definition to include build-ev-dev
and dev-chain (the targets defined by the rules for "build-ev-dev:" and
"dev-chain:") so make won't treat them as up-to-date when files with those names
exist.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c34841b and a07c19c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • CHANGELOG.md
  • Cargo.toml
  • Makefile
  • bin/ev-dev/Cargo.toml
  • bin/ev-dev/assets/devnet-genesis.json
  • bin/ev-dev/src/main.rs
  • crates/node/src/node.rs
  • crates/node/src/payload_service.rs

Comment on lines +46 to +49
/// Number of accounts to display (max 20)
#[arg(long, default_value_t = 10)]
accounts: usize,
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

accounts max is documented but not enforced at input boundary.

Current behavior silently clamps later; consider validating at parse-time (or adjust help text) so CLI behavior matches the contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/ev-dev/src/main.rs` around lines 46 - 49, The documented max for the CLI
flag 'accounts' is not enforced at parse time; update the 'accounts' argument
declaration (the struct field named accounts) to validate the range at
parse-time (e.g., add a Clap value/validator that restricts values to 1..=20) so
invalid inputs produce an error instead of being silently clamped, or
alternatively adjust the help/default text to reflect the actual behavior;
ensure the change is applied on the 'accounts' field annotation used by clap.

.to_str()
.expect("valid path")
.to_string();

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

cargo fmt is still needed in this file (CI lint failure).

Formatting mismatch is reported at Line 148.

🧰 Tools
🪛 GitHub Actions: lint

[error] 148-148: rustfmt formatting changes detected. Run 'cargo fmt' to fix formatting in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/ev-dev/src/main.rs` at line 148, Run rustfmt/cargo fmt to fix formatting
in main.rs: reformat the file (or run `cargo fmt`) to resolve the lint error
reported at line 148; ensure the `main` function and surrounding declarations in
bin/ev-dev/src/main.rs are formatted to match rustfmt rules and commit the
resulting changes.

Comment on lines +192 to +194
if let Err(err) = Cli::<EvolveChainSpecParser, EvolveArgs>::try_parse_from(args)
.expect("valid CLI args")
.run(|builder, _evolve_args| async move {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid panic on nested CLI parse errors; return a user-facing error instead.

Using .expect("valid CLI args") can crash the launcher on invalid composed args instead of failing gracefully.

🛠️ Proposed error-handling fix
-    if let Err(err) = Cli::<EvolveChainSpecParser, EvolveArgs>::try_parse_from(args)
-        .expect("valid CLI args")
-        .run(|builder, _evolve_args| async move {
+    let cli = match Cli::<EvolveChainSpecParser, EvolveArgs>::try_parse_from(args) {
+        Ok(cli) => cli,
+        Err(err) => {
+            eprintln!("{err}");
+            std::process::exit(2);
+        }
+    };
+
+    if let Err(err) = cli.run(|builder, _evolve_args| async move {
             info!("=== EV-DEV: Starting local development chain ===");
             let handle = builder
                 .node(EvolveNode::new())
@@
-        })
+        })
     {
         eprintln!("Error: {err:?}");
         std::process::exit(1);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/ev-dev/src/main.rs` around lines 192 - 194, Replace the .expect("valid
CLI args") panic by handling the result of Cli::<EvolveChainSpecParser,
EvolveArgs>::try_parse_from(args) explicitly: match or map the Err case and
convert the clap parse error into a user-facing error (e.g., print the clap
error via eprintln!("{}", err) or return Err(err.into()) from main) so the
launcher fails gracefully instead of panicking; then pass the successful parsed
Cli value into .run(|builder, _evolve_args| async move { ... }) as before.

.pool
.pending_transactions()
.into_iter()
.map(|tx| tx.transaction.clone_into_consensus().into_inner())
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

cargo fmt is still required in this file (CI is failing).

Lint already flags formatting mismatches at Line 189, Line 310, Line 319, and Line 411.

Also applies to: 310-310, 319-319, 411-411

🧰 Tools
🪛 GitHub Actions: lint

[error] 189-189: rustfmt formatting changes detected. Run 'cargo fmt' to fix formatting in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/node/src/payload_service.rs` at line 189, The file
crates/node/src/payload_service.rs has formatting mismatches flagged by CI; run
rustfmt (cargo fmt) and reformat the file so the closure/map expressions like
the one that returns tx.transaction.clone_into_consensus().into_inner() and the
nearby expressions are formatted according to rustfmt rules (fix spacing, line
breaks and trailing commas) — simply run cargo fmt and commit the resulting
changes to resolve the lint failures at the locations currently flagged.

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.

1 participant