Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
fix: handle initialization errors on server.listen, handle fallback r…
Browse files Browse the repository at this point in the history
…ejections (#1227)

fixes #938
  • Loading branch information
davidmurdoch committed Sep 22, 2021
1 parent 4209702 commit 20a46cc
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 162 deletions.
271 changes: 141 additions & 130 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,140 +256,152 @@ 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
);

{
// 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.#blockBeingSavedPromise = this.#initializeGenesisBlock(
firstBlockTime,
options.miner.blockGasLimit,
initialAccounts
);
blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then(
({ block }) => block
);
// if we don't already have a latest block, create a genesis block!
if (!latest) {
if (initialAccounts.length > 0) {
await this.#commitAccounts(initialAccounts);
}

this.#blockBeingSavedPromise = this.#initializeGenesisBlock(
firstBlockTime,
options.miner.blockGasLimit,
initialAccounts
);
blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then(
({ block }) => block
);
}
}
}

{
// 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
));
{
// 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

//#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);

miner.on("block", this.#handleNewBlockData);
this.once("stop").then(() => miner.clearListeners());
}
} catch (e) {
// we failed to start up :-( bail!
this.#state = Status.stopping;
// ignore errors while stopping here, since we are already in an
// exceptional case
await this.stop().catch(_ => {});

this.once("stop").then(() => miner.clearListeners());
throw e;
}

this.#state = Status.started;
Expand Down Expand Up @@ -1567,25 +1579,27 @@ 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.
if (this.#state === Status.starting) {
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;
Expand All @@ -1594,10 +1608,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;
}
}
2 changes: 1 addition & 1 deletion src/chains/ethereum/ethereum/src/forking/fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>((resolve, reject) => {
(forkingOptions.provider as any).send(
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 15 additions & 8 deletions src/chains/ethereum/ethereum/src/forking/trie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ const Ganache = {
provider: <T extends FlavorName = typeof DefaultFlavor>(
options?: ProviderOptions<T>
): ConnectorsByName[T]["provider"] => {
const connector = ConnectorLoader.initialize<T>(options);
return connector.provider;
const loader = ConnectorLoader.initialize<T>(options);
return loader.connector.provider;
}
};

Expand Down
Loading

0 comments on commit 20a46cc

Please sign in to comment.