Skip to content

feat: add proposer selection #228

Open
tac0turtle wants to merge 7 commits into
mainfrom
marko/precompile_proposer
Open

feat: add proposer selection #228
tac0turtle wants to merge 7 commits into
mainfrom
marko/precompile_proposer

Conversation

@tac0turtle
Copy link
Copy Markdown
Contributor

@tac0turtle tac0turtle commented Apr 27, 2026

Description

this pr adds proposer selection to ev-reth based on evstack/ev-node#3282

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Refactoring

Related Issues

Fixes #(issue)

Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Testing

Additional Notes

Summary by CodeRabbit

  • New Features

    • Added proposer control functionality enabling execution-layer proposer rotation
    • Added RPC API method to query next proposer state
    • Added chain spec configuration options for proposer control setup (admin, activation height, initial value)
  • Documentation

    • Added architecture decision record documenting proposer rotation design
    • Updated precompile documentation with proposer control interface and usage examples

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a proposer-control precompile for EVM-based proposer rotation. It spans the precompile contract, EVM factory integration, chainspec configuration, executor wiring, a new JSON-RPC API, node binary registration, and supporting documentation. All changes work together to enable execution-owned proposer management.

Changes

Proposer Rotation Precompile

Layer / File(s) Summary
Precompile Contract Implementation
crates/ev-precompiles/src/proposer.rs, crates/ev-precompiles/src/lib.rs
New ProposerControlPrecompile contract exposes nextProposer(), setNextProposer(bytes32), and admin() methods. Validates that only the admin can update the proposer, stores values in a fixed storage slot, and defaults to initial_next_proposer when slot is zero. Comprehensive tests cover authorization, static-call rejection, and default behavior.
EVM Factory Settings & Installation
crates/ev-revm/src/factory.rs, crates/ev-revm/src/lib.rs
Adds ProposerControlPrecompileSettings struct and installation helpers to both EvEvmFactory and EvTxEvmFactory. Precompile registers at PROPOSER_CONTROL_PRECOMPILE_ADDR once the configured activation height is reached. New test validates that storage writes occur only after activation.
Chainspec Configuration
crates/node/src/config.rs
Extends chainspec deserialization to load proposer-control admin, activation height, and initial next proposer. EvolvePayloadBuilderConfig exposes these via a public proposer_control_precompile_settings() method. Zero address disables the feature; activation defaults to 0 when admin is set but activation is missing.
Payload Service Propagation
crates/node/src/payload_service.rs
Copies proposer-control config fields from self.config into the evolved chain spec during payload builder construction.
Executor Wiring
crates/node/src/executor.rs
build_evm_config reads proposer settings, logs activation details, and constructs ProposerControlPrecompileSettings for the EvTxEvmFactory.
Proposer RPC API
crates/node/src/proposer_rpc.rs, crates/node/Cargo.toml
New EvolveProposerApi JSON-RPC trait with getNextProposer(block) method. Implementation loads precompile storage at the requested block, returns configured fallback when slot is zero. Adds jsonrpsee dependencies (server, macros features).
Node RPC Registration
bin/ev-dev/src/main.rs, bin/ev-reth/src/main.rs
During RPC module setup, both dev and main nodes construct EvolveProposerApiImpl from chain spec, compute initial_next_proposer, and merge the proposer RPC module alongside existing txpool RPC for HTTP/WS transports.
Test Fixtures
crates/tests/src/common.rs
Updates EvolveTestFixture::new to extract proposer settings from config and wire them into EvTxEvmFactory.
Documentation
crates/ev-precompiles/README.md, docs/adr/ADR-0004-proposer-rotation-precompile.md
Precompile README covers interface, address, chainspec fields, and example transactions. ADR-0004 specifies design, storage semantics, authorization, backward compatibility, and test requirements.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • evstack/ev-reth#145: Both PRs modify bin/ev-dev/src/main.rs's RPC module setup by wiring the txpool RPC (and the main PR extends that same RPC setup with proposer RPC integration).

Suggested reviewers

  • randygrok
  • damiannolan

