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

fix: rearchitect server status and move async functions from constructors to initialize functions #877

Merged
merged 13 commits into from
Mar 30, 2021
Merged
6 changes: 4 additions & 2 deletions src/chains/ethereum/ethereum/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,15 @@ export default class EthereumApi implements types.Api {
const blockchain = (this.#blockchain = new Blockchain(
options,
common,
initialAccounts,
coinbaseAddress
));
blockchain.on("start", () => emitter.emit("connect"));
emitter.on("disconnect", blockchain.stop.bind(blockchain));
}

async initialize() {
await this.#blockchain.initialize(this.#wallet.initialAccounts);
}

//#region db
/**
* Stores a string in the local database.
Expand Down
196 changes: 101 additions & 95 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ export default class Blockchain extends Emittery.Typed<
constructor(
options: EthereumInternalOptions,
common: Common,
initialAccounts: Account[],
coinbaseAddress: Address
) {
super();
Expand Down Expand Up @@ -214,113 +213,120 @@ export default class Blockchain extends Emittery.Typed<
}
}

const database = (this.#database = new Database(options.database, this));
database.once("ready").then(async () => {
const blocks = (this.blocks = await BlockManager.initialize(
common,
database.blockIndexes,
database.blocks
));
this.coinbase = coinbaseAddress;

// if we have a latest block, use it to set up the trie.
const latest = blocks.latest;
if (latest) {
this.#blockBeingSavedPromise = Promise.resolve({
block: latest,
blockLogs: null
});
this.trie = new SecureTrie(
database.trie,
latest.header.stateRoot.toBuffer()
);
} else {
this.trie = new SecureTrie(database.trie, null);
}

this.blockLogs = new BlockLogManager(database.blockLogs);
this.transactions = new TransactionManager(
options.miner,
common,
this,
database.transactions
);
this.transactionReceipts = new Manager(
database.transactionReceipts,
TransactionReceipt
);
this.accounts = new AccountManager(this, database.trie);
this.storageKeys = database.storageKeys;
this.#database = new Database(options.database, this);
}

this.coinbase = coinbaseAddress;
async initialize(initialAccounts: Account[]) {
await this.#database.initialize();

// create VM and listen to step events
this.vm = this.createVmFromStateTrie(
this.trie,
options.chain.allowUnlimitedContractSize
const database = this.#database;
mikeseese marked this conversation as resolved.
Show resolved Hide resolved
const options = this.#options;
const common = this.#common;

const blocks = (this.blocks = await BlockManager.initialize(
common,
database.blockIndexes,
database.blocks
));

// if we have a latest block, use it to set up the trie.
const latest = blocks.latest;
if (latest) {
this.#blockBeingSavedPromise = Promise.resolve({
block: latest,
blockLogs: null
});
this.trie = new SecureTrie(
database.trie,
latest.header.stateRoot.toBuffer()
);
} else {
this.trie = new SecureTrie(database.trie, null);
}

{
// 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.transactions = new TransactionManager(
options.miner,
common,
this,
database.transactions
);
this.transactionReceipts = new Manager(
database.transactionReceipts,
TransactionReceipt
);
this.accounts = new AccountManager(this, database.trie);
this.storageKeys = database.storageKeys;

// if we don't already have a latest block, create a genesis block!
if (!latest) {
await this.#commitAccounts(initialAccounts);
// create VM and listen to step events
this.vm = this.createVmFromStateTrie(
this.trie,
options.chain.allowUnlimitedContractSize
);

this.#blockBeingSavedPromise = this.#initializeGenesisBlock(
firstBlockTime,
options.miner.blockGasLimit
);
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,
instamine,
this.vm,
this.#readyNextBlock
));

//#region automatic mining
const nullResolved = Promise.resolve(null);
const mineAll = (maxTransactions: number) =>
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, 1));
} else {
// interval mining
const wait = () => unref(setTimeout(next, minerOpts.blockTime * 1e3));
const next = () => mineAll(-1).then(wait);
wait();
}
//#endregion
// if we don't already have a latest block, create a genesis block!
if (!latest) {
await this.#commitAccounts(initialAccounts);

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

this.once("stop").then(() => miner.clearListeners());
{
// configure and start miner
const txPool = this.transactions.transactionPool;
const minerOpts = options.miner;
const miner = (this.#miner = new Miner(
minerOpts,
txPool.executables,
this.#instamine,
this.vm,
this.#readyNextBlock
));

//#region automatic mining
const nullResolved = Promise.resolve(null);
const mineAll = (maxTransactions: number) =>
this.#isPaused() ? nullResolved : this.mine(maxTransactions);
if (this.#instamine) {
// insta mining
// whenever the transaction pool is drained mine the txs into blocks
txPool.on("drain", mineAll.bind(null, 1));
} else {
// interval mining
const wait = () => unref(setTimeout(next, minerOpts.blockTime * 1e3));
const next = () => mineAll(-1).then(wait);
wait();
}
//#endregion

this.#state = Status.started;
this.emit("start");
});
miner.on("block", this.#handleNewBlockData);

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

this.#state = Status.started;
this.emit("start");
}

#saveNewBlock = ({
Expand Down
15 changes: 9 additions & 6 deletions src/chains/ethereum/ethereum/src/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ export class Connector
) {
super();

const provider = (this.#provider = new EthereumProvider(
this.#provider = new EthereumProvider(
providerOptions,
executor
));
provider.on("connect", () => {
// tell the consumer (like a `ganache-core` server/connector) everything is ready
this.emit("ready");
});
);
}

async initialize() {
await this.#provider.initialize();
// no need to wait for #provider.once("connect") as the initialize()
// promise has already accounted for that after the promise is resolved
await this.emit("ready");
}

parse(message: Buffer) {
Expand Down
3 changes: 1 addition & 2 deletions src/chains/ethereum/ethereum/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ export default class Database extends Emittery {

this.#options = options;
this.blockchain = blockchain;
this.#initialize();
}

#initialize = async () => {
initialize = async () => {
const levelupOptions: any = {
keyEncoding: "binary",
valueEncoding: "binary"
Expand Down
5 changes: 5 additions & 0 deletions src/chains/ethereum/ethereum/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export default class EthereumProvider
this.#api = new EthereumApi(providerOptions, wallet, this);
}

async initialize() {
await this.#api.initialize();
await this.emit("connect");
}

/**
* Returns the options, including defaults and generated, used to start Ganache.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/chains/ethereum/ethereum/src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ export default class Wallet {
fileData.addresses[address] = address;
fileData.private_keys[address] = privateKey;
});

// WARNING: Do not turn this to an async method without
// making a Wallet.initialize() function and calling it via
// Provider.initialize(). No async methods in constructors.
// writeFileSync here is acceptable.
writeFileSync(opts.accountKeysPath, JSON.stringify(fileData));
}
//#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,10 @@ describe("api", () => {
const blockchain = new Blockchain(
EthereumOptionsConfig.normalize({}),
common,
initialAccounts,
address
);

await blockchain.once("start");
await blockchain.initialize(initialAccounts);

// Deployment transaction
const deploymentTransaction = new Transaction(
Expand Down
8 changes: 2 additions & 6 deletions src/chains/ethereum/ethereum/tests/helpers/getProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,8 @@ const getProvider = async (
const requestCoordinator = new RequestCoordinator(doAsync ? 0 : 1);
const executor = new Executor(requestCoordinator);
const provider = new EthereumProvider(options, executor);
await new Promise(resolve => {
provider.on("connect", () => {
requestCoordinator.resume();
resolve(void 0);
});
});
await provider.initialize();
requestCoordinator.resume();
return provider;
};

Expand Down
34 changes: 32 additions & 2 deletions src/packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Providers } from "@ganache/flavors";
import ConnectorLoader from "./src/connector-loader";
import { ProviderOptions, ServerOptions } from "./src/options";
import Server from "./src/server";
Expand All @@ -6,7 +7,36 @@ export { Status } from "./src/server";
export { ProviderOptions, ServerOptions, serverDefaults } from "./src/options";

export default {
/**
* Creates a Ganache server instance that creates and
* serves an underlying Ganache provider. Initialization
* doesn't begin until `server.listen(...)` is called.
* `server.listen(...)` returns a promise that resolves
* when initialization is finished.
*
* @param options Configuration options for the server;
* `options` includes provider based options as well.
* @returns A provider instance for the flavor
* `options.flavor` which defaults to `ethereum`.
*/
server: (options?: ServerOptions) => new Server(options),
provider: (options?: ProviderOptions) =>
ConnectorLoader.initialize(options).provider

/**
* Initializes a Web3 provider for a Ganache instance.
* This function starts an asynchronous task, but does not
* finish it by the time the function returns. Listen to
* `provider.on("connect", () => {...})` or wait for
* `await provider.once("connect")` for initialization to
* finish. You may start sending requests to the provider
* before initialization finishes however; these requests
* will start being consumed after initialization finishes.
*
* @param options Configuration options for the provider.
* @returns A provider instance for the flavor
* `options.flavor` which defaults to `ethereum`.
*/
provider: (options?: ProviderOptions): Providers => {
const connector = ConnectorLoader.initialize(options);
return connector.provider;
}
};
4 changes: 4 additions & 0 deletions src/packages/core/src/connector-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export default {
executor
);

// Purposely not awaiting on this to prevent a breaking change
// to the `Ganache.provider()` method
connector.initialize();

// 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.
connector.on("ready", requestCoordinator.resume);
Expand Down
Loading