EIP-1898 object selectors fail before downstream RPC in eth-json-rpc-middleware 26.0.0
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 2/5
- 预计耗时
- 1-3 小时
- 新手友好度
- 62/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- typescript
- 领域
- api, blockchain
调研方向
该问题位于 packages/eth-json-rpc-middleware/src/retryOnEmpty.ts 中,其中非字符串的区块引用被当作数字处理;同时在 src/block-cache.ts 中,此类选择器可能进入数字或 latest 缓存分组。先阅读这两个文件中处理区块引用的部分,然后运行提议的 src/eip1898-regression.test.ts。当对象选择器用例在两种中间件顺序下都通过,并且现有的中间件测试套件保持通过时,即视为完成。
由索引模型根据 Issue 内容生成。
描述
Summary
eth_call with { blockHash, requireCanonical: true } or { blockNumber } can fail in the block-cache/retry-on-empty middleware before the request reaches downstream RPC. The retry path treats the block reference as a string (blockRef.slice). Both middleware orders are covered by the regression.
Versions and source
@metamask/eth-json-rpc-middleware26.0.0@metamask/network-controller37.1.1 (HTTP integration)- Node 24.22.1, Yarn 4.17.1
- Current core source checked at commit
4c3af5af945a1e6496819c90a8b59ff77e815071: the two affected source files are byte-identical to the pinned 26.0.0 source. - This is a reproducible library issue. It does not establish the exact stack inside any installed extension binary.
Request shape
{
jsonrpc: '2.0', id: 1, method: 'eth_call',
params: [
{ to: '0x' + '11'.repeat(20), data: '0x313ce567' },
{ blockHash: '0x' + 'ab'.repeat(32), requireCanonical: true }
]
}
The address and block hash are synthetic. No live chain, account, key or wallet is needed. Wire the middleware to a controlled downstream that checks the selector unchanged and returns a test value; the included native regression contains the full harness.
Expected
Object selectors reach the downstream validator/executor unchanged. Distinct hashes and canonical flags must not share a numeric/latest cache bucket. Downstream rejection must remain observable. Invalid selectors may be rejected downstream; this proposal does not make middleware a complete EIP-1898 validator.
Actual and measured regression
- Native typed regression with both guards removed: 27 expected FAIL, 0 PASS.
- Native custom/Infura-branch HTTP harness: baseline 4 PASS / 16 FAIL; candidate 20 PASS / 0 FAIL. Baseline swapped exactly two published npm 26.0.0 dist files into the candidate network-controller environment; it was not a pristine rebuild of the whole controller.
- The Infura branch uses the native URL construction, then an injected fetch adapter confines traffic to an owned loopback HTTP server. No real Infura connection or credential is used. Retries/failover are disabled in this diagnostic profile.
Proposed minimal change
- In retry-on-empty, bypass numeric block handling for non-string block selectors.
- In block-cache, do not place non-string block selectors into numeric/latest cache buckets.
The separate native regression covers object selectors across five RPC methods, both middleware orders, downstream errors and hash/canonical separation. The HTTP harness adds inflight coalescing, downstream recovery, numeric-cache and local-chainId controls.
Validation scope
Previously completed candidate checks: middleware suite 483 PASS, engine suite 260 PASS / 1 original SKIP; target semantic TypeScript, coverage thresholds, root build and root aggregate lint passed. Root TypeScript lint is a ratchet: raw root tsc still reports 1628 tracked baseline diagnostics. These suites overlap earlier runs and must not be summed.
Not established: full monorepo suite, extension IPC/TLS integration, default retry/failover behavior, installed wallet compatibility or real transaction execution. No signatures or sends were performed.
Please confirm whether this minimal selector-preservation approach belongs in core and advise on the appropriate regression/release path.
Proposed runtime diff (review proposal, not an official release)
--- a/packages/eth-json-rpc-middleware/src/block-cache.ts
+++ b/packages/eth-json-rpc-middleware/src/block-cache.ts
@@ -89,6 +89,11 @@
}
// check blockTag
const blockTag = blockTagForRequest(request);
+
+ // Object selectors must not share numeric/latest cache buckets.
+ if (blockTag !== undefined && typeof blockTag !== 'string') {
+ return false;
+ }
if (blockTag === 'pending') {
return false;
--- a/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts
+++ b/packages/eth-json-rpc-middleware/src/retryOnEmpty.ts
@@ -64,6 +64,11 @@
: undefined;
// omitted blockRef implies "latest"
blockRef ??= 'latest';
+
+ // Preserve EIP-1898 selectors for downstream validation and execution.
+ if (typeof blockRef !== 'string') {
+ return next();
+ }
// skip if non-number block reference
if (['latest', 'pending'].includes(blockRef)) {
Native typed regression patch (27 cases)
--- /dev/null
+++ b/packages/eth-json-rpc-middleware/src/eip1898-regression.test.ts
@@ -0,0 +1,159 @@
+import { PollingBlockTracker } from '@metamask/eth-block-tracker';
+import { InternalProvider } from '@metamask/eth-json-rpc-provider';
+import { JsonRpcEngineV2 } from '@metamask/json-rpc-engine/v2';
+import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine/v2';
+import type { Json, JsonRpcRequest } from '@metamask/utils';
+
+import {
+ createRetryOnEmptyMiddleware,
+ createBlockCacheMiddleware,
+} from './index.js';
+
+type Counts = { tracker: number; terminal: number };
+type Harness = {
+ counts: Counts;
+ invoke: (method: string, params: Json[]) => Promise<Json>;
+};
+
+// Dropping either guard must fail at least one regression.
+describe('EIP-1898 selector preservation', () => {
+ const address = `0x${'11'.repeat(20)}`;
+ const hashOne = `0x${'ab'.repeat(32)}`;
+ const hashTwo = `0x${'cd'.repeat(32)}`;
+ const selectors: Json[] = [
+ { blockHash: hashOne, requireCanonical: true },
+ { blockNumber: '0x123' },
+ ];
+ const owned: (() => Promise<void>)[] = [];
+
+ afterEach(async () => {
+ for (const cleanup of owned.splice(0).reverse()) {
+ await cleanup();
+ }
+ });
+
+ function pipeline(
+ order: string[],
+ terminal: (request: Readonly<JsonRpcRequest>) => Json,
+ ): Harness {
+ const counts: Counts = { tracker: 0, terminal: 0 };
+ const remote = JsonRpcEngineV2.create<
+ JsonRpcMiddleware<JsonRpcRequest, Json>
+ >({
+ middleware: [
+ async (): Promise<Json> => {
+ counts.tracker += 1;
+ throw Error('OBJECT_SELECTOR_REACHED_NUMERIC_TRACKER');
+ },
+ ],
+ });
+ const provider = new InternalProvider({ engine: remote });
+ const tracker = new PollingBlockTracker({
+ provider,
+ keepEventLoopActive: false,
+ });
+ const stages: Record<string, JsonRpcMiddleware<JsonRpcRequest, Json>> = {
+ cache: createBlockCacheMiddleware({ blockTracker: tracker }),
+ retry: createRetryOnEmptyMiddleware({ provider, blockTracker: tracker }),
+ };
+ const engine = JsonRpcEngineV2.create<
+ JsonRpcMiddleware<JsonRpcRequest, Json>
+ >({
+ middleware: [
+ ...order.map((key) => stages[key]),
+ async ({ request }): Promise<Json> => {
+ counts.terminal += 1;
+ return terminal(request);
+ },
+ ],
+ });
+ owned.push(async (): Promise<void> => {
+ await tracker.destroy();
+ await engine.destroy();
+ await remote.destroy();
+ });
+ let requestId = 0;
+ return {
+ counts,
+ invoke: (method: string, params: Json[]): Promise<Json> => {
+ requestId += 1;
+ return engine.handle({ jsonrpc: '2.0', id: requestId, method, params });
+ },
+ };
+ }
+
+ for (const order of [
+ ['cache', 'retry'],
+ ['retry', 'cache'],
+ ]) {
+ for (const method of [
+ 'eth_call',
+ 'eth_getBalance',
+ 'eth_getCode',
+ 'eth_getTransactionCount',
+ 'eth_getStorageAt',
+ ]) {
+ for (const selector of selectors) {
+ it(`${order.join('→')} forwards selector for ${method} ${JSON.stringify(selector)}`, async () => {
+ let params: Json[];
+ if (method === 'eth_call') {
+ params = [{ to: address, data: '0x313ce567' }, selector];
+ } else if (method === 'eth_getStorageAt') {
+ params = [address, '0x0', selector];
+ } else {
+ params = [address, selector];
+ }
+ const harness = pipeline(order, (request) => {
+ expect(request.params).toStrictEqual(params);
+ return '0x1234';
+ });
+ expect(await harness.invoke(method, params)).toBe('0x1234');
+ expect(harness.counts).toStrictEqual({ tracker: 0, terminal: 1 });
+ });
+ }
+ }
+ for (const reason of ['NON_CANONICAL', 'UNKNOWN_BLOCK', 'RATE_LIMIT']) {
+ it(`${order.join('→')} preserves downstream ${reason} without retry/cache`, async () => {
+ const error = Object.assign(new Error(reason), { code: -32000 });
+ let fail = true;
+ const harness = pipeline(order, () => {
+ if (fail) {
+ throw error;
+ }
+ return '0xbeef';
+ });
+ const params: Json[] = [
+ { to: address },
+ { blockHash: hashOne, requireCanonical: true },
+ ];
+ await expect(harness.invoke('eth_call', params)).rejects.toBe(error);
+ fail = false;
+ expect(await harness.invoke('eth_call', params)).toBe('0xbeef');
+ expect(harness.counts).toStrictEqual({ tracker: 0, terminal: 2 });
+ });
+ }
+ }
+
+ it('block cache keeps hash/canonical responses separate across repeated calls', async () => {
+ let index = 0;
+ const responses = ['0xaa', '0xbb', '0xcc'];
+ const harness = pipeline(['cache'], () => {
+ const result = responses[index];
+ index += 1;
+ if (result === undefined) {
+ throw Error('UNEXPECTED_EXTRA_DOWNSTREAM_REQUEST');
+ }
+ return result;
+ });
+ for (const [selector, expected] of [
+ [{ blockHash: hashOne, requireCanonical: true }, '0xaa'],
+ [{ blockHash: hashOne, requireCanonical: false }, '0xbb'],
+ [{ blockHash: hashTwo, requireCanonical: true }, '0xcc'],
+ ] as const) {
+ expect(
+ await harness.invoke('eth_call', [{ to: address }, selector]),
+ ).toBe(expected);
+ }
+ expect(harness.counts).toStrictEqual({ tracker: 0, terminal: 3 });
+ });
+});
- 主要语言
- TypeScript
- 星标
- 420
- 派生
- 313
- 平均合并
- 1 天 12 小时
- 30 天内合并 PR
- 384
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 没有贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
MetaMask/core 的其他 Issue
-
CI: "Validate changelog diffs" job fails on every fork-sourced pull request可能已有人在做 @Roxen-AS 于 11 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 84/100
维护者通常 1 天内回复
-
platform-api-docs: `scan` strategy misses ESM-only packages, which ship `.d.ts` rather than `.d.cts`可能已有人在做 @gaohan1994 于 13 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 76/100
维护者通常 1 天内回复
-
[utils] Replace the `uuid` dependency with `crypto.randomUUID()`可能已有人在做 @avantikonde 于 26 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 82/100
MetaMask/core#10210 · 1 个 reaction ·
维护者通常 1 天内回复
-
[utils] `toWei` accepts fractional wei and returns wrong values可能已有人在做 @Roxen-AS 于 11 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 88/100
维护者通常 1 天内回复
-
eth-json-rpc-middleware: `TransactionParamsStruct` rejects numeric `chainId`, breaking dapp transactions可能已有人在做 @truehazker 于 69 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 86/100
维护者通常 1 天内回复
相似的 Issue
-
难度 1/5 1 小时以内 新手友好度 85/100
formbricks/formbricks#9918 ·
维护者通常 1 天内回复
-
enhancement
难度 2/5 1-3 小时 新手友好度 65/100
thecodacus/pithagoras#364 ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 72/100
openclimatefix/quartz-frontend#823 ·
维护者通常 4 天内回复
-
area:agent-manager community needs-triage
难度 2/5 1-3 小时 新手友好度 76/100
维护者通常 1 天内回复
-
`contacts.create` posts to `/audiences/undefined/contacts` when `audienceId` is undefined可能已有人在做 @RaphaelFakhri 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 70/100
resend/resend-node#1176 ·
维护者通常 1 天内回复