以太坊完成执行层与共识层的拆分后,很多旧项目依赖的节点仍然把两类职责混在一起。EIP-8110通过定义执行层外部接口,让React这类前端应用必须重新审视节点连接方式。执行层只负责交易执行、状态读取和EVM交互,区块头信息、同步状态和验证者数据需要走共识层客户端。迁移时如果继续把RPC请求打到同一个端口,就会出现方法不存在或返回结构不符合预期的问题。

EIP-8110执行层与旧节点模式的核心差异
EIP-8110对执行层客户端暴露的JSON-RPC方法做了收敛,移除了与共识相关的eth_syncing部分字段和parity扩展方法,同时强化了eth_getBlockByNumber、eth_getTransactionReceipt等核心方法对执行层数据的返回保证。旧版React应用通常通过web3.js或ethers.js连接一个单一RPC地址,再用同一个Provider实例查询余额、发送交易、获取最新区块号。迁移后,获取最新区块号仍属于执行层能力,但要区分该块是否被最终确认,必须向共识层端点请求最终性信息。
很多组件在渲染时会调用blockNumber来展示同步状态,执行层返回的block.number是最近的执行块号,不代表已经进入规范链。如果直接把该值展示为已确认高度,可能误导用户。建议在React状态层拆分为执行块高和共识层最终块高两个数据源。这种差异对应用影响最大的是轮询逻辑和区块确认等待。
// 旧方案:一个Provider同时获取执行和共识信息
const provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545');
const blockNumber = await provider.getBlockNumber();
const syncStatus = await provider.send('eth_syncing', []);
// 迁移后 执行层只保留执行数据
const execProvider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545');
const latestBlock = await execProvider.getBlockNumber();
// 共识层数据单独请求
const beaconProvider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:5052');
const finality = await beaconProvider.send('eth/v1/node/finality_checkpoints', []);
如果继续使用旧的轮询组件去拉取执行层eth_syncing方法,迁移后可能只得到null或缺少字段,导致前端渲染崩溃。需要在迁移前排查所有涉及同步状态、最终性标记的组件,把获取逻辑拆分成两个独立请求。对于只展示当前执行块号的组件,可以保持原逻辑不变,但要注意文案上避免出现已确认字样。
React中初始化执行层Provider的改造
在React应用里,Provider通常放在Context中统一注入,组件通过useContext获取。迁移到EIP-8110执行层后,建议创建两个独立的Provider上下文:一个指向执行层RPC端点,另一个指向共识层RPC端点。这样做的好处是组件可以按需选择数据源,不需要在每次调用时手动切换连接地址,也方便后续对共识层端点做缓存或限流。
很多团队担心双Provider会增加组件复杂度,实际上可以把执行层Provider作为默认值,大多数交易相关操作仍然从默认Provider读取。只有需要最终性数据或验证者信息时,才从共识层Provider获取。这样改动量最小,核心业务代码几乎不需要重写。
import React, { createContext, useContext } from 'react';
import { JsonRpcProvider } from '@ethersproject/providers';
const ExecutionContext = createContext(null);
const ConsensusContext = createContext(null);
export function Providers({ children }) {
const execProvider = new JsonRpcProvider('http://127.0.0.1:8545');
const consProvider = new JsonRpcProvider('http://127.0.0.1:5052');
return (
<ExecutionContext.Provider value={execProvider}>
<ConsensusContext.Provider value={consProvider}>
{children}
</ConsensusContext.Provider>
</ExecutionContext.Provider>
);
}
export function useExecutionProvider() {
return useContext(ExecutionContext);
}
export function useConsensusProvider() {
return useContext(ConsensusContext);
}
上述结构可以直接替换原有的单Provider写法。如果项目使用Redux或其他状态管理库,也可以把Provider实例放到store外部创建,避免在组件内部频繁实例化。需要注意执行层和共识层端点可能部署在不同主机或端口,环境变量配置要拆分成两类,不要把同一个URL同时赋给两个Provider。
迁移过程中还有一个容易忽略的点:某些第三方库自动检测网络ID时会调用net_version方法,该方法在执行层依然可用,但返回的是执行链ID。如果应用依赖链ID做分支逻辑,迁移后不会受影响。但若库内部还会请求eth_getBlockByNumber并解析共识字段,就需要升级库版本或改用轻量封装。
交易发送与状态更新的兼容策略
执行层负责交易池和广播,EIP-8110对交易序列化格式没有根本变化,但要求客户端对EIP-1559费用模型的支持更严格。React应用如果直接调用sendTransaction,Provider会尝试从执行层节点获取nonce和gas估算。迁移后需要确保nonce来自执行层,但gas价格可以通过执行层建议值,也可以使用外部价格源。如果React应用中使用了本地钱包签名,需要把签名后的交易通过eth_sendRawTransaction发送到执行层节点,避免共识层无效请求。
很多项目在升级后会遇到交易一直停留在pending状态的情况,根本原因往往是把签名后的原始交易发送给了共识层RPC端点,或者用共识层Provider去估算gas。正确做法是任何涉及gas估算、nonce获取、交易广播的操作都只走执行层Provider。共识层Provider只用于查询最终性、验证者集合等只读数据。
// 使用执行层Provider发送已签名交易
async function sendSignedTransaction(execProvider, signedTx) {
const txHash = await execProvider.send('eth_sendRawTransaction', [signedTx]);
return txHash;
}
// 获取nonce必须走执行层
async function getNextNonce(execProvider, address) {
const nonce = await execProvider.getTransactionCount(address, 'pending');
return nonce;
}
对于需要等待交易确认的界面,旧代码通常会轮询getTransactionReceipt直到receipt出现。在EIP-8110执行层环境下,该逻辑依然有效,因为receipt数据只存在于执行层。但要注意如果同时需要展示最终性状态,比如资金已完全不可逆转,就需要额外请求共识层端点。可以把等待逻辑拆成两个阶段:先等待执行层receipt,再异步查询共识层最终性状态并更新UI提示。
迁移后的调试与测试注意事项
改造完成后,本地开发环境需要同时运行执行层客户端和共识层客户端。执行层可以选择Hardhat、Anvil等模拟节点,共识层可以使用独立模拟器或直接连接公开测试网。如果只启动执行层模拟节点而缺少共识层端点,React应用在初始化共识Provider时不会报错,但后续请求最终性数据会失败。建议在开发启动脚本里检查两个端点是否可访问,避免运行一段时间后才暴露问题。
测试用例也要同步调整。原来只需要mock一个Provider的测试,现在需要准备两个mock对象,分别返回执行层和共识层的数据。对于涉及交易签名和广播的单测,可以只关注执行层Provider的行为,把共识层Provider置为null或返回固定最终性数据。这样测试依然保持快速,不会因为引入共识层模拟而大幅增加复杂度。
性能方面,由于共识层查询可能比执行层慢,建议对最终性数据做轻量缓存,避免在React组件每次渲染时都发起请求。可以用简单的内存缓存结合定时刷新策略,例如每12秒更新一次最终性状态。交易相关的数据不要缓存,以免展示过期nonce或余额。