From 26165a6f41a8bcdd6d4a1d71b400c5987c586128 Mon Sep 17 00:00:00 2001 From: Luiz Date: Tue, 2 Jun 2026 15:19:59 -0300 Subject: [PATCH] fix(runtime): exact-slot eth_getStorageAt, eth_getCode fallback for create2, and receipt-poll retry --- src/runtime/cheatcodes.js | 1 + src/runtime/ethers-bridge.js | 51 +++++++++++++++++++++++++----------- src/runtime/wait.js | 31 +++++++++++++++++----- 3 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/runtime/cheatcodes.js b/src/runtime/cheatcodes.js index f0374a8..a497034 100644 --- a/src/runtime/cheatcodes.js +++ b/src/runtime/cheatcodes.js @@ -497,6 +497,7 @@ async function maybeAutoRegisterContract(tronWeb, toBase58, triggerJson) { module.exports = { rpcCall, + isTransientFetchError, callOrFalse, mine, setBlockTime, diff --git a/src/runtime/ethers-bridge.js b/src/runtime/ethers-bridge.js index e66df7a..e6cf418 100644 --- a/src/runtime/ethers-bridge.js +++ b/src/runtime/ethers-bridge.js @@ -126,10 +126,27 @@ const stubProvider = { const addr = remapped || signersMod.toBase58(address); try { const info = await tw.trx.getContract(addr); - return info && info.bytecode ? '0x' + info.bytecode : '0x'; + if (info && info.bytecode) return '0x' + info.bytecode; } catch { - return '0x'; + /* fall through to eth_getCode */ } + // `getContract` reads ContractStore, which misses CREATE2-opcode + // deployments (e.g. RelayedCall's assembly-`create2` relayer) — those + // live in CodeStore. Fall back to `eth_getCode` on `/jsonrpc`, which + // reads CodeStore. Same endpoint maybeAutoRegisterContract uses. + try { + const base = tw.fullNode.host.replace(/\/$/, ''); + const hexAddr = '0x' + TronWeb.address.toHex(addr).slice(2); + const res = await fetch(base + '/jsonrpc', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_getCode', params: [hexAddr, 'latest'], id: 1 }), + }).then((r) => r.json()); + if (res && typeof res.result === 'string' && res.result !== '0x' && res.result !== '0x0') return res.result; + } catch { + /* */ + } + return '0x'; }, async getBalance(address) { const tw = this._tw(); @@ -217,25 +234,27 @@ const stubProvider = { async getStorage(address, slot) { const tw = this._tw(); if (!tw) return '0x' + '0'.repeat(64); - const addr = signersMod.toBase58(address); - const slotHex = typeof slot === 'bigint' ? ethersV6.toBeHex(slot, 32) : slot; - const rpc = tw.fullNode.host.replace(/\/$/, '') + '/tre'; - const res = await fetch(rpc, { + // Read the EXACT slot via `eth_getStorageAt` on `/jsonrpc`. The prior + // `/tre debug_storageRangeAt` approach returned `Object.values(storage)[0]` + // — the FIRST slot the node yields, NOT the requested one (java-tron's + // storageRangeAt does not filter to the key) — so ERC-1967 slot reads + // (proxy implementation/admin) came back as slot 0 (the impl's `value`). + // `eth_getStorageAt` is exact. Also apply the EVM-pred → TVM-actual remap + // (matches getCode/getBalance) for predicted-address reads (clones, etc.). + const remapped = lookupTvmActualBase58(address); + const addr = remapped || signersMod.toBase58(address); + const slotHex = ethersV6.toBeHex(slot, 32); + const base = tw.fullNode.host.replace(/\/$/, ''); + const hexAddr = '0x' + TronWeb.address.toHex(addr).slice(2); + const res = await fetch(base + '/jsonrpc', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: 1, - method: 'debug_storageRangeAt', - params: [0, 0, addr, slotHex, 1], - }), + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getStorageAt', params: [hexAddr, slotHex, 'latest'] }), }) .then((r) => r.json()) .catch(() => null); - const storage = res && res.result && res.result.storage; - if (storage) { - const entry = Object.values(storage)[0]; - if (entry && entry.value) return '0x' + entry.value; + if (res && typeof res.result === 'string' && res.result !== '0x') { + return ethersV6.zeroPadValue(res.result, 32); } return '0x' + '0'.repeat(64); }, diff --git a/src/runtime/wait.js b/src/runtime/wait.js index 8aa360c..4eaa8cc 100644 --- a/src/runtime/wait.js +++ b/src/runtime/wait.js @@ -6,7 +6,7 @@ // `getUnconfirmedTransactionInfo` first and only fall back to the // solidified view as a hedge for public networks. -const { mine } = require('./cheatcodes'); +const { mine, isTransientFetchError } = require('./cheatcodes'); // JSON parser that preserves integer precision above 2^53. TVM's // transaction-info response embeds `callValueInfo[*].callValue` (and @@ -46,12 +46,29 @@ async function _getInfoBigSafe(tronWeb, txId, endpoint) { // TronWeb's JSON parsing (lossy on large integers). const base = tronWeb.fullNode.host.replace(/\/$/, ''); const url = base + endpoint; - const resp = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ value: txId }), - }); - const text = await resp.text(); + const fetchText = async () => { + const resp = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ value: txId }), + }); + return resp.text(); + }; + // Single retry on a connection-level error ("fetch failed", "other side + // closed", ECONNRESET, …) on EITHER the request or the body read. java-tron + // under G1GC with -Xmx2g can stop-the-world for 50–200 ms late in long + // parallel runs; a pause exceeding the HTTP keep-alive timeout drops the + // socket and surfaces here. Without a retry, one dropped poll fails the test + // — and because every later fixture's waitForReceipt hits the same node, it + // cascades the rest of the worker. Mirrors rpcCall's retry (cheatcodes.js). + let text; + try { + text = await fetchText(); + } catch (e) { + if (!isTransientFetchError(e)) throw e; + await new Promise((r) => setTimeout(r, 100)); + text = await fetchText(); + } if (!text) return null; return jsonParseBigSafe(text); }