EIP-1898 object selectors fail before downstream RPC in eth-json-rpc-middleware 26.0.0
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 62/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- typescript
- Área
- api, blockchain
Línea de trabajo
El error está en packages/eth-json-rpc-middleware/src/retryOnEmpty.ts, donde una referencia de bloque que no es una cadena se trata como un número, y en src/block-cache.ts, donde estos selectores pueden acabar en los buckets de caché numéricos o latest. Empieza leyendo ambos archivos alrededor de la gestión de referencias de bloque y luego ejecuta el src/eip1898-regression.test.ts propuesto. Está terminado cuando los casos con selectores de objeto pasan en ambos órdenes de middleware y la suite de pruebas existente del middleware sigue en verde.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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 });
+ });
+});
- Lenguaje dominante
- TypeScript
- Estrellas
- 422
- Forks
- 313
- Merge medio
- 1 d 12 h
- PR fusionados (30 d)
- 384
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Sin guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de MetaMask/core
-
CI: "Validate changelog diffs" job fails on every fork-sourced pull requestPosiblemente ocupada @Roxen-AS la tomó hace 13 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
Los mantenedores suelen responder en 1 día
-
platform-api-docs: `scan` strategy misses ESM-only packages, which ship `.d.ts` rather than `.d.cts`Posiblemente ocupada @gaohan1994 la tomó hace 15 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
[utils] Replace the `uuid` dependency with `crypto.randomUUID()`Posiblemente ocupada @avantikonde la tomó hace 27 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
MetaMask/core#10210 · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
[utils] `toWei` accepts fractional wei and returns wrong valuesPosiblemente ocupada @Roxen-AS la tomó hace 13 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
Los mantenedores suelen responder en 1 día
-
eth-json-rpc-middleware: `TransactionParamsStruct` rejects numeric `chainId`, breaking dapp transactionsPosiblemente ocupada @truehazker la tomó hace 71 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
MetaMask/core#9964 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de MetaMask/core
Issues similares
-
Report: &TV InternationalAbiertocheck:passed streams:remove
Dificultad 1/5 Menos de una hora Aptitud para principiantes 67/100
Los mantenedores suelen responder en 1 día
-
bug priority-p1
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
-
beginner blocked
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
hiero-ledger/hiero-sdk-js#4670 ·
Los mantenedores suelen responder en 1 día
-
[Exploratory] Expense - 2 selected drop-down is shown twice when selecting hold from more option.AbiertoMonthly Not a priority
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
Expensify/App#106080 · 1 comentario ·
Los mantenedores suelen responder en 1 día