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);
+      });
+    });
+  });
+});