Skip to content

Add precompile for testing#94

Draft
zjg555543 wants to merge 3 commits intomainfrom
zjg/add-precompile
Draft

Add precompile for testing#94
zjg555543 wants to merge 3 commits intomainfrom
zjg/add-precompile

Conversation

@zjg555543
Copy link
Copy Markdown
Contributor

@zjg555543 zjg555543 commented Jan 16, 2026

1. Background

XLayer is an Optimism-based L2 that requires custom precompiled contracts (Poseidon hash at address 0x100).
The challenge is to integrate these custom precompiles into a Reth-based node without modifying upstream Reth code.

Why This Matters

  • Upstream Compatibility: Keep the ability to sync with upstream Reth updates
  • Clean Separation: Custom logic lives in workspace crates, not in forked dependencies
  • Maintainability: Changes are isolated and easier to review

Key Constraint

Reth's EVM configuration is tightly coupled through the ConfigureEvm trait. We need to inject custom
precompiles at EVM creation time without touching the upstream reth-optimism-evm crate.

2. Solution Architecture

┌─────────────────────────────────────────────────────────────────────┐
│  bin/node/src/main.rs                                               │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ Node Builder                                                   │  │
│  │   .with_components(                                            │  │
│  │       xlayer_node.components()                                 │  │
│  │           .executor(XLayerExecutorBuilder)  ◄─── Custom!       │  │
│  │   )                                                             │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              │ build_evm()
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│  crates/node/src/lib.rs                                             │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ XLayerExecutorBuilder::build_evm()                             │  │
│  │   returns: XLayerEvmConfig                                     │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│  crates/evm/src/config.rs                                           │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ XLayerEvmConfig                                                │  │
│  │   wraps: OpEvmConfig<..., XLayerEvmFactory>  ◄─── Key!         │  │
│  │                                                                 │  │
│  │   OpEvmConfig::new_with_evm_factory(                           │  │
│  │       chain_spec,                                              │  │
│  │       receipt_builder,                                         │  │
│  │       XLayerEvmFactory ◄─── Custom EVM factory                 │  │
│  │   )                                                             │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              │ create_evm() / create_evm_with_inspector()
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│  crates/evm/src/evm_factory.rs                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ XLayerEvmFactory::create_evm()                                 │  │
│  │   1. let mut evm = OpEvmFactory::default().create_evm(...)     │  │
│  │   2. *evm.components_mut().2 = xlayer_precompiles_map(spec)    │  │
│  │      ▲                                                          │  │
│  │      └── Replaces precompiles without touching OpEvm!          │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              │ xlayer_precompiles(hardfork)
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│  crates/evm/src/factory.rs                                          │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ xlayer_precompiles(hardfork: OpHardfork)                       │  │
│  │   returns: &'static Precompiles                                │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│  crates/evm/src/precompiles/mod.rs                                  │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ XLayerPrecompiles::precompiles()                               │  │
│  │   if hardfork >= Jovian:                                       │  │
│  │       xlayer_with_poseidon()  ◄── Adds Poseidon                │  │
│  │   else:                                                         │  │
│  │       Precompiles::latest()                                    │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────┐
│  crates/evm/src/precompiles/poseidon.rs                             │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │ pub const POSEIDON: Precompile                                 │  │
│  │   address: 0x0000000000000000000000000000000000000100          │  │
│  │   run: poseidon_run()                                          │  │
│  │                                                                 │  │
│  │ fn poseidon_run(input, gas_limit) -> PrecompileResult          │  │
│  │   - Validates input length (multiple of 32)                    │  │
│  │   - Calculates gas: BASE + PER_INPUT * num_inputs              │  │
│  │   - Executes: poseidon_hash(input)                             │  │
│  │   - Returns 32-byte hash                                       │  │
│  └───────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

Key Design Points

  1. No Upstream Changes: All custom code lives in crates/evm/, crates/node/, and bin/node/
  2. Wrapper Pattern: XLayerEvmConfig wraps OpEvmConfig with a custom EVM factory
  3. Factory Injection: XLayerEvmFactory delegates to OpEvmFactory then swaps precompiles
  4. Static Precompiles: xlayer_precompiles() returns &'static Precompiles for performance
  5. Hardfork Activation: Poseidon is only active for OpHardfork::Jovian and later

3. Key Code Call Stack

Startup Path (Node Launch → EVM Creation)

main()
  ├─ NodeBuilder::with_components(xlayer_node.components().executor(XLayerExecutorBuilder))
  │
  └─ XLayerExecutorBuilder::build_evm(ctx)
      │
      └─ xlayer_evm_config(ctx.chain_spec())
          │
          └─ XLayerEvmConfig::new(chain_spec)
              │
              └─ OpEvmConfig::new_with_evm_factory(
                     chain_spec,
                     OpRethReceiptBuilder::default(),
                     XLayerEvmFactory::default()  ◄── Custom factory injected here
                 )

Transaction Execution Path (TX → Precompile)

