diff --git a/src/chains/ethereum/package.json b/src/chains/ethereum/package.json index eab86a37a9..8c70b9e768 100644 --- a/src/chains/ethereum/package.json +++ b/src/chains/ethereum/package.json @@ -56,7 +56,6 @@ "emittery": "0.7.2", "eth-sig-util": "2.5.3", "ethereumjs-abi": "0.6.8", - "ethereumjs-account": "3.0.0", "ethereumjs-block": "2.2.2", "ethereumjs-common": "1.5.2", "ethereumjs-tx": "2.1.2", diff --git a/src/chains/ethereum/src/api.ts b/src/chains/ethereum/src/api.ts index db436acc51..0e3c7a9cb5 100644 --- a/src/chains/ethereum/src/api.ts +++ b/src/chains/ethereum/src/api.ts @@ -17,7 +17,6 @@ import Transaction from "./things/transaction"; import Wallet from "./wallet"; import { decode as rlpDecode } from "rlp"; import { $INLINE_JSON } from "ts-transformer-inline-file"; -import keccak from "keccak"; import { PromiEvent, utils } from "@ganache/utils"; import Emittery from "emittery"; @@ -46,10 +45,11 @@ import { Hardfork } from "./options/chain-options"; // Read in the current ganache version from core's package.json const { version } = $INLINE_JSON("../../../packages/ganache/package.json"); +const { keccak } = utils; //#endregion //#region Constants -const RPCQUANTITY_ZERO = utils.RPCQUANTITY_ZERO; +const { RPCQUANTITY_ZERO } = utils; const CLIENT_VERSION = `Ganache/v${version}/EthereumJS TestRPC/v${version}/ethereum-js`; const PROTOCOL_VERSION = Data.from("0x3f"); const RPC_MODULES = { @@ -347,10 +347,12 @@ export default class EthereumApi implements types.Api { * * @param address * @param nonce - * @returns true if it worked + * @returns `true` if it worked */ @assertArgLength(2) async evm_setAccountNonce(address: string, nonce: string) { + // TODO: the effect of this function could happen during a block mine operation, which would cause all sorts of + // issues. We need to figure out a good way of timing this. return new Promise((resolve, reject) => { const buffer = Address.from(address).toBuffer(); const blockchain = this.#blockchain; @@ -369,6 +371,8 @@ export default class EthereumApi implements types.Api { return; } + // TODO: do we need to mine a block here? The changes we're making really don't make any sense at all + // and produce an invalid trie going forward. blockchain.mine(0).then(() => resolve(true), reject); }); } @@ -628,7 +632,7 @@ export default class EthereumApi implements types.Api { */ @assertArgLength(1) async web3_sha3(data: string) { - return Data.from(keccak("keccak256").update(data).digest()); + return Data.from(keccak(Buffer.from(data))); } //#endregion diff --git a/src/chains/ethereum/src/blockchain.ts b/src/chains/ethereum/src/blockchain.ts index c7bfce25d3..47eaa9a23e 100644 --- a/src/chains/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/src/blockchain.ts @@ -6,12 +6,11 @@ import Emittery from "emittery"; import BlockManager from "./data-managers/block-manager"; import BlockLogs from "./things/blocklogs"; import TransactionManager from "./data-managers/transaction-manager"; -import CheckpointTrie from "merkle-patricia-tree"; +import SecureTrie from "merkle-patricia-tree/secure"; import { BN, KECCAK256_RLP } from "ethereumjs-util"; import Account from "./things/account"; import { promisify } from "util"; import { Quantity, Data } from "@ganache/utils"; -import EthereumJsAccount from "ethereumjs-account"; import AccountManager from "./data-managers/account-manager"; import { utils } from "@ganache/utils"; import Transaction from "./things/transaction"; @@ -152,7 +151,7 @@ export default class Blockchain extends Emittery.Typed< public transactionReceipts: Manager; public accounts: AccountManager; public vm: VM; - public trie: CheckpointTrie; + public trie: SecureTrie; readonly #database: Database; readonly #common: Common; @@ -219,12 +218,12 @@ export default class Blockchain extends Emittery.Typed< block: latest, blockLogs: null }); - this.trie = new CheckpointTrie( + this.trie = new SecureTrie( database.trie, latest.header.stateRoot.toBuffer() ); } else { - this.trie = new CheckpointTrie(database.trie, null); + this.trie = new SecureTrie(database.trie, null); } this.blockLogs = new BlockLogManager(database.blockLogs); @@ -495,7 +494,7 @@ export default class Blockchain extends Emittery.Typed< } createVmFromStateTrie = ( - stateTrie: CheckpointTrie, + stateTrie: SecureTrie, allowUnlimitedContractSize: boolean ) => { const blocks = this.blocks; @@ -518,26 +517,20 @@ export default class Blockchain extends Emittery.Typed< }); }; - #commitAccounts = async (accounts: Account[]): Promise => { - const stateManager = this.vm.stateManager; - const putAccount = promisify(stateManager.putAccount.bind(stateManager)); - const checkpoint = promisify(stateManager.checkpoint.bind(stateManager)); - const commit = promisify(stateManager.commit.bind(stateManager)); - await checkpoint(); - const l = accounts.length; - const pendingAccounts = Array(l); - for (let i = 0; i < l; i++) { - const account = accounts[i]; - const ethereumJsAccount = new EthereumJsAccount(); - (ethereumJsAccount.nonce = account.nonce.toBuffer()), - (ethereumJsAccount.balance = account.balance.toBuffer()); - pendingAccounts[i] = putAccount( - account.address.toBuffer(), - ethereumJsAccount - ); - } - await Promise.all(pendingAccounts); - await commit(); + #commitAccounts = (accounts: Account[]) => { + return new Promise((resolve, reject) => { + let length = accounts.length; + const cb = (err: Error) => { + if (err) reject(err); + else { + if (--length === 0) resolve(void 0); + } + }; + for (let i = 0; i < length; i++) { + const account = accounts[i]; + this.trie.put(account.address.toBuffer(), account.serialize(), cb); + } + }); }; #initializeGenesisBlock = async ( @@ -797,7 +790,7 @@ export default class Blockchain extends Emittery.Typed< ); if (gasLeft >= 0) { - const stateTrie = new CheckpointTrie( + const stateTrie = new SecureTrie( this.#database.trie, parentBlock.header.stateRoot.toBuffer() ); @@ -886,7 +879,7 @@ export default class Blockchain extends Emittery.Typed< // // TODO: Forking needs the forked block number passed during this step: // https://github.com/trufflesuite/ganache-core/blob/develop/lib/blockchain_double.js#L917 - const trie = new CheckpointTrie( + const trie = new SecureTrie( this.#database.trie, parentBlock.header.stateRoot.toBuffer() ); @@ -1136,7 +1129,16 @@ export default class Blockchain extends Emittery.Typed< // clean up listeners this.vm.removeAllListeners(); - this.transactions.transactionPool.clearListeners(); + + // pause processing new transactions... + await this.transactions.pause(); + + // then pause the miner, too. + await this.#miner.pause(); + + // wait for anything in the process of being saved to finish up + await this.#blockBeingSavedPromise; + await this.emit("stop"); if (this.#state === Status.started) { diff --git a/src/chains/ethereum/src/data-managers/account-manager.ts b/src/chains/ethereum/src/data-managers/account-manager.ts index f0be2b9c37..dad21409e4 100644 --- a/src/chains/ethereum/src/data-managers/account-manager.ts +++ b/src/chains/ethereum/src/data-managers/account-manager.ts @@ -7,7 +7,7 @@ import { LevelUp } from "levelup"; import { rlp } from "ethereumjs-util"; import { utils, Quantity } from "@ganache/utils"; -const RPCQUANTITY_ZERO = utils.RPCQUANTITY_ZERO; +const { keccak, RPCQUANTITY_ZERO } = utils; export default class AccountManager { #blockchain: Blockchain; @@ -25,7 +25,7 @@ export default class AccountManager { const block = await blockchain.blocks.get(blockNumber); const trieCopy = new Trie(this.#trie, block.header.stateRoot.toBuffer()); return new Promise((resolve, reject) => { - trieCopy.get(address.toBuffer(), (err, data) => { + trieCopy.get(keccak(address.toBuffer()), (err: Error, data: Buffer) => { if (err) return reject(err); resolve(data); }); diff --git a/src/chains/ethereum/src/things/account.ts b/src/chains/ethereum/src/things/account.ts index 88639235c8..5e0eefdcf8 100644 --- a/src/chains/ethereum/src/things/account.ts +++ b/src/chains/ethereum/src/things/account.ts @@ -20,7 +20,7 @@ export default class Account { } public static fromBuffer(buffer: Buffer) { - const account = Object.create(Account); + const account = Object.create(Account.prototype); const arr = (rlp.decode(buffer) as any) as [Buffer, Buffer, Buffer, Buffer]; account.nonce = Quantity.from(arr[0]); account.balance = Quantity.from(arr[1]); @@ -30,13 +30,11 @@ export default class Account { } public serialize() { - return rlp.encode( - Buffer.concat([ - this.nonce.toBuffer(), - this.balance.toBuffer(), - this.stateRoot, - this.codeHash - ]) - ); + return rlp.encode([ + this.nonce.toBuffer(), + this.balance.toBuffer(), + this.stateRoot, + this.codeHash + ]); } } diff --git a/src/chains/ethereum/tests/api/eth/eth.test.ts b/src/chains/ethereum/tests/api/eth/eth.test.ts index 5de4379d33..c4f5234c99 100644 --- a/src/chains/ethereum/tests/api/eth/eth.test.ts +++ b/src/chains/ethereum/tests/api/eth/eth.test.ts @@ -124,7 +124,7 @@ describe("api", () => { }); describe("eth_getTransactionCount", () => { - it("should get the tranasction count of the block", async () => { + it("should get the transaction count of the block", async () => { const tx = { from: accounts[0], to: accounts[1], diff --git a/src/chains/ethereum/tests/api/eth/subscribe.test.ts b/src/chains/ethereum/tests/api/eth/subscribe.test.ts index 1a326693da..4d090012ba 100644 --- a/src/chains/ethereum/tests/api/eth/subscribe.test.ts +++ b/src/chains/ethereum/tests/api/eth/subscribe.test.ts @@ -74,20 +74,20 @@ describe("api", () => { gasLimit: gasLimit, gasUsed: "0x0", hash: - "0xf821422e084d82d550019e555b656b9113c9af45c4c03fad670caaa9b5d8acde", + "0x7ffe37737a6a39477f0cd9eeb632a6b393c440ad17b6e82fcbe1ebf6556d768c", logsBloom: `0x${"0".repeat(512)}`, miner: `0x${"0".repeat(40)}`, mixHash: `0x${"0".repeat(64)}`, nonce: "0x0000000000000000", number: Quantity.from(startingBlockNumber + 1).toString(), parentHash: - "0x746144f35cfbcc1bb8ea1dcd540b674c81cc25ffec8fa1ec42d444cba9678cc2", + "0x48ba6f672feca71b75568153ce81c75d0a8074c935d4ab2a23fc7dd64dd13fe2", receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", stateRoot: - "0x8281cb204e0242d2d9178e392b60eaf4563ae5ffc4897c9c6cf6e99a4d35aff3", + "0xf0d9daf6f1dd86ae11b32005e9717845db40b6ddd926a37973922a957c400071", timestamp: Quantity.from(timestamp).toString(), transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" diff --git a/src/packages/utils/npm-shrinkwrap.json b/src/packages/utils/npm-shrinkwrap.json index 5499a0fbaa..0a99a65d45 100644 --- a/src/packages/utils/npm-shrinkwrap.json +++ b/src/packages/utils/npm-shrinkwrap.json @@ -33,6 +33,25 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, + "keccak": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", + "requires": { + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + } + }, + "node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" + }, + "node-gyp-build": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" + }, "uWebSockets.js": { "version": "github:uNetworking/uWebSockets.js#3dbec7b56d627193e20705844b6bd10e49848b8c", "from": "github:uNetworking/uWebSockets.js#v18.4.0", diff --git a/src/packages/utils/package.json b/src/packages/utils/package.json index e3baa76ca1..ca6cb2bf41 100644 --- a/src/packages/utils/package.json +++ b/src/packages/utils/package.json @@ -44,7 +44,8 @@ "tooling" ], "dependencies": { - "emittery": "0.7.2" + "emittery": "0.7.2", + "keccak": "3.0.1" }, "devDependencies": { "uWebSockets.js": "github:uNetworking/uWebSockets.js#v18.4.0" diff --git a/src/packages/utils/src/utils/index.ts b/src/packages/utils/src/utils/index.ts index 285ebfef0e..28c0681c74 100644 --- a/src/packages/utils/src/utils/index.ts +++ b/src/packages/utils/src/utils/index.ts @@ -7,3 +7,4 @@ export * from "./has-own"; export * from "./uint-to-buffer"; export * from "./constants"; export * from "./buffer-to-key"; +export * from "./keccak"; diff --git a/src/packages/utils/src/utils/keccak.ts b/src/packages/utils/src/utils/keccak.ts new file mode 100644 index 0000000000..56b7c54d90 --- /dev/null +++ b/src/packages/utils/src/utils/keccak.ts @@ -0,0 +1,27 @@ +import makeKeccak, { Keccak } from "keccak"; + +const RATE = 1088; +const CAPACITY = 512; + +const instance = makeKeccak("keccak256") as { + _state: { + absorb: (buffer: Buffer) => void; + squeeze: (length: number) => Buffer; + initialize: (rate: number, capacity: number) => void; + }; + _finalized: boolean; +}; + +/** + * keccak256, but faster if you use it a lot. + * @param buffer + */ +export function keccak(buffer: Buffer) { + instance._state.absorb(buffer); + const digest = instance._state.squeeze(32); + // reset and remove result from memory + instance._state.initialize(RATE, CAPACITY); + // make this re-usable + instance._finalized = false; + return digest; +}