From e02c536a0b3f54fceb69d7d84f16998552056305 Mon Sep 17 00:00:00 2001 From: Andres Adjimann <adjisb@sandbox.game> Date: Wed, 21 Sep 2022 11:42:30 -0300 Subject: [PATCH] test: add tests and some fixes --- .../src/data-managers/block-manager.ts | 3 + .../src/data-managers/blocklog-manager.ts | 52 +-- .../ethereum/tests/forking/contracts/Logs.sol | 16 + .../ethereum/tests/forking/logs.test.ts | 314 ++++++++++++++++++ 4 files changed, 366 insertions(+), 19 deletions(-) create mode 100644 src/chains/ethereum/ethereum/tests/forking/contracts/Logs.sol create mode 100644 src/chains/ethereum/ethereum/tests/forking/logs.test.ts diff --git a/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts b/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts index 26b923b7d7..1864b3e5b8 100644 --- a/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts +++ b/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts @@ -173,6 +173,8 @@ export default class BlockManager extends Manager<Block> { return Quantity.from(tagOrBlockNumber); } + // TODO: This won't work when we have a fallback/fork and it is + // TODO: called from api.eth_getBlockTransactionCountByHash async getNumberFromHash(hash: string | Buffer | Tag) { return this.#blockIndexes.get(Data.toBuffer(hash)).catch(e => { if (e.status === NOTFOUND) return null; @@ -295,6 +297,7 @@ export default class BlockManager extends Manager<Block> { this.#blockIndexes.get(LATEST_INDEX_KEY).catch(e => null) ]); + // TODO: this must take into account fallback/fork. if (earliest) this.earliest = earliest; if (latestBlockNumber) { diff --git a/src/chains/ethereum/ethereum/src/data-managers/blocklog-manager.ts b/src/chains/ethereum/ethereum/src/data-managers/blocklog-manager.ts index a52a79d85d..8e9751e576 100644 --- a/src/chains/ethereum/ethereum/src/data-managers/blocklog-manager.ts +++ b/src/chains/ethereum/ethereum/src/data-managers/blocklog-manager.ts @@ -1,13 +1,14 @@ -import { BlockLogs, FilterArgs } from "@ganache/ethereum-utils"; +import { BlockLogs, FilterArgs, RangeFilterArgs, Tag } from "@ganache/ethereum-utils"; import { LevelUp } from "levelup"; import Manager from "./manager"; import { Quantity } from "@ganache/utils"; import Blockchain from "../blockchain"; -import { parseFilter, parseFilterDetails } from "../helpers/filter-parsing"; +import { parseFilter, parseFilterDetails, parseFilterRange } from "../helpers/filter-parsing"; import { Ethereum } from "../api-types"; export default class BlockLogManager extends Manager<BlockLogs> { #blockchain: Blockchain; + constructor(base: LevelUp, blockchain: Blockchain) { super(base, BlockLogs); this.#blockchain = blockchain; @@ -31,38 +32,51 @@ export default class BlockLogManager extends Manager<BlockLogs> { async getLogs(filter: FilterArgs): Promise<Ethereum.Logs> { const blockchain = this.#blockchain; if ("blockHash" in filter) { + // TODO: revert back to getNumberFromHash (when they add support for fallback/forks) + const block = await blockchain.blocks.getByHash(filter.blockHash); + if (!block) return []; + const blockNumber = block.header.number; + const logs = await this.get(blockNumber.toBuffer()); const { addresses, topics } = parseFilterDetails(filter); - const blockNumber = await blockchain.blocks.getNumberFromHash( - filter.blockHash - ); - if (!blockNumber) return []; - - const logs = await this.get(blockNumber); return logs ? [...logs.filter(addresses, topics)] : []; } - const { addresses, topics, fromBlock, toBlock } = parseFilter( - filter, - blockchain - ); - const from = Quantity.min(fromBlock, toBlock); + const rangeFilter = filter as RangeFilterArgs; + + let { fromBlock, toBlock } = parseFilterRange(rangeFilter, blockchain); const fork = this.#blockchain.fallback; if (!fork) { - return await this.getLocal(from.toNumber(), toBlock.toNumber(), filter); + return await this.getLocal(fromBlock, toBlock, filter); + } + if (rangeFilter.fromBlock == Tag.earliest) { + fromBlock = await this.forkEarliest(); } + if (rangeFilter.toBlock == Tag.earliest) { + toBlock = await this.forkEarliest(); + } + fromBlock = Quantity.min(fromBlock, toBlock); + const ret: Ethereum.Logs = []; - if (fork.isValidForkBlockNumber(from)) { - ret.push(...await this.getFromFork(from, Quantity.min(toBlock, fork.blockNumber), filter)); + if (fork.isValidForkBlockNumber(fromBlock)) { + ret.push(...await this.getFromFork(fromBlock, Quantity.min(toBlock, fork.blockNumber), filter)); } if (!fork.isValidForkBlockNumber(toBlock)) { - ret.push(...await this.getLocal(fork.blockNumber.toNumber() + 1, toBlock.toNumber(), filter)); + const blockNumberPlusOne = Quantity.from(fork.blockNumber.toNumber() + 1); + ret.push(...await this.getLocal(Quantity.max(fromBlock, blockNumberPlusOne), toBlock, filter)); } return ret; } - getLocal(from: number, toBlockNumber: number, filter: FilterArgs): Promise<Ethereum.Logs> { + // TODO: Use block-manager earliest when fixed (currently it doesn't support fallback/fork correctly) + async forkEarliest() { + const fork = this.#blockchain.fallback; + const block = await fork.request<any>("eth_getBlockByNumber", [Tag.earliest, false], { disableCache: true }); + return Quantity.from(block.number); + } + + getLocal(from: Quantity, to: Quantity, filter: FilterArgs): Promise<Ethereum.Logs> { const { addresses, topics } = parseFilter(filter, this.#blockchain); const pendingLogsPromises: Promise<BlockLogs>[] = []; - for (let i = from; i <= toBlockNumber; i++) { + for (let i = from.toNumber(); i <= to.toNumber(); i++) { pendingLogsPromises.push(this.get(Quantity.toBuffer(i))); } return Promise.all(pendingLogsPromises).then(blockLogsRange => { diff --git a/src/chains/ethereum/ethereum/tests/forking/contracts/Logs.sol b/src/chains/ethereum/ethereum/tests/forking/contracts/Logs.sol new file mode 100644 index 0000000000..cfee8ee63e --- /dev/null +++ b/src/chains/ethereum/ethereum/tests/forking/contracts/Logs.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.11; + +contract Logs { + event Event(uint256 indexed first, uint256 indexed second); + + constructor() { + emit Event(1, 2); + } + + function logNTimes(uint8 n) public { + for (uint8 i = 0; i < n; i++) { + emit Event(i, i); + } + } +} diff --git a/src/chains/ethereum/ethereum/tests/forking/logs.test.ts b/src/chains/ethereum/ethereum/tests/forking/logs.test.ts new file mode 100644 index 0000000000..07ff23e3f5 --- /dev/null +++ b/src/chains/ethereum/ethereum/tests/forking/logs.test.ts @@ -0,0 +1,314 @@ +import assert from "assert"; +import { EthereumProvider } from "../../src/provider"; +import getProvider, { mnemonic } from "../helpers/getProvider"; +import compile from "../helpers/compile"; +import { join } from "path"; +import { KnownKeys } from "@ganache/utils"; +import EthereumApi from "../../src/api"; +import { Tag } from "@ganache/ethereum-utils"; + +describe("forking", () => { + describe("logs", () => { + let provider: EthereumProvider; + let contract: ReturnType<typeof compile>; + let contractAddress: string; + let creationReceipt; + let accounts: string[]; + + const deployContractAndGetAddress = async (bottomProvider: EthereumProvider) => { + const subscriptionId = await bottomProvider.send("eth_subscribe", [ + "newHeads" + ]); + const transactionHash = await bottomProvider.send("eth_sendTransaction", [ + { + from: accounts[0], + data: contract.code, + gas: "0x2fefd8" + } + ]); + await bottomProvider.once("message"); + creationReceipt = await bottomProvider.send( + "eth_getTransactionReceipt", + [transactionHash] + ); + await bottomProvider.send("eth_unsubscribe", [subscriptionId]); + return creationReceipt.contractAddress; + }; + + beforeEach(async () => { + contract = compile(join(__dirname, "./contracts/Logs.sol")); + const bottomProvider = await getProvider(); + accounts = await bottomProvider.send("eth_accounts"); + contractAddress = await deployContractAndGetAddress(bottomProvider); + provider = await getProvider({ + wallet: { + mnemonic: mnemonic + }, + fork: { + provider: { + request: (args: { + readonly method: string; + readonly params?: readonly unknown[] | object; + }) => bottomProvider.request({ + method: args.method as KnownKeys<EthereumApi>, + params: args.params as any + }) + } + } + }); + }); + + describe("getLogs", () => { + it("should return a log for the constructor transaction", async () => { + const logs = await provider.send("eth_getLogs", [ + { address: contractAddress, fromBlock: Tag.earliest } + ]); + assert.strictEqual(logs.length, 1); + }); + + it("should return the all the logs when called from earlier", async () => { + await provider.send("eth_subscribe", ["newHeads"]); + const numberOfLogs = 4; + const data = + "0x" + + contract.contract.evm.methodIdentifiers["logNTimes(uint8)"] + + numberOfLogs.toString().padStart(64, "0"); + const txHash = await provider.send("eth_sendTransaction", [ + { + from: accounts[0], + to: contractAddress, + gas: "0x2fefd8", + data: data + } + ]); + await provider.once("message"); + const txReceipt = await provider.send("eth_getTransactionReceipt", [ + txHash + ]); + assert.deepStrictEqual(txReceipt.logs.length, numberOfLogs); + const logs = await provider.send("eth_getLogs", [ + { address: contractAddress, fromBlock: "earliest" } + ]); + assert.deepStrictEqual(logs.length, numberOfLogs + 1); + assert.deepStrictEqual(logs.slice(0, 1), creationReceipt.logs); + assert.deepStrictEqual(logs.slice(1, logs.length), txReceipt.logs); + }); + + it("should return the logs for the lastblock call", async () => { + await provider.send("eth_subscribe", ["newHeads"]); + const numberOfLogs = 4; + const data = + "0x" + + contract.contract.evm.methodIdentifiers["logNTimes(uint8)"] + + numberOfLogs.toString().padStart(64, "0"); + const txHash = await provider.send("eth_sendTransaction", [ + { + from: accounts[0], + to: contractAddress, + gas: "0x2fefd8", + data: data + } + ]); + await provider.once("message"); + const txReceipt = await provider.send("eth_getTransactionReceipt", [ + txHash + ]); + assert.deepStrictEqual(txReceipt.logs.length, numberOfLogs); + const logs = await provider.send("eth_getLogs", [ + { address: contractAddress } + ]); + assert.deepStrictEqual(logs, txReceipt.logs); + }); + + it("should filter out other blocks when using `latest`", async () => { + await provider.send("eth_subscribe", ["newHeads"]); + const numberOfLogs = 4; + const data = + "0x" + + contract.contract.evm.methodIdentifiers["logNTimes(uint8)"] + + numberOfLogs.toString().padStart(64, "0"); + await provider.send("eth_sendTransaction", [ + { + from: accounts[0], + to: contractAddress, + gas: "0x2fefd8", + data: data + } + ]); + await provider.once("message"); + await provider.send("evm_mine"); + await provider.once("message"); + const logs = await provider.send("eth_getLogs", [ + { address: contractAddress, toBlock: "latest", fromBlock: "latest" } + ]); + assert.strictEqual(logs.length, 0); + }); + + it("should filter appropriately when using fromBlock and toBlock", async () => { + const genesisBlockNumber = "0x0"; + const deployBlockNumber = "0x1"; + const emptyBlockNumber = "0x2"; + await provider.send("evm_mine"); // 0x2 + + await provider.send("eth_subscribe", ["newHeads"]); + const numberOfLogs = 4; + const data = + "0x" + + contract.contract.evm.methodIdentifiers["logNTimes(uint8)"] + + numberOfLogs.toString().padStart(64, "0"); + const txHash = await provider.send("eth_sendTransaction", [ + { + from: accounts[0], + to: contractAddress, + gas: "0x2fefd8", + data: data + } + ]); // 0x3 + await provider.once("message"); + const { blockNumber } = await provider.send( + "eth_getTransactionReceipt", + [txHash] + ); + + async function testGetLogs( + fromBlock: string, + toBlock: string, + expected: number, + address: string = contractAddress + ) { + const logs = await provider.send("eth_getLogs", [ + { address, fromBlock, toBlock } + ]); + assert.strictEqual( + logs.length, + expected, + `there should be ${expected} log(s) between the ${fromBlock} block and the ${toBlock} block` + ); + } + + // tests ranges up to latest/blockNumber + await testGetLogs("earliest", "earliest", 0); + await testGetLogs(genesisBlockNumber, genesisBlockNumber, 0); + await testGetLogs("earliest", emptyBlockNumber, 1); + await testGetLogs(genesisBlockNumber, emptyBlockNumber, 1); + await testGetLogs("earliest", "latest", numberOfLogs + 1); + await testGetLogs("earliest", blockNumber, numberOfLogs + 1); + await testGetLogs(genesisBlockNumber, "latest", numberOfLogs + 1); + await testGetLogs(genesisBlockNumber, blockNumber, numberOfLogs + 1); + await testGetLogs(deployBlockNumber, "latest", numberOfLogs + 1); + await testGetLogs(deployBlockNumber, blockNumber, numberOfLogs + 1); + await testGetLogs(emptyBlockNumber, "latest", numberOfLogs); + await testGetLogs(emptyBlockNumber, blockNumber, numberOfLogs); + + // tests variations where latest === blockNumber + await testGetLogs(blockNumber, blockNumber, numberOfLogs); + await testGetLogs(blockNumber, "latest", numberOfLogs); + await testGetLogs("latest", blockNumber, numberOfLogs); + await testGetLogs("latest", "latest", numberOfLogs); + + // mine an extra block + await provider.send("evm_mine"); // 0x3 + const lastBlockNumber = `0x${(parseInt(blockNumber) + 1).toString( + 16 + )}`; + await provider.once("message"); + + // test variations of `earliest` and `0x0` + await testGetLogs(genesisBlockNumber, genesisBlockNumber, 0); + await testGetLogs("earliest", "earliest", 0); + await testGetLogs("earliest", genesisBlockNumber, 0); + await testGetLogs(genesisBlockNumber, "earliest", 0); + + // test misc ranges not already tested + await testGetLogs(genesisBlockNumber, deployBlockNumber, 1); + await testGetLogs("earliest", deployBlockNumber, 1); + await testGetLogs("earliest", "latest", numberOfLogs + 1); + await testGetLogs(genesisBlockNumber, "latest", numberOfLogs + 1); + await testGetLogs(deployBlockNumber, "latest", numberOfLogs + 1); + // test variations involving the last block number + await testGetLogs( + genesisBlockNumber, + lastBlockNumber, + numberOfLogs + 1 + ); + await testGetLogs( + deployBlockNumber, + lastBlockNumber, + numberOfLogs + 1 + ); + await testGetLogs(emptyBlockNumber, lastBlockNumber, numberOfLogs); + await testGetLogs(lastBlockNumber, "latest", 0); + await testGetLogs("latest", lastBlockNumber, 0); + }); + + it("should filter appropriately when using blockHash", async () => { + const genesisBlockNumber = "0x0"; + const deployBlockNumber = "0x1"; + const emptyBlockNumber = "0x2"; + await provider.send("evm_mine"); // 0x2 + + await provider.send("eth_subscribe", ["newHeads"]); + const numberOfLogs = 4; + const data = + "0x" + + contract.contract.evm.methodIdentifiers["logNTimes(uint8)"] + + numberOfLogs.toString().padStart(64, "0"); + const txHash = await provider.send("eth_sendTransaction", [ + { + from: accounts[0], + to: contractAddress, + gas: "0x2fefd8", + data: data + } + ]); // 0x3 + await provider.once("message"); + const { blockHash } = await provider.send( + "eth_getTransactionReceipt", + [txHash] + ); + + async function testGetLogs( + blockHash: string, + expected: number, + address: string = contractAddress + ) { + const logs = await provider.send("eth_getLogs", [ + { address, blockHash } + ]); + assert.strictEqual( + logs.length, + expected, + `there should be ${expected} log(s) at the ${blockHash} block` + ); + } + + // tests blockHash + let { hash: genesisBlockHash } = await provider.send( + "eth_getBlockByNumber", + [genesisBlockNumber] + ); + await testGetLogs(blockHash, 4); + await testGetLogs(genesisBlockHash, 0); + let { hash: deployBlockHash } = await provider.send( + "eth_getBlockByNumber", + [deployBlockNumber] + ); + await testGetLogs(deployBlockHash, 1, null); + let { hash: emptyBlockHash } = await provider.send( + "eth_getBlockByNumber", + [emptyBlockNumber] + ); + await testGetLogs(emptyBlockHash, 0); + const invalidBlockHash = "0x123456789"; + await testGetLogs(invalidBlockHash, 0); + + // mine an extra block + await provider.send("evm_mine"); + await provider.once("message"); + + // make sure we still get the right data + await testGetLogs(blockHash, 4); + }); + }); + }); +});