Poem

A rabbit hops through precompile fields,
Where proposers spin and storage yields,
From chainspec to RPC, the config flows free,
Building next-block authority with glee! 🐰✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete; the Type of Change checkbox is unchecked, testing details are missing, and most checklist items remain unchecked despite significant implementation work. Complete the template by selecting the appropriate type of change (New feature), filling in testing details, describing how the feature was validated, and indicating which checklist items were completed.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and generic, using non-descriptive phrasing that doesn't convey the specific scope or key aspects of the changes. Replace with a more specific title that captures the core change, such as 'feat: add proposer-control precompile for EVM-based proposer selection' or 'feat: implement proposer rotation via precompile.'
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch marko/precompile_proposer

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.

@randygrok randygrok marked this pull request as ready for review June 8, 2026 09:04
@randygrok randygrok requested a review from a team as a code owner June 8, 2026 09:04
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bin/ev-dev/src/main.rs`:
- Around line 305-315: The TUI startup path fails to register the proposer RPC,
so evolve_getNextProposer is unavailable when run_with_tui is used; fix by
creating and merging the proposer API in the TUI branch the same way as the
non-TUI branch: construct EvolvePayloadBuilderConfig (or reuse proposer_cfg),
derive initial_next_proposer, instantiate EvolveProposerApiImpl (proposer_api)
with ctx.provider() and the initial value, then call
ctx.modules.merge_configured(proposer_api.into_rpc()) alongside merging
evolve_txpool in the run_with_tui code path so the proposer RPC is registered
for TUI mode as well.

In `@crates/ev-precompiles/README.md`:
- Around line 202-204: The fenced code block containing the address
0x000000000000000000000000000000000000f101 should include a language tag to
satisfy MD040; update the fence opening from ``` to ```text so the block is
fenced as text (locate the fenced block containing the address string and add
the language tag).
- Around line 208-252: The README docs and example ABI need to use bytes32 for
proposer values to match the precompile: update the IProposerControl interface
declaration (replace address with bytes32 for nextProposer() and
setNextProposer(bytes32)), change the JSON config keys (proposerControlAdmin
stays address but initialNextProposer must be a bytes32 value), and update all
example CLI commands and cast calls to pass/expect a bytes32 (and the
corresponding calldata/signature) instead of an address; verify references to
nextProposer and setNextProposer in the prose reflect bytes32 semantics to avoid
mismatched calldata with crates/ev-precompiles/src/proposer.rs.

In `@docs/adr/ADR-0004-proposer-rotation-precompile.md`:
- Around line 93-99: The ADR currently declares proposer identity as address (in
the IProposerControl interface and related prose) but the implementation uses
bytes32 proposer IDs; update the ADR to use bytes32 for nextProposer() and
setNextProposer(bytes32 proposer) in the IProposerControl symbol references and
change all prose/semantics that mention “zero address” to reference the bytes32
zero value (bytes32::ZERO) instead; apply the same change to the other affected
sections noted (around the prose covering lines 134-156 and 166-167) so the API
signatures and semantics match the implemented bytes32 proposer identity while
leaving admin() as an address if implementation uses address for admin.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 74e3a819-0c8e-4041-a931-d5e6a76d39e8

📥 Commits

Reviewing files that changed from the base of the PR and between 0e613d1 and 335fc02.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • bin/ev-dev/src/main.rs
  • bin/ev-reth/src/main.rs
  • crates/ev-precompiles/README.md
  • crates/ev-precompiles/src/lib.rs
  • crates/ev-precompiles/src/proposer.rs
  • crates/ev-revm/src/factory.rs
  • crates/ev-revm/src/lib.rs
  • crates/node/Cargo.toml
  • crates/node/src/config.rs
  • crates/node/src/executor.rs
  • crates/node/src/lib.rs
  • crates/node/src/payload_service.rs
  • crates/node/src/proposer_rpc.rs
  • crates/tests/src/common.rs
  • docs/adr/ADR-0004-proposer-rotation-precompile.md

Comment thread bin/ev-dev/src/main.rs
Comment on lines +305 to 315
let proposer_cfg =
EvolvePayloadBuilderConfig::from_chain_spec(ctx.config().chain.as_ref())?;
let initial_next_proposer = proposer_cfg
.proposer_control_precompile_settings()
.map(|(_, _, initial_next_proposer)| initial_next_proposer)
.unwrap_or_default();
let proposer_api =
EvolveProposerApiImpl::new(ctx.provider().clone(), initial_next_proposer);
ctx.modules.merge_configured(evolve_txpool.into_rpc())?;
ctx.modules.merge_configured(proposer_api.into_rpc())?;
Ok(())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Proposer RPC is not registered in TUI mode.

You register proposer RPC in the non-TUI startup path, but Line 380-385 (run_with_tui) still only merges txpool. This makes evolve_getNextProposer unavailable whenever --tui is used.

Suggested fix
@@
         let _handle = builder
             .node(EvolveNode::new())
             .extend_rpc_modules(move |ctx| {
                 let evolve_cfg = EvolveConfig::default();
                 let evolve_txpool =
                     EvolveTxpoolApiImpl::new(ctx.pool().clone(), evolve_cfg.max_txpool_bytes);
+                let proposer_cfg =
+                    EvolvePayloadBuilderConfig::from_chain_spec(ctx.config().chain.as_ref())?;
+                let initial_next_proposer = proposer_cfg
+                    .proposer_control_precompile_settings()
+                    .map(|(_, _, initial_next_proposer)| initial_next_proposer)
+                    .unwrap_or_default();
+                let proposer_api =
+                    EvolveProposerApiImpl::new(ctx.provider().clone(), initial_next_proposer);
                 ctx.modules.merge_configured(evolve_txpool.into_rpc())?;
+                ctx.modules.merge_configured(proposer_api.into_rpc())?;
                 Ok(())
             })
             .launch_with_debug_capabilities()
             .await?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/ev-dev/src/main.rs` around lines 305 - 315, The TUI startup path fails to