BlockExecutor::execute_and_verify_one()
  │
  ├─ EvmFactory::create_evm(db, env)
  │   │
  │   └─ XLayerEvmFactory::create_evm(db, env)
  │       ├─ let mut evm = OpEvmFactory::default().create_evm(db, env)
  │       ├─ let spec_id = env.cfg_env.spec()
  │       └─ *evm.components_mut().2 = xlayer_precompiles_map(spec_id)
  │           │
  │           └─ xlayer_precompiles(hardfork_from_spec_id(spec_id))
  │               │
  │               └─ XLayerPrecompiles::new(hardfork).precompiles()
  │                   │
  │                   └─ if hardfork >= Jovian:
  │                          xlayer_with_poseidon()  ◄── Poseidon added here
  │                       else:
  │                          Precompiles::latest()
  │
  └─ evm.transact()
      │
      └─ [if tx.to == 0x100]
          │
          └─ poseidon_run(input, gas_limit)
              ├─ Validate input (multiple of 32 bytes)
              ├─ Calculate gas: 60 + 6 * num_inputs
              ├─ poseidon_hash(input) → 32-byte output
              └─ return PrecompileOutput::new(gas_used, output)

Verification

Test the precompile with:

# Call Poseidon at 0x100 with single input (0x01)
cast call 0x0000000000000000000000000000000000000100 \
  0x0000000000000000000000000000000000000000000000000000000000000001 \
  --rpc-url http://localhost:8124 \
  --gas-limit 100000

# Estimate gas
cast rpc eth_estimateGas \
  '{"to":"0x0000000000000000000000000000000000000100","data":"0x0000000000000000000000000000000000000000000000000000000000000001"}' \
  --rpc-url http://localhost:8124

Or run the automated test:

./tests/test-precompile.sh

@Vui-Chee
Copy link
Copy Markdown
Contributor

@review

@JimmyShi22

This comment was marked as abuse.

@XLayer-Bot

This comment was marked as abuse.

XLayer-Bot

This comment was marked as abuse.

@XLayer-Bot

This comment was marked as abuse.

XLayer-Bot

This comment was marked as abuse.

XLayer-Bot

This comment was marked as abuse.

@XLayer-Bot

This comment was marked as abuse.

@okx okx deleted a comment from XLayer-Bot Apr 10, 2026
@okx okx deleted a comment from XLayer-Bot Apr 10, 2026
@okx okx deleted a comment from XLayer-Bot Apr 10, 2026
@okx okx deleted a comment from XLayer-Bot Apr 10, 2026
@JimmyShi22
Copy link
Copy Markdown
Contributor

@claude pls review this

Copy link
Copy Markdown

@XLayer-Bot XLayer-Bot left a comment

Choose a reason for hiding this comment

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

Overall the architecture is clean and the injection pattern (wrapping OpEvmFactory / swapping the precompile map) is a solid approach for adding custom precompiles without forking upstream. A few issues need to be addressed before this can ship.

workflow run

/// Input format: N * 32 bytes (N field elements)
/// Output format: 32 bytes (one field element)
fn poseidon_hash(input: &[u8]) -> Result<Bytes, PrecompileError> {
let _ = input;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Critical: poseidon_hash is not implemented.

fn poseidon_hash(input: &[u8]) -> Result<Bytes, PrecompileError> {
    let _ = input;
    Ok(Bytes::from(vec![0u8; 32]))  // always returns 32 zero bytes
}

The input is explicitly discarded and the function always returns 32 zero bytes. The poseidon-rs, num-bigint, and ff_ce crates are added to Cargo.toml but never imported or used anywhere. Any contract relying on 0x100 for a real Poseidon hash will silently receive wrong results. This needs to be wired up to an actual Poseidon implementation before being deployed.

let num_inputs = if input.is_empty() { 0 } else { input.len() / 32 };
let gas_cost = POSEIDON_BASE_GAS + (num_inputs as u64) * POSEIDON_PER_INPUT_GAS;

tracing::info!(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tracing::info! is too verbose for a precompile hot path. Every transaction targeting 0x100 emits two structured info-level log lines (entry + success). In production this creates significant log noise proportional to Poseidon call volume. Please lower these to tracing::trace! (or at most debug!).

xlayer_with_poseidon()
} else {
// Use standard OP Stack precompiles.
Precompiles::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.

Precompiles::latest() returns the most recent precompile set known to the current revm version, not the set that corresponds to a specific hardfork. For a node syncing in e.g. Holocene context, this silently enables precompiles from hardforks that haven't activated yet. The recommended approach is to map each hardfork to its snapshot (e.g. Precompiles::cancun(), Precompiles::prague() etc.). The same issue applies to line 35 inside xlayer_with_poseidon() where Precompiles::latest() is used as the base for the Jovian+ set.

self,
ctx: &BuilderContext<Node>,
pool: Pool,
_evm_config: xlayer_evm::XLayerEvmConfig,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flashblocks sequencer path discards XLayerEvmConfig.

async fn spawn_payload_builder_service(
    self,
    ctx: &BuilderContext<Node>,
    pool: Pool,
    _evm_config: xlayer_evm::XLayerEvmConfig,  // unused — note the underscore
) -> ... {
    let base_evm = OpEvmConfig::optimism(ctx.chain_spec());  // fresh stock config
    FlashblocksServiceBuilder::spawn_payload_builder_service(self.0, ctx, pool, base_evm).await
}

The XLayerEvmConfig (which carries the Poseidon precompile) is silently dropped and replaced with a vanilla OpEvmConfig. When running as a flashblocks sequencer, blocks are built without the Poseidon precompile registered, so any transaction calling 0x100 will fail during block building even though it passes during execution/validation. The XLayerEvmConfig should be passed through (or the flashblocks builder needs to be updated to support custom EVM configs).

.with_add_ons(add_ons)
.with_components(
xlayer_node.components()
.executor(XLayerExecutorBuilder) // ← 使用自定义 Executor with Poseidon
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Development comment with Chinese text () left in production code. Please remove or replace with an English comment if explanation is needed.

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.

4 participants