diff --git a/ts-tests/tests/test-contract-storage.ts b/ts-tests/tests/test-contract-storage.ts index e6c49380a4..8119e3d467 100644 --- a/ts-tests/tests/test-contract-storage.ts +++ b/ts-tests/tests/test-contract-storage.ts @@ -3,7 +3,7 @@ import { AbiItem } from "web3-utils"; import Test from "../build/contracts/Storage.json"; import { GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY, FIRST_CONTRACT_ADDRESS } from "./config"; -import { createAndFinalizeBlock, customRequest, describeWithFrontier } from "./util"; +import { createAndFinalizeBlock, customRequest, describeWithFrontier, waitForReceipt } from "./util"; describeWithFrontier("Frontier RPC (Contract)", (context) => { const TEST_CONTRACT_BYTECODE = Test.bytecode; @@ -81,34 +81,37 @@ describeWithFrontier("Frontier RPC (Contract)", (context) => { }); it("SSTORE cost should properly take into account transaction initial value", async function () { - let nonce = await context.web3.eth.getTransactionCount(GENESIS_ACCOUNT); + this.timeout(30000); - await context.web3.eth.accounts.wallet.add(GENESIS_ACCOUNT_PRIVATE_KEY); + let nonce = await context.web3.eth.getTransactionCount(GENESIS_ACCOUNT); const contract = new context.web3.eth.Contract(TEST_CONTRACT_ABI, FIRST_CONTRACT_ADDRESS, { from: GENESIS_ACCOUNT, gasPrice: "0x3B9ACA00", }); - const promisify = (inner) => new Promise((resolve, reject) => inner(resolve, reject)); - - let tx1 = contract.methods - .setStorage("0x2A", "0x1") - .send({ from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ }); + const sendSetStorageTx = async (value: string, txNonce: number) => { + const tx = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + to: FIRST_CONTRACT_ADDRESS, + data: contract.methods.setStorage("0x2A", value).encodeABI(), + value: "0x00", + gasPrice: "0x3B9ACA00", + gas: "0x100000", + nonce: txNonce, + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); - let tx2 = contract.methods - .setStorage("0x2A", "0x1") - .send({ from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ }); + const txHash = (await customRequest(context.web3, "eth_sendRawTransaction", [tx.rawTransaction])).result; + await createAndFinalizeBlock(context.web3); - let tx3 = contract.methods - .setStorage("0x2A", "0x2") - .send( - { from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ }, - async (hash) => await createAndFinalizeBlock(context.web3) - ); + return waitForReceipt(context.web3, txHash); + }; - tx1 = await tx1; - tx2 = await tx2; - tx3 = await tx3; + const tx1 = await sendSetStorageTx("0x1", nonce++); + const tx2 = await sendSetStorageTx("0x1", nonce++); + const tx3 = await sendSetStorageTx("0x2", nonce++); // cost minus SSTORE const baseCost = 24029; diff --git a/ts-tests/tests/test-receipt-consistency.ts b/ts-tests/tests/test-receipt-consistency.ts index 24c8b96aad..01cbdfe009 100644 --- a/ts-tests/tests/test-receipt-consistency.ts +++ b/ts-tests/tests/test-receipt-consistency.ts @@ -2,7 +2,13 @@ import { expect } from "chai"; import { step } from "mocha-steps"; import { GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY } from "./config"; -import { createAndFinalizeBlockNowait, describeWithFrontier, customRequest, waitForBlock } from "./util"; +import { + createAndFinalizeBlockNowait, + describeWithFrontier, + customRequest, + waitForBlock, + waitForReceipt, +} from "./util"; /** * Test for receipt consistency (ADR-003). @@ -28,19 +34,6 @@ describeWithFrontier("Frontier RPC (Receipt Consistency)", (context) => { throw new Error(`Timed out waiting for txpool pending >= ${minPending}`); } - async function waitForReceipt(txHash: string, timeoutMs = 10000) { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - const receipt = await context.web3.eth.getTransactionReceipt(txHash); - if (receipt !== null) { - return receipt; - } - await createAndFinalizeBlockNowait(context.web3); - await new Promise((resolve) => setTimeout(resolve, 50)); - } - throw new Error(`Timed out waiting for receipt ${txHash}`); - } - step("should return receipt immediately after block is visible", async function () { const tx = await context.web3.eth.accounts.signTransaction( { @@ -118,7 +111,7 @@ describeWithFrontier("Frontier RPC (Receipt Consistency)", (context) => { // All receipts should eventually be available and point to visible blocks. for (let i = 0; i < txCount; i++) { - const receipt = await waitForReceipt(txHashes[i]); + const receipt = await waitForReceipt(context.web3, txHashes[i]); expect(receipt, `Receipt for tx ${i}`).to.not.be.null; expect(receipt.transactionHash).to.equal(txHashes[i]); diff --git a/ts-tests/tests/test-selfdestruct.ts b/ts-tests/tests/test-selfdestruct.ts index 9d7863b762..e31fe8947f 100644 --- a/ts-tests/tests/test-selfdestruct.ts +++ b/ts-tests/tests/test-selfdestruct.ts @@ -4,7 +4,7 @@ import { AbiItem } from "web3-utils"; import SelfDestructAfterCreate2 from "../build/contracts/SelfDestructAfterCreate2.json"; import { GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY, FIRST_CONTRACT_ADDRESS } from "./config"; -import { createAndFinalizeBlock, customRequest, describeWithFrontier } from "./util"; +import { createAndFinalizeBlock, customRequest, describeWithFrontier, waitForReceipt } from "./util"; chaiUse(chaiAsPromised); @@ -50,32 +50,41 @@ describeWithFrontier("Test self-destruct contract", (context) => { result: TEST_CONTRACT_DEPLOYED_BYTECODE, }); - // Prepare signer and fetch latest nonce - await context.web3.eth.accounts.wallet.add(GENESIS_ACCOUNT_PRIVATE_KEY); let nonce = await context.web3.eth.getTransactionCount(GENESIS_ACCOUNT); - const contract = new context.web3.eth.Contract(TEST_CONTRACT_ABI, FIRST_CONTRACT_ADDRESS, { from: GENESIS_ACCOUNT, gasPrice: "0x3B9ACA00", }); - let tx1 = contract.methods.step1().send({ from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ }); + const sendTx = async (data: string, txNonce: number) => { + const signed = await context.web3.eth.accounts.signTransaction( + { + from: GENESIS_ACCOUNT, + to: FIRST_CONTRACT_ADDRESS, + data, + value: "0x00", + gasPrice: "0x3B9ACA00", + gas: "0x100000", + nonce: txNonce, + }, + GENESIS_ACCOUNT_PRIVATE_KEY + ); + const txHash = (await customRequest(context.web3, "eth_sendRawTransaction", [signed.rawTransaction])) + .result as string; + return txHash; + }; - let tx2 = contract.methods.step2().send({ from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ }); + const tx1Hash = await sendTx(contract.methods.step1().encodeABI(), nonce++); + const tx2Hash = await sendTx(contract.methods.step2().encodeABI(), nonce++); + const tx3Hash = await sendTx(contract.methods.cannotRecreateInTheSameCall().encodeABI(), nonce++); - let tx3 = contract.methods - .cannotRecreateInTheSameCall() - .send( - { from: GENESIS_ACCOUNT, gas: "0x100000", nonce: nonce++ }, - async (_hash) => await createAndFinalizeBlock(context.web3) - ); + await createAndFinalizeBlock(context.web3); - const { transactionHash: tx1Hash } = await tx1; - const { transactionHash: tx2Hash } = await tx2; - const { transactionHash: tx3Hash } = await tx3; + const tx1Receipt = await waitForReceipt(context.web3, tx1Hash); + const tx2Receipt = await waitForReceipt(context.web3, tx2Hash); + const tx3Receipt = await waitForReceipt(context.web3, tx3Hash); - for (let txHash of [tx1Hash, tx2Hash, tx3Hash]) { - const receipt = await context.web3.eth.getTransactionReceipt(txHash); + for (const receipt of [tx1Receipt, tx2Receipt, tx3Receipt]) { expect(receipt.status).to.be.true; } diff --git a/ts-tests/tests/test-transaction-version.ts b/ts-tests/tests/test-transaction-version.ts index 097c04a6cb..799dcd9fc7 100644 --- a/ts-tests/tests/test-transaction-version.ts +++ b/ts-tests/tests/test-transaction-version.ts @@ -3,7 +3,7 @@ import { expect } from "chai"; import { step } from "mocha-steps"; import { GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY, CHAIN_ID } from "./config"; -import { createAndFinalizeBlock, describeWithFrontier } from "./util"; +import { createAndFinalizeBlock, describeWithFrontier, waitForReceipt } from "./util"; // We use ethers library in this test as apparently web3js's types are not fully EIP-1559 compliant yet. describeWithFrontier("Frontier RPC (Transaction Version)", (context) => { @@ -30,18 +30,6 @@ describeWithFrontier("Frontier RPC (Transaction Version)", (context) => { throw new Error(`Timed out waiting for transaction ${txHash} to reach the pool`); } - async function waitForReceipt(txHash: string, timeoutMs = 5000) { - const start = Date.now(); - while (Date.now() - start < timeoutMs) { - const receipt = await context.web3.eth.getTransactionReceipt(txHash); - if (receipt !== null) { - return receipt; - } - await new Promise((resolve) => setTimeout(resolve, 50)); - } - throw new Error(`Timed out waiting for receipt ${txHash}`); - } - step("should handle Legacy transaction type 0", async function () { let tx = { from: GENESIS_ACCOUNT, @@ -56,7 +44,7 @@ describeWithFrontier("Frontier RPC (Transaction Version)", (context) => { const txHash = (await sendTransaction(context, tx)).hash; await waitForTransactionSeen(txHash); await createAndFinalizeBlock(context.web3); - let receipt = await waitForReceipt(txHash); + let receipt = await waitForReceipt(context.web3, txHash); const minedBlock = await context.web3.eth.getBlock(receipt.blockNumber); expect(minedBlock.transactions).to.include(txHash); expect(receipt.transactionHash).to.be.eq(txHash); @@ -82,7 +70,7 @@ describeWithFrontier("Frontier RPC (Transaction Version)", (context) => { const txHash = (await sendTransaction(context, tx)).hash; await waitForTransactionSeen(txHash); await createAndFinalizeBlock(context.web3); - let receipt = await waitForReceipt(txHash); + let receipt = await waitForReceipt(context.web3, txHash); const minedBlock = await context.web3.eth.getBlock(receipt.blockNumber); expect(minedBlock.transactions).to.include(txHash); expect(receipt.transactionHash).to.be.eq(txHash); @@ -109,7 +97,7 @@ describeWithFrontier("Frontier RPC (Transaction Version)", (context) => { const txHash = (await sendTransaction(context, tx)).hash; await waitForTransactionSeen(txHash); await createAndFinalizeBlock(context.web3); - let receipt = await waitForReceipt(txHash); + let receipt = await waitForReceipt(context.web3, txHash); const minedBlock = await context.web3.eth.getBlock(receipt.blockNumber); expect(minedBlock.transactions).to.include(txHash); expect(receipt.transactionHash).to.be.eq(txHash); diff --git a/ts-tests/tests/util.ts b/ts-tests/tests/util.ts index 143bbe81d0..3fd63e0168 100644 --- a/ts-tests/tests/util.ts +++ b/ts-tests/tests/util.ts @@ -65,22 +65,66 @@ export async function waitForBlock( throw new Error(`Timeout waiting for block ${blockTag} to be indexed${errorSuffix}`); } -// Create a block, finalize it, and wait for it to be indexed by mapping-sync. +// Create a block, optionally finalize it, and wait for it to be indexed by mapping-sync. // This ensures the block is visible via eth_getBlockByNumber before returning. +// When finalize is false the block is still imported (best block); we wait for it to be +// visible as "latest". The node exposes the best chain to eth RPC, so this works for both. export async function createAndFinalizeBlock(web3: Web3, finalize: boolean = true) { const response = await customRequest(web3, "engine_createBlock", [true, finalize, null]); - if (!response.result) { + if (!response?.result?.hash) { throw new Error(`Unexpected result: ${JSON.stringify(response)}`); } + const blockHash = response.result.hash as string; - // Use chain head as source of truth for the new block height, then wait until - // mapping-sync exposes that exact block via eth RPC. - const head = (await customRequest(web3, "chain_getHeader", [])).result; - if (!head?.number) { - throw new Error(`Unexpected chain head response: ${JSON.stringify(head)}`); + // Get the block number from the created block's header. Poll until the header is visible + // (import can lag) and retry on transient RPC errors. + const headerTimeout = 10_000; + const headerStart = Date.now(); + let header: { number?: string } | null = null; + let headerLastError: Error | null = null; + while (Date.now() - headerStart < headerTimeout) { + try { + const headerResp = await customRequest(web3, "chain_getHeader", [blockHash]); + const h = headerResp.result as { number?: string } | null; + if (h?.number != null) { + header = h; + break; + } + } catch (error) { + headerLastError = error instanceof Error ? error : new Error(String(error)); + } + await new Promise((r) => setTimeout(r, 50)); } + if (!header?.number) { + const errSuffix = headerLastError ? ` (last error: ${headerLastError.message})` : ""; + throw new Error(`chain_getHeader(${blockHash}) returned no header for created block${errSuffix}`); + } + const expectedNumber = parseInt(header.number, 16); + const expectedBlockTag = "0x" + expectedNumber.toString(16); + + await waitForBlock(web3, expectedBlockTag, 10_000); - await waitForBlock(web3, head.number, 5000); + // Also wait for eth_blockNumber / "latest" to be at least the new block, so tests that + // assert on getBlockNumber() or use "latest" see the block we just created. + // Use >= so we don't timeout if the node advances past expectedNumber between polls. + // Retry on transient RPC errors instead of failing fast. + const rpcSyncTimeout = 10_000; + const rpcStart = Date.now(); + let rpcLastError: Error | null = null; + while (Date.now() - rpcStart < rpcSyncTimeout) { + try { + const current = await customRequest(web3, "eth_blockNumber", []); + const n = current.result != null ? parseInt(current.result, 16) : -1; + if (n >= expectedNumber) { + return; + } + } catch (error) { + rpcLastError = error instanceof Error ? error : new Error(String(error)); + } + await new Promise((r) => setTimeout(r, 50)); + } + const rpcErrorSuffix = rpcLastError ? ` (last error: ${rpcLastError.message})` : ""; + throw new Error(`eth_blockNumber did not reach ${expectedNumber} after ${rpcSyncTimeout}ms${rpcErrorSuffix}`); } // Create a block and finalize it without waiting for indexing. @@ -92,6 +136,19 @@ export async function createAndFinalizeBlockNowait(web3: Web3) { } } +// Wait for a receipt to be available for a given transaction hash. +export async function waitForReceipt(web3: Web3, txHash: string, timeoutMs = 10000) { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const receipt = await web3.eth.getTransactionReceipt(txHash); + if (receipt !== null) { + return receipt; + } + await new Promise((resolve) => setTimeout(resolve, 50)); + } + throw new Error(`Timed out waiting for receipt ${txHash}`); +} + export async function startFrontierNode( provider?: string, additionalArgs: string[] = []