register the proposer RPC, so evolve_getNextProposer is unavailable when
run_with_tui is used; fix by creating and merging the proposer API in the TUI
branch the same way as the non-TUI branch: construct EvolvePayloadBuilderConfig
(or reuse proposer_cfg), derive initial_next_proposer, instantiate
EvolveProposerApiImpl (proposer_api) with ctx.provider() and the initial value,
then call ctx.modules.merge_configured(proposer_api.into_rpc()) alongside
merging evolve_txpool in the run_with_tui code path so the proposer RPC is
registered for TUI mode as well.

Comment on lines +202 to +204
```
0x000000000000000000000000000000000000f101
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the new fenced block.

Line 202 opens a fence without a language (```), which triggers MD040. Use ```text for the address block.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 202-202: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ev-precompiles/README.md` around lines 202 - 204, The fenced code
block containing the address 0x000000000000000000000000000000000000f101 should
include a language tag to satisfy MD040; update the fence opening from ``` to
```text so the block is fenced as text (locate the fenced block containing the
address string and add the language tag).

Source: Linters/SAST tools

Comment on lines +208 to +252
```solidity
interface IProposerControl {
function nextProposer() external view returns (address);
function setNextProposer(address proposer) external;
function admin() external view returns (address);
}
```

### Configuration

```json
{
"config": {
"evolve": {
"proposerControlAdmin": "0x1234567890123456789012345678901234567890",
"proposerControlActivationHeight": 0,
"initialNextProposer": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
}
}
}
```

For existing chains, set `proposerControlActivationHeight` to a future block and upgrade all nodes
before that height. `initialNextProposer` should be the currently active proposer so reads are stable
before the first rotation transaction.

### Operations

The configured admin rotates the next proposer with:

```bash
cast send --rpc-url $RPC_URL --private-key $ADMIN_KEY \
0x000000000000000000000000000000000000f101 \
"setNextProposer(address)" 0xNEXT_PROPOSER
```

The stored proposer can be read through either the precompile ABI or ev-reth's convenience RPC:

