From 8613d011b565dd50b0e98f6f68382b57c643f101 Mon Sep 17 00:00:00 2001 From: David Murdoch Date: Tue, 21 Sep 2021 20:08:17 -0400 Subject: [PATCH 1/4] fix log typo --- src/chains/ethereum/ethereum/src/forking/fork.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chains/ethereum/ethereum/src/forking/fork.ts b/src/chains/ethereum/ethereum/src/forking/fork.ts index 135d62ef81..a30960c691 100644 --- a/src/chains/ethereum/ethereum/src/forking/fork.ts +++ b/src/chains/ethereum/ethereum/src/forking/fork.ts @@ -91,7 +91,7 @@ export class Fork { // TODO: remove support for legacy providers // legacy `.send` console.warn( - "WARNING: Ganache forking only supports EIP-1193-compliant providers. Legacy support for send is currently enabled, but will be removed in a future version _without_ a breaking change. To remove this warning, switch to an EIP-1193 provider. This error is probably caused by an old version of Web3's HttpProvider (or an ganache < v7)" + "WARNING: Ganache forking only supports EIP-1193-compliant providers. Legacy support for send is currently enabled, but will be removed in a future version _without_ a breaking change. To remove this warning, switch to an EIP-1193 provider. This error is probably caused by an old version of Web3's HttpProvider (or ganache < v7)" ); return new Promise((resolve, reject) => { (forkingOptions.provider as any).send( From b691a8b3ca9f9cca98b41bd842f7c43a701661f4 Mon Sep 17 00:00:00 2001 From: David Murdoch Date: Tue, 21 Sep 2021 20:59:37 -0400 Subject: [PATCH 2/4] handle connector/provider initialization errors on server.listen --- .../ethereum/ethereum/src/blockchain.ts | 270 +++++++++--------- .../src/forking/handlers/http-handler.ts | 4 +- src/packages/core/index.ts | 4 +- src/packages/core/src/connector-loader.ts | 8 +- src/packages/core/src/server.ts | 37 +-- src/packages/core/tests/server.test.ts | 10 + 6 files changed, 179 insertions(+), 154 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index efb49a6f2e..f12e6eab4e 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -256,140 +256,149 @@ export default class Blockchain extends Emittery.Typed< const options = this.#options; const instamine = this.#instamine; - let common: Common; - if (this.fallback) { - await Promise.all([database.initialize(), this.fallback.initialize()]); - common = this.common = this.fallback.common; - options.fork.blockNumber = this.fallback.blockNumber.toNumber(); - options.chain.networkId = common.networkId(); - options.chain.chainId = common.chainId(); - } else { - await database.initialize(); - common = this.common = createCommon( - options.chain.chainId, - options.chain.networkId, - options.chain.hardfork - ); - } - - const blocks = (this.blocks = await BlockManager.initialize( - this, - common, - database.blockIndexes, - database.blocks - )); - - this.blockLogs = new BlockLogManager(database.blockLogs, this); - this.transactions = new TransactionManager( - options.miner, - common, - this, - database.transactions - ); - this.transactionReceipts = new TransactionReceiptManager( - database.transactionReceipts, - this - ); - this.accounts = new AccountManager(this); - this.storageKeys = database.storageKeys; - - // if we have a latest block, use it to set up the trie. - const { latest } = blocks; - { - let stateRoot: Data | null; - if (latest) { - this.#blockBeingSavedPromise = Promise.resolve({ - block: latest, - blockLogs: null - }); - ({ stateRoot } = latest.header); + try { + let common: Common; + if (this.fallback) { + await this.fallback.initialize(); + await database.initialize(); + + common = this.common = this.fallback.common; + options.fork.blockNumber = this.fallback.blockNumber.toNumber(); + options.chain.networkId = common.networkId(); + options.chain.chainId = common.chainId(); } else { - stateRoot = null; + await database.initialize(); + common = this.common = createCommon( + options.chain.chainId, + options.chain.networkId, + options.chain.hardfork + ); } - this.trie = makeTrie(this, database.trie, stateRoot); - } - // create VM and listen to step events - this.vm = await this.createVmFromStateTrie( - this.trie, - options.chain.allowUnlimitedContractSize, - true - ); + const blocks = (this.blocks = await BlockManager.initialize( + this, + common, + database.blockIndexes, + database.blocks + )); - { - // create first block - let firstBlockTime: number; - if (options.chain.time != null) { - // If we were given a timestamp, use it instead of the `_currentTime` - const t = options.chain.time.getTime(); - firstBlockTime = Math.floor(t / 1000); - this.setTime(t); - } else { - firstBlockTime = this.#currentTime(); + this.blockLogs = new BlockLogManager(database.blockLogs, this); + this.transactions = new TransactionManager( + options.miner, + common, + this, + database.transactions + ); + this.transactionReceipts = new TransactionReceiptManager( + database.transactionReceipts, + this + ); + this.accounts = new AccountManager(this); + this.storageKeys = database.storageKeys; + + // if we have a latest block, use it to set up the trie. + const { latest } = blocks; + { + let stateRoot: Data | null; + if (latest) { + this.#blockBeingSavedPromise = Promise.resolve({ + block: latest, + blockLogs: null + }); + ({ stateRoot } = latest.header); + } else { + stateRoot = null; + } + this.trie = makeTrie(this, database.trie, stateRoot); } - // if we don't already have a latest block, create a genesis block! - if (!latest) { - if (initialAccounts.length > 0) { - await this.#commitAccounts(initialAccounts); - } + // create VM and listen to step events + this.vm = await this.createVmFromStateTrie( + this.trie, + options.chain.allowUnlimitedContractSize, + true + ); - this.#blockBeingSavedPromise = this.#initializeGenesisBlock( - firstBlockTime, - options.miner.blockGasLimit, - initialAccounts - ); - blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then( - ({ block }) => block - ); - } - } + { + // create first block + let firstBlockTime: number; + if (options.chain.time != null) { + // If we were given a timestamp, use it instead of the `_currentTime` + const t = options.chain.time.getTime(); + firstBlockTime = Math.floor(t / 1000); + this.setTime(t); + } else { + firstBlockTime = this.#currentTime(); + } - { - // configure and start miner - const txPool = this.transactions.transactionPool; - const minerOpts = options.miner; - const miner = (this.#miner = new Miner( - minerOpts, - txPool.executables, - this.vm, - this.#readyNextBlock - )); + // if we don't already have a latest block, create a genesis block! + if (!latest) { + if (initialAccounts.length > 0) { + await this.#commitAccounts(initialAccounts); + } - //#region re-emit miner events: - miner.on("ganache:vm:tx:before", event => { - this.emit("ganache:vm:tx:before", event); - }); - miner.on("ganache:vm:tx:step", event => { - if (!this.#emitStepEvent) return; - this.emit("ganache:vm:tx:step", event); - }); - miner.on("ganache:vm:tx:after", event => { - this.emit("ganache:vm:tx:after", event); - }); - //#endregion - - //#region automatic mining - const nullResolved = Promise.resolve(null); - const mineAll = (maxTransactions: Capacity) => - this.#isPaused() ? nullResolved : this.mine(maxTransactions); - if (instamine) { - // insta mining - // whenever the transaction pool is drained mine the txs into blocks - txPool.on("drain", mineAll.bind(null, Capacity.Single)); - } else { - // interval mining - const wait = () => - // unref, so we don't hold the chain open if nothing can interact with it - unref((this.#timer = setTimeout(next, minerOpts.blockTime * 1e3))); - const next = () => mineAll(Capacity.FillBlock).then(wait); - wait(); + this.#blockBeingSavedPromise = this.#initializeGenesisBlock( + firstBlockTime, + options.miner.blockGasLimit, + initialAccounts + ); + blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then( + ({ block }) => block + ); + } } - //#endregion - miner.on("block", this.#handleNewBlockData); + { + // configure and start miner + const txPool = this.transactions.transactionPool; + const minerOpts = options.miner; + const miner = (this.#miner = new Miner( + minerOpts, + txPool.executables, + this.vm, + this.#readyNextBlock + )); + + //#region re-emit miner events: + miner.on("ganache:vm:tx:before", event => { + this.emit("ganache:vm:tx:before", event); + }); + miner.on("ganache:vm:tx:step", event => { + if (!this.#emitStepEvent) return; + this.emit("ganache:vm:tx:step", event); + }); + miner.on("ganache:vm:tx:after", event => { + this.emit("ganache:vm:tx:after", event); + }); + //#endregion + + //#region automatic mining + const nullResolved = Promise.resolve(null); + const mineAll = (maxTransactions: Capacity) => + this.#isPaused() ? nullResolved : this.mine(maxTransactions); + if (instamine) { + // insta mining + // whenever the transaction pool is drained mine the txs into blocks + txPool.on("drain", mineAll.bind(null, Capacity.Single)); + } else { + // interval mining + const wait = () => + // unref, so we don't hold the chain open if nothing can interact with it + unref((this.#timer = setTimeout(next, minerOpts.blockTime * 1e3))); + const next = () => mineAll(Capacity.FillBlock).then(wait); + wait(); + } + //#endregion + + miner.on("block", this.#handleNewBlockData); - this.once("stop").then(() => miner.clearListeners()); + this.once("stop").then(() => miner.clearListeners()); + } + } catch (e) { + // we failed to start up. bail! :-( + this.#state = Status.stopping; + this.stop(); + throw e; } this.#state = Status.started; @@ -1567,7 +1576,7 @@ export default class Blockchain extends Emittery.Typed< * Gracefully shuts down the blockchain service and all of its dependencies. */ public async stop() { - // If the blockchain is still initalizing we don't want to shut down + // If the blockchain is still initializing we don't want to shut down // yet because there may still be database calls in flight. Leveldb may // cause a segfault due to a race condition between a db write and the close // call. @@ -1575,17 +1584,19 @@ export default class Blockchain extends Emittery.Typed< await this.once("ready"); } + this.#state = Status.stopping; + // stop the polling miner, if necessary clearTimeout(this.#timer); // clean up listeners - this.vm.removeAllListeners(); + this.vm && this.vm.removeAllListeners(); // pause processing new transactions... - await this.transactions.pause(); + this.transactions && (await this.transactions.pause()); // then pause the miner, too. - await this.#miner.pause(); + this.#miner && (await this.#miner.pause()); // wait for anything in the process of being saved to finish up await this.#blockBeingSavedPromise; @@ -1594,10 +1605,7 @@ export default class Blockchain extends Emittery.Typed< await this.emit("stop"); - if (this.#state === Status.started) { - this.#state = Status.stopping; - await this.#database.close(); - this.#state = Status.stopped; - } + this.#database && (await this.#database.close()); + this.#state = Status.stopped; } } diff --git a/src/chains/ethereum/ethereum/src/forking/handlers/http-handler.ts b/src/chains/ethereum/ethereum/src/forking/handlers/http-handler.ts index d2c9931c83..846fe8958b 100644 --- a/src/chains/ethereum/ethereum/src/forking/handlers/http-handler.ts +++ b/src/chains/ethereum/ethereum/src/forking/handlers/http-handler.ts @@ -1,6 +1,6 @@ import { EthereumInternalOptions } from "@ganache/ethereum-options"; import { JsonRpcResponse, JsonRpcError } from "@ganache/utils"; -import { AbortError } from "@ganache/ethereum-utils"; +import { AbortError, CodedError } from "@ganache/ethereum-utils"; // TODO: support http2 import http, { RequestOptions, Agent as HttpAgent } from "http"; import https, { Agent as HttpsAgent } from "https"; @@ -172,7 +172,7 @@ export class HttpHandler extends BaseHandler implements Handler { if ("result" in result) { return result.result; } else if ("error" in result) { - throw result.error; + throw new CodedError(result.error.message, result.error.code); } }); this.requestCache.set(data, promise); diff --git a/src/packages/core/index.ts b/src/packages/core/index.ts index b86efb7fd7..1f248f49a0 100644 --- a/src/packages/core/index.ts +++ b/src/packages/core/index.ts @@ -44,8 +44,8 @@ const Ganache = { provider: ( options?: ProviderOptions ): ConnectorsByName[T]["provider"] => { - const connector = ConnectorLoader.initialize(options); - return connector.provider; + const loader = ConnectorLoader.initialize(options); + return loader.connector.provider; } }; diff --git a/src/packages/core/src/connector-loader.ts b/src/packages/core/src/connector-loader.ts index 55b6e0d2e5..1a613388c7 100644 --- a/src/packages/core/src/connector-loader.ts +++ b/src/packages/core/src/connector-loader.ts @@ -36,14 +36,16 @@ const initialize = ( // connector.connect interface const connectPromise = connector.connect ? connector.connect() - : (connector as any).initialize(); + : ((connector as any).initialize() as Promise); // The request coordinator is initialized in a "paused" state; when the // provider is ready we unpause.. This lets us accept queue requests before // we've even fully initialized. - connectPromise.then(requestCoordinator.resume); - return connector; + return { + connector, + promise: connectPromise.then(requestCoordinator.resume) + }; }; /** diff --git a/src/packages/core/src/server.ts b/src/packages/core/src/server.ts index e9fc51c7de..e00d5b0418 100644 --- a/src/packages/core/src/server.ts +++ b/src/packages/core/src/server.ts @@ -6,6 +6,15 @@ import { } from "./options"; import allSettled from "promise.allsettled"; + +// This `shim()` is necessary for `Promise.allSettled` to be shimmed +// in `node@10`. We cannot use `allSettled([...])` directly due to +// https://github.com/es-shims/Promise.allSettled/issues/5 without +// upgrading Typescript. TODO: if Typescript is upgraded to 4.2.3+ +// then this line could be removed and `Promise.allSettled` below +// could replaced with `allSettled`. +allSettled.shim(); + import AggregateError from "aggregate-error"; import { App, @@ -100,7 +109,7 @@ export class Server< #connector: ConnectorsByName[T]; #websocketServer: WebsocketServer | null = null; - #initializer: Promise; + #initializer: Promise<[void, void]>; public get provider(): ConnectorsByName[T]["provider"] { return this.#connector.provider; @@ -125,10 +134,16 @@ export class Server< // const server = Ganache.server(); // const provider = server.provider; // await server.listen(8545) - const connector = (this.#connector = ConnectorLoader.initialize( - this.#providerOptions - )); - this.#initializer = this.initialize(connector); + const loader = ConnectorLoader.initialize(this.#providerOptions); + const connector = (this.#connector = loader.connector); + + // Since the ConnectorLoader starts an async promise that we intentionally + // don't await yet we keep the promise around for something else to handle + // later. + this.#initializer = Promise.all([ + loader.promise, + this.initialize(connector) + ]); } private async initialize(connector: Connector) { @@ -180,18 +195,8 @@ export class Server< this.#status = ServerStatus.opening; - const initializePromise = this.#initializer; - - // This `shim()` is necessary for `Promise.allSettled` to be shimmed - // in `node@10`. We cannot use `allSettled([...])` directly due to - // https://github.com/es-shims/Promise.allSettled/issues/5 without - // upgrading Typescript. TODO: if Typescript is upgraded to 4.2.3+ - // then this line could be removed and `Promise.allSettled` below - // could replaced with `allSettled`. - allSettled.shim(); - const promise = Promise.allSettled([ - initializePromise, + this.#initializer, new Promise( (resolve: (listenSocket: false | us_listen_socket) => void) => { // Make sure we have *exclusive* use of this port. diff --git a/src/packages/core/tests/server.test.ts b/src/packages/core/tests/server.test.ts index e65ea7bb33..3cbeed75d8 100644 --- a/src/packages/core/tests/server.test.ts +++ b/src/packages/core/tests/server.test.ts @@ -71,6 +71,16 @@ describe("server", () => { return response; } + it("handles connector initialization errors by rejecting on .listen", async () => { + // This Ganache.server({...}) here will cause an internal error in the + // Ethereum provider initialization. We don't want to throw an unhandled + // promise reject; so we handle it in the `listen` method. + const s = Ganache.server({ + fork: { url: "https://mainnet.infura.io/v3/INVALID_URL" } + }); + await assert.rejects(s.listen(port)); + }); + it("returns its status", async () => { const s = Ganache.server(); try { From 7e56f02ecd5be0a240fad1e182ebfcafbf30ebbc Mon Sep 17 00:00:00 2001 From: David Murdoch Date: Tue, 21 Sep 2021 21:05:49 -0400 Subject: [PATCH 3/4] await the stop in blockchain --- src/chains/ethereum/ethereum/src/blockchain.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index f12e6eab4e..1a86100833 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -395,9 +395,12 @@ export default class Blockchain extends Emittery.Typed< this.once("stop").then(() => miner.clearListeners()); } } catch (e) { - // we failed to start up. bail! :-( + // we failed to start up :-( bail! this.#state = Status.stopping; - this.stop(); + // ignore errors while stopping here, since we are already in an + // exceptional case + await this.stop().catch(_ => {}); + throw e; } From 54115c74b5f2095eab54cab7743e5c7ad6cf4df9 Mon Sep 17 00:00:00 2001 From: David Murdoch Date: Wed, 22 Sep 2021 11:04:44 -0400 Subject: [PATCH 4/4] don't allow unhandle rejections to happen when the codeProm promise rejects --- .../ethereum/ethereum/src/forking/trie.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/forking/trie.ts b/src/chains/ethereum/ethereum/src/forking/trie.ts index dec6608c5d..e06ff84aa6 100644 --- a/src/chains/ethereum/ethereum/src/forking/trie.ts +++ b/src/chains/ethereum/ethereum/src/forking/trie.ts @@ -173,15 +173,22 @@ export class ForkTrie extends GanacheTrie { // because code requires additional asynchronous processing, we await and // process it ASAP - const codeHex = await codeProm; - if (codeHex !== "0x") { - const code = Data.from(codeHex).toBuffer(); - // the codeHash is just the keccak hash of the code itself - account.codeHash = keccak(code); - if (!account.codeHash.equals(KECCAK256_NULL)) { - // insert the code directly into the database with a key of `codeHash` - promises[2] = this.db.put(account.codeHash, code); + try { + const codeHex = await codeProm; + if (codeHex !== "0x") { + const code = Data.from(codeHex).toBuffer(); + // the codeHash is just the keccak hash of the code itself + account.codeHash = keccak(code); + if (!account.codeHash.equals(KECCAK256_NULL)) { + // insert the code directly into the database with a key of `codeHash` + promises[2] = this.db.put(account.codeHash, code); + } } + } catch (e) { + // Since we fired off some promises that may throw themselves we need to + // catch these errors and discard them. + Promise.all(promises).catch(e => {}); + throw e; } // finally, set the `nonce` and `balance` on the account before returning