diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 0b377a2701..c112eab492 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -53,6 +53,179 @@ import { GanacheRawBlock } from "@ganache/ethereum-block"; import { Capacity } from "./miner/miner"; import { Ethereum } from "./api-types"; +type TransactionSimulationTransaction = Ethereum.Transaction & { + txHash: DATA; + traceTypes: string[]; +}; + +type TransactionSimulationArgs = { + transactions: [TransactionSimulationTransaction[]]; + overrides?: Ethereum.Call.Overrides; + block?: QUANTITY | Ethereum.Tag; +}; + +type Log = [address: Address, topics: DATA[], data: DATA]; +type StorageChange = { + key: Data; + address: Address; + from: Data; + to: Data; +}; +type StateChange = { + address: Data; + from: { + nonce: Quantity; + balance: Quantity; + storageRoot: Data; + codeHash: Data; + }; + to: { + nonce: Quantity; + balance: Quantity; + storageRoot: Data; + codeHash: Data; + }; +}; +type TransactionSimulationResult = { + returnValue: Data; + gas: Quantity; + logs: Log[]; + receipts?: Data[]; + trace?: []; + storageChanges: StorageChange[]; + stateChanges: StateChange[]; +}; + +async function simulateTransaction( + blockchain: Blockchain, + options: EthereumInternalOptions, + transaction: Ethereum.Call.Transaction, + blockNumber: QUANTITY | Ethereum.Tag = Tag.latest, + overrides: Ethereum.Call.Overrides = {} +): Promise<{ + result: any; + storageChanges: Map; + stateChanges: Map< + Buffer, + [[Buffer, Buffer, Buffer, Buffer], [Buffer, Buffer, Buffer, Buffer]] + >; +}> { + // EVMResult + const common = blockchain.common; + const blocks = blockchain.blocks; + const parentBlock = await blocks.get(blockNumber); + const parentHeader = parentBlock.header; + + let gas: Quantity; + if (typeof transaction.gasLimit === "undefined") { + if (typeof transaction.gas !== "undefined") { + gas = Quantity.from(transaction.gas); + } else { + // eth_call isn't subject to regular transaction gas limits by default + gas = options.miner.callGasLimit; + } + } else { + gas = Quantity.from(transaction.gasLimit); + } + + let data: Data; + if (typeof transaction.data === "undefined") { + if (typeof transaction.input !== "undefined") { + data = Data.from(transaction.input); + } + } else { + data = Data.from(transaction.data); + } + + // eth_call doesn't validate that the transaction has a sufficient + // "effectiveGasPrice". however, if `maxPriorityFeePerGas` or + // `maxFeePerGas` values are set, the baseFeePerGas is used to calculate + // the effectiveGasPrice, which is used to calculate tx costs/refunds. + const baseFeePerGasBigInt = parentBlock.header.baseFeePerGas + ? parentBlock.header.baseFeePerGas.toBigInt() + : undefined; + + let gasPrice: Quantity; + const hasGasPrice = typeof transaction.gasPrice !== "undefined"; + // if the original block didn't have a `baseFeePerGas` (baseFeePerGasBigInt + // is undefined) then EIP-1559 was not active on that block and we can't use + // type 2 fee values (as they rely on the baseFee) + if (!common.isActivatedEIP(1559) || baseFeePerGasBigInt === undefined) { + gasPrice = hasGasPrice + ? Quantity.Zero + : Quantity.from(transaction.gasPrice); + } else { + const hasMaxFeePerGas = typeof transaction.maxFeePerGas !== "undefined"; + const hasMaxPriorityFeePerGas = + typeof transaction.maxPriorityFeePerGas !== "undefined"; + + if (hasGasPrice && (hasMaxFeePerGas || hasMaxPriorityFeePerGas)) { + throw new Error( + "both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified" + ); + } + // User specified 1559 gas fields (or none), use those + let maxFeePerGas = 0n; + let maxPriorityFeePerGas = 0n; + if (hasMaxFeePerGas) { + maxFeePerGas = BigInt(transaction.maxFeePerGas); + } + if (hasMaxPriorityFeePerGas) { + maxPriorityFeePerGas = BigInt(transaction.maxPriorityFeePerGas); + } + if (maxPriorityFeePerGas > 0 || maxFeePerGas > 0) { + const a = maxFeePerGas - baseFeePerGasBigInt; + const tip = a < maxPriorityFeePerGas ? a : maxPriorityFeePerGas; + gasPrice = Quantity.from(baseFeePerGasBigInt + tip); + } else { + gasPrice = Quantity.Zero; + } + } + + const incr = + typeof options.miner.timestampIncrement === "string" + ? 12n + : options.miner.timestampIncrement.toBigInt(); + + const block = new RuntimeBlock( + blockchain.common, + Quantity.from(parentHeader.number.toNumber() + 1), + parentBlock.hash(), + blockchain.coinbase, + gas, + parentHeader.gasUsed, + Quantity.from(parentHeader.timestamp.toBigInt() + incr), + Quantity.Zero, //options.miner.difficulty, + parentHeader.totalDifficulty, + blockchain.getMixHash(parentHeader.parentHash.toBuffer()), + baseFeePerGasBigInt, + KECCAK256_RLP + ); + + const simulatedTransaction = { + gas, + // if we don't have a from address, our caller sut be the configured coinbase address + from: + transaction.from == null + ? blockchain.coinbase + : Address.from(transaction.from), + to: transaction.to == null ? null : Address.from(transaction.to), + gasPrice, + value: transaction.value == null ? null : Quantity.from(transaction.value), + data, + block + }; + //const _result = await blockchain.runTransaction(tx, parentBlock, parentBlock); + + const result = await blockchain.simulateTransaction( + simulatedTransaction, + parentBlock, + overrides + ); + + return result; +} + async function autofillDefaultTransactionValues( tx: TypedTransaction, eth_estimateGas: ( @@ -1802,8 +1975,10 @@ export default class EthereumApi implements Api { const addressData = await trie.get(addressBuf); // An address's stateRoot is stored in the 3rd rlp entry const addressStateRoot = decode(addressData)[2]; + trie.setContext(addressStateRoot, addressBuf, blockNum); const value = await trie.get(paddedPosBuff); + return Data.from(decode(value), 32); } @@ -2750,6 +2925,83 @@ export default class EthereumApi implements Api { ); } + /** + * This only simulates the first transaction supplied by args.transactions + * @param {TransactionSimulationArgs} args + * @returns Promise + */ + async evm_simulateTransactions( + args: TransactionSimulationArgs + ): Promise { + // todo: need to be able to pass in multiple transactions + const transaction = args.transactions[0][0]; + const blockNumber = args.block || "latest"; + + const overrides = args.overrides; + //@ts-ignore + const { result, storageChanges, stateChanges, timings } = + await simulateTransaction( + this.#blockchain, + this.#options, + transaction, + blockNumber, + overrides + ); + + const parsedStorageChanges = []; + for (const key of storageChanges.keys()) { + const [contractAddress, from, to] = storageChanges.get(key); + parsedStorageChanges.push({ + key: Data.from(key), + address: Address.from(contractAddress), + from: Data.from(from), + to: Data.from(to) + }); + } + + const parsedStateChanges = []; + for (const address of stateChanges.keys()) { + const [before, after] = stateChanges.get(address); + parsedStateChanges.push({ + address: Data.from(address), + before: { + nonce: Quantity.from(before[0]), + balance: Quantity.from(before[1]), + storageRoot: Data.from(before[2]), + codeHash: Data.from(before[3]) + }, + after: { + nonce: Quantity.from(after[0]), + balance: Quantity.from(after[1]), + storageRoot: Data.from(after[2]), + codeHash: Data.from(after[3]) + } + }); + } + + const returnValue = Data.from(result.returnValue || "0x"); + const gas = Quantity.from(result.executionGasUsed); + const logs = result.logs?.map(([addr, topics, data]) => ({ + address: Data.from(addr), + topics: topics?.map(t => Data.from(t)), + data: Data.from(data) + })); + + return { + returnValue, + gas, + logs, + //todo: populate receipts + receipts: undefined, + //todo: populate trace + trace: undefined, + storageChanges: parsedStorageChanges, + stateChanges: parsedStateChanges, + //@ts-ignore + timings + }; + } + /** * Executes a new message call immediately without creating a transaction on the block chain. * @@ -2806,112 +3058,21 @@ export default class EthereumApi implements Api { blockNumber: QUANTITY | Ethereum.Tag = Tag.latest, overrides: Ethereum.Call.Overrides = {} ): Promise { - const blockchain = this.#blockchain; - const common = blockchain.common; - const blocks = blockchain.blocks; - const parentBlock = await blocks.get(blockNumber); - const parentHeader = parentBlock.header; - const options = this.#options; - - let gas: Quantity; - if (typeof transaction.gasLimit === "undefined") { - if (typeof transaction.gas !== "undefined") { - gas = Quantity.from(transaction.gas); - } else { - // eth_call isn't subject to regular transaction gas limits by default - gas = options.miner.callGasLimit; - } - } else { - gas = Quantity.from(transaction.gasLimit); - } - - let data: Data; - if (typeof transaction.data === "undefined") { - if (typeof transaction.input !== "undefined") { - data = Data.from(transaction.input); - } - } else { - data = Data.from(transaction.data); - } - - // eth_call doesn't validate that the transaction has a sufficient - // "effectiveGasPrice". however, if `maxPriorityFeePerGas` or - // `maxFeePerGas` values are set, the baseFeePerGas is used to calculate - // the effectiveGasPrice, which is used to calculate tx costs/refunds. - const baseFeePerGasBigInt = parentBlock.header.baseFeePerGas - ? parentBlock.header.baseFeePerGas.toBigInt() - : undefined; - - let gasPrice: Quantity; - const hasGasPrice = typeof transaction.gasPrice !== "undefined"; - // if the original block didn't have a `baseFeePerGas` (baseFeePerGasBigInt - // is undefined) then EIP-1559 was not active on that block and we can't use - // type 2 fee values (as they rely on the baseFee) - if (!common.isActivatedEIP(1559) || baseFeePerGasBigInt === undefined) { - gasPrice = Quantity.from(hasGasPrice ? 0 : transaction.gasPrice); - } else { - const hasMaxFeePerGas = typeof transaction.maxFeePerGas !== "undefined"; - const hasMaxPriorityFeePerGas = - typeof transaction.maxPriorityFeePerGas !== "undefined"; - - if (hasGasPrice && (hasMaxFeePerGas || hasMaxPriorityFeePerGas)) { - throw new Error( - "both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified" - ); - } - // User specified 1559 gas fields (or none), use those - let maxFeePerGas = 0n; - let maxPriorityFeePerGas = 0n; - if (hasMaxFeePerGas) { - maxFeePerGas = BigInt(transaction.maxFeePerGas); - } - if (hasMaxPriorityFeePerGas) { - maxPriorityFeePerGas = BigInt(transaction.maxPriorityFeePerGas); - } - if (maxPriorityFeePerGas > 0 || maxFeePerGas > 0) { - const a = maxFeePerGas - baseFeePerGasBigInt; - const tip = a < maxPriorityFeePerGas ? a : maxPriorityFeePerGas; - gasPrice = Quantity.from(baseFeePerGasBigInt + tip); - } else { - gasPrice = Quantity.from(0); - } - } - - const block = new RuntimeBlock( - blockchain.common, - parentHeader.number, - parentHeader.parentHash, - blockchain.coinbase, - gas, - parentHeader.gasUsed, - parentHeader.timestamp, - options.miner.difficulty, - parentHeader.totalDifficulty, - blockchain.getMixHash(parentHeader.parentHash.toBuffer()), - baseFeePerGasBigInt, - KECCAK256_RLP - ); - - const simulatedTransaction = { - gas, - // if we don't have a from address, our caller sut be the configured coinbase address - from: - transaction.from == null - ? blockchain.coinbase - : Address.from(transaction.from), - to: transaction.to == null ? null : Address.from(transaction.to), - gasPrice, - value: - transaction.value == null ? null : Quantity.from(transaction.value), - data, - block - }; - - return blockchain.simulateTransaction( - simulatedTransaction, - parentBlock, + const { result } = await simulateTransaction( + this.#blockchain, + this.#options, + transaction, + blockNumber, overrides ); + + console.log({ + keys: Object.keys(result), + returnValue: result.returnValue, + logs: result.logs, + evmException: result.exceptionError + }); + return Data.from(result.returnValue || "0x"); } /** diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index aec91df3b6..e09d9230cb 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -78,6 +78,7 @@ import { GanacheStateManager } from "./state-manager"; import { TrieDB } from "./trie-db"; import { Trie } from "@ethereumjs/trie"; import { removeEIP3860InitCodeSizeLimitCheck } from "./helpers/common-helpers"; +import { bigIntToBuffer } from "@ganache/utils"; const mclInitPromise = mcl.init(mcl.BLS12_381).then(() => { mcl.setMapToMode(mcl.IRTF); // set the right map mode; otherwise mapToG2 will return wrong values. @@ -1106,8 +1107,18 @@ export default class Blockchain extends Emittery { parentBlock: Block, overrides: CallOverrides ) { - let result: EVMResult; + const { header } = transaction.block; + + const timings: { time: number; label: string }[] = []; + timings.push({ time: performance.now(), label: "start" }); + + let result: EVMResult; + const storageChanges = new Map(); + const stateChanges = new Map< + Buffer, + [[Buffer, Buffer, Buffer, Buffer], [Buffer, Buffer, Buffer, Buffer]] + >(); const data = transaction.data; let gasLimit = transaction.gas.toBigInt(); // subtract out the transaction's base fee from the gas limit before @@ -1115,15 +1126,18 @@ export default class Blockchain extends Emittery { const hasToAddress = transaction.to != null; const to = hasToAddress ? new Address(transaction.to.toBuffer()) : null; + //todo: getCommonForBlockNumber doesn't presently respect shanghai, so we just assume it's the same common as the fork + // this won't work as expected if simulating on blocks before shanghai. const common = this.fallback ? this.fallback.getCommonForBlockNumber( this.common, BigInt(transaction.block.header.number.toString()) ) : this.common; + common.setHardfork("shanghai"); - const gasLeft = - gasLimit - calculateIntrinsicGas(data, hasToAddress, common); + const intrinsicGas = calculateIntrinsicGas(data, hasToAddress, common); + const gasLeft = gasLimit - intrinsicGas; const transactionContext = {}; this.emit("ganache:vm:tx:before", { @@ -1145,24 +1159,54 @@ export default class Blockchain extends Emittery { false, // precompiles have already been initialized in the stateTrie common ); - + //console.log({ stateRoot: await vm.stateManager.getStateRoot() }); + const stateManager = vm.stateManager as GanacheStateManager; // take a checkpoint so the `runCall` never writes to the trie. We don't // commit/revert later because this stateTrie is ephemeral anyway. await vm.eei.checkpoint(); - - vm.evm.events.on("step", (event: InterpreterStep) => { - const logs = maybeGetLogs(event); - if (logs) { - options.logging.logger.log(...logs); - this.emit("ganache:vm:tx:console.log", { - context: transactionContext, - logs - }); + vm.evm.events.on("step", async (event: InterpreterStep) => { + if ( + event.opcode.name === "CALL" || + event.opcode.name === "DELEGATECALL" || + event.opcode.name === "STATICCALL" || + event.opcode.name === "JUMP" + ) { + //console.log(event.opcode.name); } - if (!this.#emitStepEvent) return; - const ganacheStepEvent = makeStepEvent(transactionContext, event); - this.emit("ganache:vm:tx:step", ganacheStepEvent); + if (event.opcode.name === "SSTORE") { + const stackLength = event.stack.length; + const keyBigInt = event.stack[stackLength - 1]; + const key = + keyBigInt === 0n + ? BUFFER_32_ZERO + : // todo: this isn't super efficient, but :shrug: we probably don't do it often + Data.toBuffer(bigIntToBuffer(keyBigInt), 32); + const valueBigInt = event.stack[stackLength - 2]; + + const value = Data.toBuffer(bigIntToBuffer(valueBigInt), 32); + // todo: DELEGATE_CALL might impact the address context from which the `before` value should be fetched + + const storageTrie = await stateManager.getStorageTrie( + event.codeAddress.toBuffer() + ); + + const from = decode(await storageTrie.get(key)); + + /*console.log({ + SSTORE_refund: event.gasRefund, + address: Data.from(event.codeAddress.toBuffer()), + key: Data.from(key), + from: Data.from(from), + to: Data.from(value) + });*/ + + storageChanges.set(key, [ + event.codeAddress.toBuffer(), + from.length === 0 ? Buffer.alloc(32) : Data.toBuffer(from, 32), + value + ]); + } }); const caller = transaction.from.toBuffer(); @@ -1189,14 +1233,20 @@ export default class Blockchain extends Emittery { // we run this transaction so that things that rely on these values // are correct (like contract creation!). const fromAccount = await vm.eei.getAccount(callerAddress); - fromAccount.nonce += 1n; - const txCost = gasLimit * transaction.gasPrice.toBigInt(); + + // todo: re previous comment, incrementing the nonce here results in a double + // incremented nonce in the result :/ Need to validate whether this is required. + //fromAccount.nonce += 1n; + const intrinsicTxCost = intrinsicGas * transaction.gasPrice.toBigInt(); + //todo: does the execution gas get subtracted from the balance? const startBalance = fromAccount.balance; // TODO: should we throw if insufficient funds? - fromAccount.balance = txCost > startBalance ? 0n : startBalance - txCost; + fromAccount.balance = + intrinsicTxCost > startBalance ? 0n : startBalance - intrinsicTxCost; await vm.eei.putAccount(callerAddress, fromAccount); - // finally, run the call + timings.push({ time: performance.now(), label: "running transaction" }); + result = await vm.evm.runCall({ caller: callerAddress, data: transaction.data && transaction.data.toBuffer(), @@ -1206,6 +1256,48 @@ export default class Blockchain extends Emittery { value: transaction.value == null ? 0n : transaction.value.toBigInt(), block: transaction.block as any }); + timings.push({ + time: performance.now(), + label: "finished running transaction" + }); + + const afterCache = stateManager["_cache"]["_cache"] as any; // OrderedMap + + const asyncAccounts: Promise[] = []; + + afterCache.forEach(i => { + asyncAccounts.push( + new Promise(async resolve => { + const addressBuf = Buffer.from(i[0], "hex"); + const beforeAccount = await this.vm.stateManager.getAccount( + Address.from(addressBuf) + ); + + // todo: it's a shame to serialize here - should get the raw address directly. + const beforeRaw = beforeAccount.serialize(); + if (!beforeRaw.equals(i[1].val)) { + // the account has changed + const address = Buffer.from(i[0], "hex"); + const after = decode(i[1].val); + const before = [ + Quantity.toBuffer(beforeAccount.nonce), + Quantity.toBuffer(beforeAccount.balance), + beforeAccount.storageRoot, + beforeAccount.codeHash + ] as EthereumRawAccount; + stateChanges.set(address, [before, after]); + } + resolve(); + }) + ); + }); + + await Promise.all(asyncAccounts); + + timings.push({ + time: performance.now(), + label: "finished building state diff" + }); } else { result = { execResult: { @@ -1215,13 +1307,49 @@ export default class Blockchain extends Emittery { } } as EVMResult; } + this.emit("ganache:vm:tx:after", { context: transactionContext }); if (result.execResult.exceptionError) { throw new CallError(result); } else { - return Data.from(result.execResult.returnValue || "0x"); + const totalGasSpent = result.execResult.executionGasUsed + intrinsicGas; + const maxRefund = totalGasSpent / 5n; + const actualRefund = + result.execResult.gasRefund > maxRefund + ? maxRefund + : result.execResult.gasRefund; + + /*console.log({ + totalGasSpent, + execGas: result.execResult.executionGasUsed, + maxRefund, + intrinsicGas, + refund: result.execResult.gasRefund, + actualRefund + });*/ + + //todo: we are treating the property "executionGasUsed" as the total gas + // cost, which it is not. Probably should derive a return object here, + // rather than just using the object returned from the EVM. + result.execResult.executionGasUsed = + (result.execResult.executionGasUsed || 0n) + + intrinsicGas - + actualRefund; + + const startTime = timings[0].time; + const timingsSummary = timings.map(({ time, label }) => ({ + label, + duration: time - startTime + })); + + return { + result: result.execResult, + storageChanges, + stateChanges, + timings: timingsSummary + }; } } diff --git a/src/chains/ethereum/ethereum/src/connector.ts b/src/chains/ethereum/ethereum/src/connector.ts index d38dc48f80..0ea59a8cb0 100644 --- a/src/chains/ethereum/ethereum/src/connector.ts +++ b/src/chains/ethereum/ethereum/src/connector.ts @@ -106,27 +106,29 @@ export class Connector< format( result: any, - payload: R + payload: R, + durationMs?: number ): RecognizedString | Generator; - format(result: any, payload: R): RecognizedString; - format(results: any[], payloads: R[]): RecognizedString; + format(result: any, payload: R, durationMs?: number): RecognizedString; + format(results: any[], payloads: R[], durationMs?: number): RecognizedString; format( results: any | any[], - payload: R | R[] + payload: R | R[], + durationMs?: number ): RecognizedString | Generator { if (Array.isArray(payload)) { return JSON.stringify( payload.map((payload, i) => { const result = results[i]; if (result instanceof Error) { - return makeError(payload.id, result as any); + return makeError(payload.id, result as any, durationMs); } else { - return makeResponse(payload.id, result); + return makeResponse(payload.id, result, durationMs); } }) ); } else { - const json = makeResponse(payload.id, results); + const json = makeResponse(payload.id, results, durationMs); if ( payload.method === "debug_traceTransaction" && typeof results === "object" && @@ -159,8 +161,18 @@ export class Connector< } } - formatError(error: Error & { code: number }, payload: R): RecognizedString { - const json = makeError(payload && payload.id ? payload.id : null, error); + formatError( + error: Error & { code: number }, + payload: R, + durationMs?: number + ): RecognizedString { + console.log("Formatting error", durationMs); + const json = makeError( + payload && payload.id ? payload.id : null, + error, + undefined, + durationMs + ); return JSON.stringify(json); } diff --git a/src/chains/ethereum/ethereum/src/forking/handlers/base-handler.ts b/src/chains/ethereum/ethereum/src/forking/handlers/base-handler.ts index 7edc82fda1..2537de7e49 100644 --- a/src/chains/ethereum/ethereum/src/forking/handlers/base-handler.ts +++ b/src/chains/ethereum/ethereum/src/forking/handlers/base-handler.ts @@ -164,18 +164,17 @@ export class BaseHandler { method: string, params: any[], key: string, - send: ( - ...args: unknown[] - ) => Promise<{ + send: (...args: unknown[]) => Promise<{ response: { result: any } | { error: { message: string; code: number } }; raw: string | Buffer; }>, options = { disableCache: false } ): Promise { + const memCached = this.getFromMemCache(key); + if (memCached !== undefined) { + return memCached; + } if (!options.disableCache) { - const memCached = this.getFromMemCache(key); - if (memCached !== undefined) return memCached; - const diskCached = await this.getFromSlowCache(method, params, key); if (diskCached !== undefined) { this.valueCache.set(key, Buffer.from(diskCached.raw)); @@ -189,33 +188,30 @@ export class BaseHandler { if (this.abortSignal.aborted) return Promise.reject(new AbortError()); if (hasOwn(response, "result")) { - if (!options.disableCache) { - // cache non-error responses only - this.valueCache.set(key, raw); - + // cache non-error responses only + this.valueCache.set(key, raw); + if (!options.disableCache && this.persistentCache) { // swallow errors for the persistentCache, since it's not vital that // it always works - if (this.persistentCache) { - const prom = this.persistentCache - .put( - method, - params, - key, - typeof raw === "string" ? Buffer.from(raw) : raw - ) - .catch(_ => { - // the cache.put may fail if the db is closed while a request - // is in flight. This is a "fire and forget" method. - }); - - // track these unawaited `puts` - this.fireForget.add(prom); - - // clean up once complete - prom.finally(() => { - this.fireForget.delete(prom); + const prom = this.persistentCache + .put( + method, + params, + key, + typeof raw === "string" ? Buffer.from(raw) : raw + ) + .catch(_ => { + // the cache.put may fail if the db is closed while a request + // is in flight. This is a "fire and forget" method. }); - } + + // track these unawaited `puts` + this.fireForget.add(prom); + + // clean up once complete + prom.finally(() => { + this.fireForget.delete(prom); + }); } return response.result as T; diff --git a/src/chains/ethereum/ethereum/src/forking/trie.ts b/src/chains/ethereum/ethereum/src/forking/trie.ts index f7a3fc3914..eec815cd5d 100644 --- a/src/chains/ethereum/ethereum/src/forking/trie.ts +++ b/src/chains/ethereum/ethereum/src/forking/trie.ts @@ -162,7 +162,7 @@ export class ForkTrie extends GanacheTrie { // the fork block because we can't possibly delete keys _before_ the fork // block, since those happened before ganache was even started // This little optimization can cut debug_traceTransaction time _in half_. - if (!this.isPreForkBlock) { + if (true || !this.isPreForkBlock) { const delKey = this.createDelKey(key); const metaDataPutPromise = this.checkpointedMetadata.put( delKey, @@ -277,7 +277,7 @@ export class ForkTrie extends GanacheTrie { // the fork block because we can't possibly delete keys _before_ the fork // block, since those happened before ganache was even started // This little optimization can cut debug_traceTransaction time _in half_. - if (!this.isPreForkBlock && (await this.keyWasDeleted(key))) return null; + if (await this.keyWasDeleted(key)) return null; if (this.address === null) { // if the trie context's address isn't set, our key represents an address: diff --git a/src/chains/ethereum/ethereum/src/provider.ts b/src/chains/ethereum/ethereum/src/provider.ts index 0d56db56f6..0ae81acd01 100644 --- a/src/chains/ethereum/ethereum/src/provider.ts +++ b/src/chains/ethereum/ethereum/src/provider.ts @@ -467,7 +467,7 @@ export class EthereumProvider const result = await this.request({ method, params }); return { error: null as JsonRpcError, - result: makeResponse(payload.id, JSON.parse(JSON.stringify(result))) + result: makeResponse(payload.id, JSON.parse(JSON.stringify(result)), 0) }; } catch (error: any) { let result: any; diff --git a/src/chains/ethereum/options/src/fork-options.ts b/src/chains/ethereum/options/src/fork-options.ts index 407cab4571..aca5377ae1 100644 --- a/src/chains/ethereum/options/src/fork-options.ts +++ b/src/chains/ethereum/options/src/fork-options.ts @@ -188,7 +188,7 @@ export type ForkConfig = { }; /** - * Disables caching of all forking requests. + * Disables (persistent) caching of all forking requests. * * @defaultValue false */ @@ -477,7 +477,7 @@ Defaults to: \`["User-Agent: Ganache/VERSION (https://www.trufflesuite.com/ganac disableCache: { normalize, default: () => false, - cliDescription: "Disables caching of all forking requests.", + cliDescription: "Disables (persistent) caching of all forking requests.", cliType: "boolean" }, deleteCache: { diff --git a/src/chains/filecoin/filecoin/src/connector.ts b/src/chains/filecoin/filecoin/src/connector.ts index 6d7db10bc9..046b10a656 100644 --- a/src/chains/filecoin/filecoin/src/connector.ts +++ b/src/chains/filecoin/filecoin/src/connector.ts @@ -62,7 +62,7 @@ export class Connector< } format(result: any, payload: R): RecognizedString { - const json = makeResponse(payload.id, result); + const json = makeResponse(payload.id, result, 1); return JSON.stringify(json); } diff --git a/src/packages/core/src/servers/http-server.ts b/src/packages/core/src/servers/http-server.ts index 0f6a67e5a9..1cd6704af9 100644 --- a/src/packages/core/src/servers/http-server.ts +++ b/src/packages/core/src/servers/http-server.ts @@ -197,6 +197,8 @@ export default class HttpServer { } #handlePost = (response: HttpResponse, request: HttpRequest) => { + const startTime = performance.now(); + // handle JSONRPC post requests... const writeHeaders = prepareCORSResponseHeaders("POST", request); @@ -241,7 +243,12 @@ export default class HttpServer { // cause an `Unhandled promise rejection` if we try) return; } - const data = connector.format(result, payload); + const endTime = performance.now(); + const data = connector.format( + result, + payload, + Math.floor(endTime - startTime) + ); if (types.isGeneratorObject(data)) { sendChunkedResponse( response, @@ -269,7 +276,13 @@ export default class HttpServer { // cause an `Unhandled promise rejection` if we try) return; } - const data = connector.formatError(error, payload); + const endTime = performance.now(); + + const data = connector.formatError( + error, + payload, + Math.floor(endTime - startTime) + ); sendResponse( response, this.#isClosing, diff --git a/src/packages/ganache/package.json b/src/packages/ganache/package.json index 4288d682e9..f2c81581cf 100644 --- a/src/packages/ganache/package.json +++ b/src/packages/ganache/package.json @@ -1,6 +1,6 @@ { "name": "ganache", - "version": "7.8.0", + "version": "7.8.0-transaction-simulation", "description": "A library and cli to create a local blockchain for fast Ethereum development.", "author": "David Murdoch", "homepage": "https://github.com/trufflesuite/ganache/tree/develop/src/packages/ganache#readme", diff --git a/src/packages/utils/src/things/jsonrpc.ts b/src/packages/utils/src/things/jsonrpc.ts index 2004a401db..d7d22c9a7a 100644 --- a/src/packages/utils/src/things/jsonrpc.ts +++ b/src/packages/utils/src/things/jsonrpc.ts @@ -17,6 +17,7 @@ export type JsonRpcRequest< }; export type JsonRpcResponse = JsonRpc & { readonly result: any; + readonly durationMs: number; }; export type JsonRpcError = JsonRpc & { readonly error: { @@ -25,6 +26,7 @@ export type JsonRpcError = JsonRpc & { readonly message: any; }; readonly result?: any; + readonly durationMs: number; }; const jsonrpc = "2.0" as const; @@ -41,17 +43,23 @@ export const makeRequest = < params: json.params }; }; -export const makeResponse = (id: string, result: any): JsonRpcResponse => { +export const makeResponse = ( + id: string, + result: any, + durationMs: number +): JsonRpcResponse => { return { id, jsonrpc, - result + result, + durationMs }; }; export const makeError = ( id: string | undefined, error: T, - result?: unknown + result?: unknown, + durationMs?: number ): JsonRpcError => { type E = { [K in keyof T]: K extends string ? T[K] : never }; // Error objects are weird, `message` isn't included in the property names, @@ -70,13 +78,15 @@ export const makeError = ( id, jsonrpc, error: details, - result + result, + durationMs: durationMs }; } else { return { id, jsonrpc, - error: details + error: details, + durationMs: durationMs }; } };