```bash
cast call --rpc-url $RPC_URL \
0x000000000000000000000000000000000000f101 \
"nextProposer()(address)"

cast rpc --rpc-url $RPC_URL evolve_getNextProposer latest
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update proposer ABI/docs to bytes32 to match the implemented precompile.

Lines 210-212, 241-242, and 249 currently document address, but crates/ev-precompiles/src/proposer.rs implements nextProposer()/setNextProposer as bytes32. Following the current README will produce incorrect calldata/signatures and failed calls.

Suggested doc diff
 interface IProposerControl {
-    function nextProposer() external view returns (address);
-    function setNextProposer(address proposer) external;
+    function nextProposer() external view returns (bytes32);
+    function setNextProposer(bytes32 proposer) external;
     function admin() external view returns (address);
 }

-cast send ... "setNextProposer(address)" 0xNEXT_PROPOSER
+cast send ... "setNextProposer(bytes32)" 0x<64-hex-bytes>

-cast call ... "nextProposer()(address)"
+cast call ... "nextProposer()(bytes32)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```solidity
interface IProposerControl {
function nextProposer() external view returns (address);
function setNextProposer(address proposer) external;
function admin() external view returns (address);
}
```
### Configuration
```json
{
"config": {
"evolve": {
"proposerControlAdmin": "0x1234567890123456789012345678901234567890",
"proposerControlActivationHeight": 0,
"initialNextProposer": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"
}
}
}
```
For existing chains, set `proposerControlActivationHeight` to a future block and upgrade all nodes
before that height. `initialNextProposer` should be the currently active proposer so reads are stable
before the first rotation transaction.
### Operations
The configured admin rotates the next proposer with:
```bash
cast send --rpc-url $RPC_URL --private-key $ADMIN_KEY \
0x000000000000000000000000000000000000f101 \
"setNextProposer(address)" 0xNEXT_PROPOSER
```
The stored proposer can be read through either the precompile ABI or ev-reth's convenience RPC:
```bash
cast call --rpc-url $RPC_URL \
0x000000000000000000000000000000000000f101 \
"nextProposer()(address)"
cast rpc --rpc-url $RPC_URL evolve_getNextProposer latest
```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ev-precompiles/README.md` around lines 208 - 252, The README docs and
example ABI need to use bytes32 for proposer values to match the precompile:
update the IProposerControl interface declaration (replace address with bytes32
for nextProposer() and setNextProposer(bytes32)), change the JSON config keys
(proposerControlAdmin stays address but initialNextProposer must be a bytes32
value), and update all example CLI commands and cast calls to pass/expect a
bytes32 (and the corresponding calldata/signature) instead of an address; verify
references to nextProposer and setNextProposer in the prose reflect bytes32
semantics to avoid mismatched calldata with
crates/ev-precompiles/src/proposer.rs.

Comment on lines +93 to +99
```solidity
interface IProposerControl {
function nextProposer() external view returns (address);
function setNextProposer(address proposer) external;
function admin() external view returns (address);
}
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bring ADR API and semantics in sync with implemented bytes32 proposer identity.

Line 95-97 and related semantics (Lines 134-156, 166-167) still specify address/“zero address”, but the implementation stores bytes32 proposer IDs and validates zero as bytes32::ZERO. This mismatch can mislead downstream adapters and tooling.

Also applies to: 134-156, 166-167

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/ADR-0004-proposer-rotation-precompile.md` around lines 93 - 99, The
ADR currently declares proposer identity as address (in the IProposerControl
interface and related prose) but the implementation uses bytes32 proposer IDs;
update the ADR to use bytes32 for nextProposer() and setNextProposer(bytes32
proposer) in the IProposerControl symbol references and change all
prose/semantics that mention “zero address” to reference the bytes32 zero value
(bytes32::ZERO) instead; apply the same change to the other affected sections
noted (around the prose covering lines 134-156 and 166-167) so the API
signatures and semantics match the implemented bytes32 proposer identity while
leaving admin() as an address if implementation uses address for admin.

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.

2 participants