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

feat: replace legacyInstamine option with instamine=eager|strict #2013

Merged
merged 16 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions src/chains/ethereum/ethereum/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,13 +305,15 @@ export default class EthereumApi implements Api {
arg?: number | { timestamp?: number; blocks?: number }
): Promise<"0x0"> {
const blockchain = this.#blockchain;
const vmErrorsOnRPCResponse = this.#options.chain.vmErrorsOnRPCResponse;
const options = this.#options;
const vmErrorsOnRPCResponse = options.chain.vmErrorsOnRPCResponse;
// Since `typeof null === "object"` we have to guard against that
if (arg !== null && typeof arg === "object") {
let { blocks, timestamp } = arg;
if (blocks == null) {
blocks = 1;
}
const strictMiner = options.miner.instamine === "strict";
// TODO(perf): add an option to mine a bunch of blocks in a batch so
// we can save them all to the database in one go.
// Developers like to move the blockchain forward by thousands of blocks
Expand All @@ -322,15 +324,20 @@ export default class EthereumApi implements Api {
timestamp,
true
);
// wait until the blocks are fully saved before mining the next ones
await new Promise(resolve => {
const off = blockchain.on("block", block => {
if (block.header.number.toBuffer().equals(blockNumber)) {
off();
resolve(void 0);
}

if (strictMiner) {
// in strict mode we have to wait until the blocks are fully saved
// before mining the next ones, in eager mode they've already been
// saved
await new Promise(resolve => {
const off = blockchain.on("block", ({ header: { number } }) => {
if (number.toBuffer().equals(blockNumber)) {
off();
resolve(void 0);
}
});
});
});
}
if (vmErrorsOnRPCResponse) {
assertExceptionalTransactions(transactions);
}
Expand Down Expand Up @@ -594,7 +601,7 @@ export default class EthereumApi implements Api {
*/
@assertArgLength(0, 1)
async miner_start(threads: number = 1) {
if (this.#options.miner.legacyInstamine === true) {
if (this.#options.miner.instamine === "eager") {
const resumption = await this.#blockchain.resume(threads);
// resumption can be undefined if the blockchain isn't currently paused
if (
Expand Down Expand Up @@ -1646,12 +1653,12 @@ export default class EthereumApi implements Api {
return receipt.toJSON(block, transaction, common);
}

// if we are performing non-legacy instamining, then check to see if the
// transaction is pending so as to warn about the v7 breaking change
// if we are performing "strict" instamining, then check to see if the
// transaction is pending so as to warn about the v7 instamine changes
const options = this.#options;
if (
options.miner.blockTime <= 0 &&
cds-amal marked this conversation as resolved.
Show resolved Hide resolved
options.miner.legacyInstamine !== true &&
options.miner.instamine === "strict" &&
this.#blockchain.isStarted()
) {
const tx = this.#blockchain.transactions.transactionPool.find(txHash);
Expand Down
78 changes: 27 additions & 51 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export type BlockchainOptions = {
coinbase: Account;
chainId: number;
common: Common;
legacyInstamine: boolean;
instamine: "eager" | "strict";
vmErrorsOnRPCResponse: boolean;
logger: Logger;
};
Expand Down Expand Up @@ -215,36 +215,8 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {

this.#options = options;
this.fallback = fallback;

const instamine = (this.#instamine =
!options.miner.blockTime || options.miner.blockTime <= 0);
const legacyInstamine = options.miner.legacyInstamine;

{
// warnings and errors
if (legacyInstamine) {
console.info(
"Legacy instamining, where transactions are fully mined before the hash is returned, is deprecated and will be removed in the future."
);
}

if (!instamine) {
if (legacyInstamine) {
console.info(
"Setting `legacyInstamine` to `true` has no effect when blockTime is non-zero"
);
}

if (options.chain.vmErrorsOnRPCResponse) {
console.info(
"Setting `vmErrorsOnRPCResponse` to `true` has no effect on transactions when blockTime is non-zero"
);
}
}
}

this.coinbase = coinbase;

this.#instamine = !options.miner.blockTime || options.miner.blockTime <= 0;
this.#database = new Database(options.database, this);
}

Expand Down Expand Up @@ -339,9 +311,8 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
options.miner.blockGasLimit,
initialAccounts
);
blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then(
({ block }) => block
);
blocks.earliest = blocks.latest =
await this.#blockBeingSavedPromise.then(({ block }) => block);
Comment on lines +314 to +315
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(prettier change)

}
}

Expand Down Expand Up @@ -476,6 +447,9 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
});
};

/**
* Emit the block now that everything has been fully saved to the database
*/
#emitNewBlock = async (blockInfo: {
block: Block;
blockLogs: BlockLogs;
Expand All @@ -484,15 +458,21 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
const options = this.#options;
const { block, blockLogs, transactions } = blockInfo;

// emit the block once everything has been fully saved to the database
transactions.forEach(transaction => {
transaction.finalize("confirmed", transaction.execException);
});

if (this.#instamine && options.miner.legacyInstamine) {
// in legacy instamine mode we must delay the broadcast of new blocks
if (this.#instamine && options.miner.instamine === "eager") {
// in eager instamine mode we must delay the broadcast of new blocks
await new Promise(resolve => {
process.nextTick(async () => {
// we delay emitting blocks and blockLogs because we need to allow for:
// ```
// await provider.request({"method": "eth_sendTransaction"...)
// await provider.once("message") // <- should work
// ```
// If we don't have this delay here the messages will be sent before
// the call has a chance to listen to the event.
setImmediate(async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out this was broken in greedy mode!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is broken?

Copy link
Member Author

@davidmurdoch davidmurdoch Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delay wasn't working. Our existing tests caught it when I switched the default.

await provider.send("eth_sendTransaction", ...);
await provider.once("message"); // <- this wasn't work, but it should.

// emit block logs first so filters can pick them up before
// block listeners are notified
await Promise.all([
Expand Down Expand Up @@ -975,11 +955,11 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
if (this.#isPaused() || !this.#instamine) {
return hash;
} else {
if (this.#instamine && this.#options.miner.legacyInstamine) {
// in legacyInstamine mode we must wait for the transaction to be saved
if (this.#instamine && this.#options.miner.instamine === "eager") {
// in eager instamine mode we must wait for the transaction to be saved
// before we can return the hash
const { status, error } = await transaction.once("finalized");
// in legacyInstamine mode we must throw on all rejected transaction
// in eager instamine mode we must throw on all rejected transaction
// errors. We must also throw on `confirmed` transactions when
// vmErrorsOnRPCResponse is enabled.
if (
Expand Down Expand Up @@ -1438,17 +1418,13 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
);

// #3 - Rerun every transaction in block prior to and including the requested transaction
const {
gas,
structLogs,
returnValue,
storage
} = await this.#traceTransaction(
newBlock.transactions[transaction.index.toNumber()],
trie,
newBlock,
options
);
const { gas, structLogs, returnValue, storage } =
await this.#traceTransaction(
newBlock.transactions[transaction.index.toNumber()],
trie,
newBlock,
options
);
Comment on lines +1421 to +1427
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(prettier change)


// #4 - Send results back
return { gas, structLogs, returnValue, storage };
Expand Down
9 changes: 1 addition & 8 deletions src/chains/ethereum/ethereum/src/miner/miner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ export default class Miner extends Emittery<{

let keepMining = true;
const priced = this.#priced;
const legacyInstamine = this.#options.legacyInstamine;
const storageKeys: StorageKeys = new Map();
let blockTransactions: TypedTransaction[];
do {
Expand Down Expand Up @@ -424,13 +423,7 @@ export default class Miner extends Emittery<{
storageKeys
);
block = finalizedBlockData.block;
const emitBlockProm = this.emit("block", finalizedBlockData);
if (legacyInstamine === true) {
// we need to wait for each block to be done mining when in legacy
// mode because things like `mine` and `miner_start` must wait for the
// first mine operation to be fully complete.
await emitBlockProm;
}
this.emit("block", finalizedBlockData);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to wait because evm_mine and miner_start wait on their own.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you feel it necessary to explain this in the review it likely belongs as a comment in the code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It explains why the old code was removed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually having to revisit this because I noticed if someone sends stops the miner via miner_stop, send enough transactions to fill two blocks, then calls miner_start, all pending transactions aren't mined before miner_start returns... which is what we want.

I think evm_mine suffers from a similar issue, but I haven't yet figured out what yet.

These issues might be rare enough that I can wait until after v7 stable to fix them, as it might take a bit of refactoring to get the timings correct.


if (onlyOneBlock) {
this.#currentlyExecutingPrice = 0n;
Expand Down
7 changes: 5 additions & 2 deletions src/chains/ethereum/ethereum/tests/api/eth/eth.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { RPCQUANTITY_GWEI } from "@ganache/utils";
import assert from "assert";
import EthereumProvider from "../../../src/provider";
import getProvider from "../../helpers/getProvider";
import getProvider, { mnemonic } from "../../helpers/getProvider";

function hex(length: number) {
return `0x${Buffer.allocUnsafe(length).fill(0).toString("hex")}`;
Expand All @@ -13,7 +13,10 @@ describe("api", () => {
let accounts: string[];

beforeEach(async () => {
provider = await getProvider();
provider = await getProvider({
miner: { instamine: "strict" },
wallet: { mnemonic }
});

accounts = await provider.send("eth_accounts");
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import getProvider from "../../helpers/getProvider";
import getProvider, { mnemonic } from "../../helpers/getProvider";
import assert from "assert";
import EthereumProvider from "../../../src/provider";

Expand All @@ -21,7 +21,10 @@ describe("api", () => {
}
}
};
provider = await getProvider({ logging: { logger } });
provider = await getProvider({
logging: { logger },
wallet: { mnemonic }
});
[from] = await provider.send("eth_accounts");
});

Expand Down Expand Up @@ -61,16 +64,23 @@ describe("api", () => {
);
});

describe("legacy instamine detection and notice", () => {
describe("strict instamine detection and notice", () => {
it("logs a warning if the transaction hasn't been mined yet", async () => {
const hash = await provider.send("eth_sendTransaction", [
{ from, to: from }
]);
const strictInstamineProvider = await getProvider({
logging: { logger },
miner: { instamine: "strict" },
wallet: { mnemonic }
});
const hash = await strictInstamineProvider.send(
"eth_sendTransaction",
[{ from, to: from }]
);

// do not wait for the tx to be mined which will create a warning
const result = await provider.send("eth_getTransactionReceipt", [
hash
]);
const result = await strictInstamineProvider.send(
"eth_getTransactionReceipt",
[hash]
);
Comment on lines +80 to +83
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(prettier change)


assert.strictEqual(result, null);
assert(
Expand Down Expand Up @@ -125,26 +135,26 @@ describe("api", () => {
);
});

it("doesn't log if legacyInstamine is enabled", async () => {
const legacyInstamineProvider = await getProvider({
it("doesn't log if instamine is set to 'eager' (default)", async () => {
const eagerInstamineProvider = await getProvider({
logging: { logger },
miner: { legacyInstamine: true }
miner: { instamine: "eager" }
});

const [from] = await legacyInstamineProvider.send("eth_accounts");
const [from] = await eagerInstamineProvider.send("eth_accounts");

const hash = await legacyInstamineProvider.send(
const hash = await eagerInstamineProvider.send(
"eth_sendTransaction",
[{ from, to: from }]
);

const result = await legacyInstamineProvider.send(
const result = await eagerInstamineProvider.send(
"eth_getTransactionReceipt",
[hash]
);

// the tx is mined before sending the tx hash back to the user
// if legacyInstamine is enabled - so they will get a receipt
// if instamine is set to 'eager' - so they will get a receipt
assert(result);
assert(
!logger.loggedStuff.includes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import assert from "assert";

describe("api", () => {
describe("eth", () => {
describe("legacy", () => {
it("when not in legacy mode, does not mine before returning the tx hash", async () => {
describe("eager", () => {
it("when in strict instamine mode, does not mine before returning the tx hash", async () => {
const provider = await getProvider({
miner: { legacyInstamine: false }
miner: { instamine: "strict" }
});
const accounts = await provider.send("eth_accounts");

Expand All @@ -23,9 +23,9 @@ describe("api", () => {
assert.strictEqual(receipt, null);
});

it("when in legacy mode, mines before returns in the tx hash", async () => {
it("when in eager instamine mode, mines before returns in the tx hash", async () => {
const provider = await getProvider({
miner: { legacyInstamine: true }
miner: { instamine: "eager" }
});
const accounts = await provider.send("eth_accounts");

Expand All @@ -44,7 +44,7 @@ describe("api", () => {

it("handles transaction balance errors, callback style", done => {
getProvider({
miner: { legacyInstamine: true },
miner: { instamine: "eager" },
chain: { vmErrorsOnRPCResponse: true }
}).then(async provider => {
const [from, to] = await provider.send("eth_accounts");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("api", () => {
it("returns a VM error when the account has insufficient funds to transfer the value at runtime", async () => {
const approximateGasCost = 99967968750001;
const provider = await getProvider({
miner: { legacyInstamine: true },
miner: { instamine: "eager" },
chain: { vmErrorsOnRPCResponse: true }
});
const accounts = await provider.send("eth_accounts");
Expand Down
Loading