From 82ac305675540c8cc6b07fb226bb633bbc3e58fb Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 27 Apr 2023 15:05:50 +1200 Subject: [PATCH 01/15] initial commit of transaction simulation endpoint --- src/chains/ethereum/ethereum/src/api.ts | 289 +++++++++++------- .../ethereum/ethereum/src/blockchain.ts | 2 +- 2 files changed, 186 insertions(+), 105 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 0b377a2701..20b0a29880 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -53,6 +53,141 @@ 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 TransactionSimulationResult = { + returnValue: Data; + gas: Quantity; + logs: Log[]; + receipts?: Data[]; + trace?: []; +}; + +async function simulateTransaction( + blockchain: Blockchain, + options: EthereumInternalOptions, + transaction: Ethereum.Call.Transaction, + blockNumber: QUANTITY | Ethereum.Tag = Tag.latest, + overrides: Ethereum.Call.Overrides = {} +): Promise { + // 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 = 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 + }; + + const result = await blockchain.simulateTransaction( + simulatedTransaction, + parentBlock, + overrides + ); + return result; +} + async function autofillDefaultTransactionValues( tx: TypedTransaction, eth_estimateGas: ( @@ -2750,6 +2885,43 @@ export default class EthereumApi implements Api { ); } + /** + * This only simulates the first transaction supplied by args.transactions + * @param {TransactionSimulationArgs} args + * @returns Promise + */ + async evm_simulateTransaction( + args: TransactionSimulationArgs + ): Promise { + const transaction = args.transactions[0]; + const blockNumber = args.block || "latest"; + const overrides = args.overrides; + + const result = await simulateTransaction( + this.#blockchain, + this.#options, + transaction, + blockNumber, + overrides + ); + + const returnValue = Data.from(result.returnValue || "0x"); + const gas = Quantity.from(result.gas); + const logs = result.logs?.map(([addr, topics, data]) => ({ + address: Data.from(addr), + topics: topics?.map(Data.from), + data: Data.from(data) + })); + + return { + returnValue, + gas, + logs, + receipts: undefined, + trace: undefined + }; + } + /** * Executes a new message call immediately without creating a transaction on the block chain. * @@ -2806,112 +2978,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..0130afcdc9 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1221,7 +1221,7 @@ export default class Blockchain extends Emittery { if (result.execResult.exceptionError) { throw new CallError(result); } else { - return Data.from(result.execResult.returnValue || "0x"); + return result.execResult; } } From 61f5b44a709fc8d273fb7db7ffef576d2096539e Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:33:21 +1200 Subject: [PATCH 02/15] Add state changes, fix logs --- src/chains/ethereum/ethereum/src/api.ts | 37 ++++++++++++++++--- .../ethereum/ethereum/src/blockchain.ts | 28 ++++++++++++-- src/packages/ganache/package.json | 2 +- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 20b0a29880..eac0a4fa3f 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -65,13 +65,19 @@ type TransactionSimulationArgs = { }; type Log = [address: Address, topics: DATA[], data: DATA]; - +type StateChange = { + key: Data; + contractAddress: Address; + from: Data; + to: Data; +}; type TransactionSimulationResult = { returnValue: Data; gas: Quantity; logs: Log[]; receipts?: Data[]; trace?: []; + stateChanges: StateChange[]; }; async function simulateTransaction( @@ -80,7 +86,10 @@ async function simulateTransaction( transaction: Ethereum.Call.Transaction, blockNumber: QUANTITY | Ethereum.Tag = Tag.latest, overrides: Ethereum.Call.Overrides = {} -): Promise { +): Promise<{ + result: any; + stateChanges: Map; +}> { // EVMResult const common = blockchain.common; const blocks = blockchain.blocks; @@ -185,6 +194,7 @@ async function simulateTransaction( parentBlock, overrides ); + return result; } @@ -2893,11 +2903,12 @@ export default class EthereumApi implements Api { async evm_simulateTransaction( args: TransactionSimulationArgs ): Promise { + // todo: need to be able to pass in multiple transactions const transaction = args.transactions[0]; const blockNumber = args.block || "latest"; const overrides = args.overrides; - const result = await simulateTransaction( + const { result, stateChanges } = await simulateTransaction( this.#blockchain, this.#options, transaction, @@ -2905,11 +2916,22 @@ export default class EthereumApi implements Api { overrides ); + const changes = []; + for (const key of stateChanges.keys()) { + const [contractAddress, from, to] = stateChanges.get(key); + changes.push({ + key: Data.from(key), + contractAddress: Address.from(contractAddress), + from: Data.from(from, 32), + to: Data.from(to, 32) + }); + } + const returnValue = Data.from(result.returnValue || "0x"); const gas = Quantity.from(result.gas); const logs = result.logs?.map(([addr, topics, data]) => ({ address: Data.from(addr), - topics: topics?.map(Data.from), + topics: topics?.map(t => Data.from(t)), data: Data.from(data) })); @@ -2917,8 +2939,11 @@ export default class EthereumApi implements Api { returnValue, gas, logs, + //todo: populate receipts receipts: undefined, - trace: undefined + //todo: populate trace + trace: undefined, + stateChanges: changes }; } @@ -2978,7 +3003,7 @@ export default class EthereumApi implements Api { blockNumber: QUANTITY | Ethereum.Tag = Tag.latest, overrides: Ethereum.Call.Overrides = {} ): Promise { - const result = await simulateTransaction( + const { result } = await simulateTransaction( this.#blockchain, this.#options, transaction, diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 0130afcdc9..33d50d4d3c 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. @@ -1107,6 +1108,7 @@ export default class Blockchain extends Emittery { overrides: CallOverrides ) { let result: EVMResult; + const stateChanges = new Map(); const data = transaction.data; let gasLimit = transaction.gas.toBigInt(); @@ -1146,11 +1148,12 @@ export default class Blockchain extends Emittery { common ); + let storageTrieByAddress = new Map(); + // 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) => { + vm.evm.events.on("step", async (event: InterpreterStep) => { const logs = maybeGetLogs(event); if (logs) { options.logging.logger.log(...logs); @@ -1160,6 +1163,24 @@ export default class Blockchain extends Emittery { }); } + if (event.opcode.name === "SSTORE") { + const stackLength = event.stack.length; + const key = bigIntToBuffer(event.stack[stackLength - 1]); + const value = bigIntToBuffer(event.stack[stackLength - 2]); + let storageTrie: Trie; + if (storageTrieByAddress.has(event.codeAddress)) { + storageTrie = storageTrieByAddress.get(event.codeAddress); + } else { + storageTrie = await ( + vm.stateManager as GanacheStateManager + ).getStorageTrie(event.codeAddress.toBuffer()); + storageTrieByAddress.set(event.codeAddress, storageTrie); + } + + const from = await storageTrie.get(key); + stateChanges.set(key, [event.codeAddress.toBuffer(), from, value]); + } + if (!this.#emitStepEvent) return; const ganacheStepEvent = makeStepEvent(transactionContext, event); this.emit("ganache:vm:tx:step", ganacheStepEvent); @@ -1195,7 +1216,6 @@ export default class Blockchain extends Emittery { // TODO: should we throw if insufficient funds? fromAccount.balance = txCost > startBalance ? 0n : startBalance - txCost; await vm.eei.putAccount(callerAddress, fromAccount); - // finally, run the call result = await vm.evm.runCall({ caller: callerAddress, @@ -1221,7 +1241,7 @@ export default class Blockchain extends Emittery { if (result.execResult.exceptionError) { throw new CallError(result); } else { - return result.execResult; + return { result: result.execResult, stateChanges }; } } 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", From a73e30be3ff194d606f4fdd614a609ba3900009c Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:33:21 +1200 Subject: [PATCH 03/15] Add state changes, fix logs --- src/chains/ethereum/ethereum/src/api.ts | 37 ++++++++++++++++--- .../ethereum/ethereum/src/blockchain.ts | 29 +++++++++++++-- src/packages/ganache/package.json | 2 +- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 20b0a29880..73974df5ca 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -65,13 +65,19 @@ type TransactionSimulationArgs = { }; type Log = [address: Address, topics: DATA[], data: DATA]; - +type StateChange = { + key: Data; + address: Address; + from: Data; + to: Data; +}; type TransactionSimulationResult = { returnValue: Data; gas: Quantity; logs: Log[]; receipts?: Data[]; trace?: []; + stateChanges: StateChange[]; }; async function simulateTransaction( @@ -80,7 +86,10 @@ async function simulateTransaction( transaction: Ethereum.Call.Transaction, blockNumber: QUANTITY | Ethereum.Tag = Tag.latest, overrides: Ethereum.Call.Overrides = {} -): Promise { +): Promise<{ + result: any; + stateChanges: Map; +}> { // EVMResult const common = blockchain.common; const blocks = blockchain.blocks; @@ -185,6 +194,7 @@ async function simulateTransaction( parentBlock, overrides ); + return result; } @@ -2893,11 +2903,12 @@ export default class EthereumApi implements Api { async evm_simulateTransaction( args: TransactionSimulationArgs ): Promise { + // todo: need to be able to pass in multiple transactions const transaction = args.transactions[0]; const blockNumber = args.block || "latest"; const overrides = args.overrides; - const result = await simulateTransaction( + const { result, stateChanges } = await simulateTransaction( this.#blockchain, this.#options, transaction, @@ -2905,11 +2916,22 @@ export default class EthereumApi implements Api { overrides ); + const changes = []; + for (const key of stateChanges.keys()) { + const [contractAddress, from, to] = stateChanges.get(key); + changes.push({ + key: Data.from(key), + address: Address.from(contractAddress), + from: Data.from(from, 32), + to: Data.from(to, 32) + }); + } + const returnValue = Data.from(result.returnValue || "0x"); const gas = Quantity.from(result.gas); const logs = result.logs?.map(([addr, topics, data]) => ({ address: Data.from(addr), - topics: topics?.map(Data.from), + topics: topics?.map(t => Data.from(t)), data: Data.from(data) })); @@ -2917,8 +2939,11 @@ export default class EthereumApi implements Api { returnValue, gas, logs, + //todo: populate receipts receipts: undefined, - trace: undefined + //todo: populate trace + trace: undefined, + stateChanges: changes }; } @@ -2978,7 +3003,7 @@ export default class EthereumApi implements Api { blockNumber: QUANTITY | Ethereum.Tag = Tag.latest, overrides: Ethereum.Call.Overrides = {} ): Promise { - const result = await simulateTransaction( + const { result } = await simulateTransaction( this.#blockchain, this.#options, transaction, diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 0130afcdc9..f50740565d 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. @@ -1107,6 +1108,7 @@ export default class Blockchain extends Emittery { overrides: CallOverrides ) { let result: EVMResult; + const stateChanges = new Map(); const data = transaction.data; let gasLimit = transaction.gas.toBigInt(); @@ -1145,12 +1147,13 @@ export default class Blockchain extends Emittery { false, // precompiles have already been initialized in the stateTrie common ); + const stateManager = vm.stateManager as GanacheStateManager; + let storageTrieByAddress = new Map(); // 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) => { + vm.evm.events.on("step", async (event: InterpreterStep) => { const logs = maybeGetLogs(event); if (logs) { options.logging.logger.log(...logs); @@ -1160,6 +1163,25 @@ export default class Blockchain extends Emittery { }); } + if (event.opcode.name === "SSTORE") { + const stackLength = event.stack.length; + const key = bigIntToBuffer(event.stack[stackLength - 1]); + const value = bigIntToBuffer(event.stack[stackLength - 2]); + let storageTrie: Trie; + if (storageTrieByAddress.has(event.codeAddress)) { + storageTrie = storageTrieByAddress.get(event.codeAddress); + } else { + storageTrie = await stateManager.getStorageTrie( + event.codeAddress.toBuffer() + ); + storageTrieByAddress.set(event.codeAddress, storageTrie); + } + + // this might not need to await, if we can get the storage trie from the original stateTrie, rather than the copy + const from = await storageTrie.get(key); + stateChanges.set(key, [event.codeAddress.toBuffer(), from, value]); + } + if (!this.#emitStepEvent) return; const ganacheStepEvent = makeStepEvent(transactionContext, event); this.emit("ganache:vm:tx:step", ganacheStepEvent); @@ -1195,7 +1217,6 @@ export default class Blockchain extends Emittery { // TODO: should we throw if insufficient funds? fromAccount.balance = txCost > startBalance ? 0n : startBalance - txCost; await vm.eei.putAccount(callerAddress, fromAccount); - // finally, run the call result = await vm.evm.runCall({ caller: callerAddress, @@ -1221,7 +1242,7 @@ export default class Blockchain extends Emittery { if (result.execResult.exceptionError) { throw new CallError(result); } else { - return result.execResult; + return { result: result.execResult, stateChanges }; } } 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", From a9da675234e590615f79d4bfb19d32122c8918ff Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 28 Apr 2023 13:25:36 +1200 Subject: [PATCH 04/15] Rename stateChanges to storageChanges. Fix storageChanges [we weren't using properly lengthed keys, and we were caching storageTries]. --- src/chains/ethereum/ethereum/src/api.ts | 14 ++++----- .../ethereum/ethereum/src/blockchain.ts | 29 ++++++++++++++----- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 73974df5ca..9ea77e1ea8 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -65,7 +65,7 @@ type TransactionSimulationArgs = { }; type Log = [address: Address, topics: DATA[], data: DATA]; -type StateChange = { +type StorageChange = { key: Data; address: Address; from: Data; @@ -77,7 +77,7 @@ type TransactionSimulationResult = { logs: Log[]; receipts?: Data[]; trace?: []; - stateChanges: StateChange[]; + storageChanges: StorageChange[]; }; async function simulateTransaction( @@ -88,7 +88,7 @@ async function simulateTransaction( overrides: Ethereum.Call.Overrides = {} ): Promise<{ result: any; - stateChanges: Map; + storageChange: Map; }> { // EVMResult const common = blockchain.common; @@ -2908,7 +2908,7 @@ export default class EthereumApi implements Api { const blockNumber = args.block || "latest"; const overrides = args.overrides; - const { result, stateChanges } = await simulateTransaction( + const { result, storageChange } = await simulateTransaction( this.#blockchain, this.#options, transaction, @@ -2917,8 +2917,8 @@ export default class EthereumApi implements Api { ); const changes = []; - for (const key of stateChanges.keys()) { - const [contractAddress, from, to] = stateChanges.get(key); + for (const key of storageChange.keys()) { + const [contractAddress, from, to] = storageChange.get(key); changes.push({ key: Data.from(key), address: Address.from(contractAddress), @@ -2943,7 +2943,7 @@ export default class EthereumApi implements Api { receipts: undefined, //todo: populate trace trace: undefined, - stateChanges: changes + storageChanges: changes }; } diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index f50740565d..71e915b1d6 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1108,7 +1108,7 @@ export default class Blockchain extends Emittery { overrides: CallOverrides ) { let result: EVMResult; - const stateChanges = new Map(); + const storageChange = new Map(); const data = transaction.data; let gasLimit = transaction.gas.toBigInt(); @@ -1165,9 +1165,25 @@ export default class Blockchain extends Emittery { if (event.opcode.name === "SSTORE") { const stackLength = event.stack.length; - const key = bigIntToBuffer(event.stack[stackLength - 1]); + const keyBigInt = event.stack[stackLength - 1]; + const key = + keyBigInt === 0n + ? Buffer.alloc(32) + : // todo: this isn't super efficient, but :shrug: we probably don't do it often + Data.toBuffer(bigIntToBuffer(keyBigInt), 32); const value = bigIntToBuffer(event.stack[stackLength - 2]); - let storageTrie: Trie; + + // todo: DELEGATE_CALL might impact the address context from which the `before` value should be fetched + const storageTrie = await stateManager.getStorageTrie( + event.codeAddress.toBuffer() + ); + + /* + // if the value of a given address and slot has it's value changed multiple times, + // the "from" value will return the stale data in subsequent state changes we may + // be able to just change it's root, but maybe it's more efficient just to get a + // new trie + if (storageTrieByAddress.has(event.codeAddress)) { storageTrie = storageTrieByAddress.get(event.codeAddress); } else { @@ -1175,11 +1191,10 @@ export default class Blockchain extends Emittery { event.codeAddress.toBuffer() ); storageTrieByAddress.set(event.codeAddress, storageTrie); - } + }*/ - // this might not need to await, if we can get the storage trie from the original stateTrie, rather than the copy const from = await storageTrie.get(key); - stateChanges.set(key, [event.codeAddress.toBuffer(), from, value]); + storageChange.set(key, [event.codeAddress.toBuffer(), from, value]); } if (!this.#emitStepEvent) return; @@ -1242,7 +1257,7 @@ export default class Blockchain extends Emittery { if (result.execResult.exceptionError) { throw new CallError(result); } else { - return { result: result.execResult, stateChanges }; + return { result: result.execResult, storageChange }; } } From 88783d222df028d18524ccbee9209b7698697736 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 28 Apr 2023 15:47:37 +1200 Subject: [PATCH 05/15] We need to RLP decode the values stored in the storage slots --- src/chains/ethereum/ethereum/src/api.ts | 6 ++++-- src/chains/ethereum/ethereum/src/blockchain.ts | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 9ea77e1ea8..a1f572cb0e 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -1947,8 +1947,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); } @@ -2922,8 +2924,8 @@ export default class EthereumApi implements Api { changes.push({ key: Data.from(key), address: Address.from(contractAddress), - from: Data.from(from, 32), - to: Data.from(to, 32) + from: Data.from(from), + to: Data.from(to) }); } diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 06efb00b11..9664ed4ecc 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1170,9 +1170,11 @@ export default class Blockchain extends Emittery { ? Buffer.alloc(32) : // todo: this isn't super efficient, but :shrug: we probably don't do it often Data.toBuffer(bigIntToBuffer(keyBigInt), 32); - const value = bigIntToBuffer(event.stack[stackLength - 2]); + 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() ); @@ -1192,8 +1194,12 @@ export default class Blockchain extends Emittery { storageTrieByAddress.set(event.codeAddress, storageTrie); }*/ - const from = await storageTrie.get(key); - storageChange.set(key, [event.codeAddress.toBuffer(), from, value]); + const from = decode(await storageTrie.get(key)); + storageChange.set(key, [ + event.codeAddress.toBuffer(), + Data.toBuffer(from, 32), + value + ]); } if (!this.#emitStepEvent) return; From 52227e71859d504c78ecabb3c9710ef8f778d936 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 28 Apr 2023 16:09:52 +1200 Subject: [PATCH 06/15] Ensure that default 32 bytee value is returned when storage slot isn't populated --- src/chains/ethereum/ethereum/src/blockchain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 9664ed4ecc..30aaa5b3c0 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1197,7 +1197,7 @@ export default class Blockchain extends Emittery { const from = decode(await storageTrie.get(key)); storageChange.set(key, [ event.codeAddress.toBuffer(), - Data.toBuffer(from, 32), + from.length === 0 ? Buffer.alloc(32) : Data.toBuffer(from, 32), value ]); } From 080ef66ae55458a4b7e629f76c6718775d4ddcf1 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 1 May 2023 10:53:26 +1200 Subject: [PATCH 07/15] Hack in performance measurement in durationMs --- src/chains/ethereum/ethereum/src/connector.ts | 14 ++++++++------ src/chains/ethereum/ethereum/src/provider.ts | 2 +- src/chains/filecoin/filecoin/src/connector.ts | 2 +- src/packages/core/src/servers/http-server.ts | 5 ++++- src/packages/utils/src/things/jsonrpc.ts | 10 ++++++++-- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/connector.ts b/src/chains/ethereum/ethereum/src/connector.ts index d38dc48f80..8eaa310f59 100644 --- a/src/chains/ethereum/ethereum/src/connector.ts +++ b/src/chains/ethereum/ethereum/src/connector.ts @@ -106,13 +106,15 @@ 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( @@ -121,12 +123,12 @@ export class Connector< if (result instanceof Error) { return makeError(payload.id, result as any); } 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" && 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/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..929079de72 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 = Date.now(); + // handle JSONRPC post requests... const writeHeaders = prepareCORSResponseHeaders("POST", request); @@ -241,7 +243,8 @@ export default class HttpServer { // cause an `Unhandled promise rejection` if we try) return; } - const data = connector.format(result, payload); + const endTime = Date.now(); + const data = connector.format(result, payload, endTime - startTime); if (types.isGeneratorObject(data)) { sendChunkedResponse( response, diff --git a/src/packages/utils/src/things/jsonrpc.ts b/src/packages/utils/src/things/jsonrpc.ts index 2004a401db..8525847b12 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: { @@ -41,11 +42,16 @@ 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 = ( From 271b3b8059dd192ad57d8c6dc223c3a4b90ed756 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 1 May 2023 10:56:55 +1200 Subject: [PATCH 08/15] use performance.now(), instead of Date.now() for performance measurementy goodness --- src/packages/core/src/servers/http-server.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/packages/core/src/servers/http-server.ts b/src/packages/core/src/servers/http-server.ts index 929079de72..deb2bc2337 100644 --- a/src/packages/core/src/servers/http-server.ts +++ b/src/packages/core/src/servers/http-server.ts @@ -197,7 +197,7 @@ export default class HttpServer { } #handlePost = (response: HttpResponse, request: HttpRequest) => { - const startTime = Date.now(); + const startTime = performance.now(); // handle JSONRPC post requests... const writeHeaders = prepareCORSResponseHeaders("POST", request); @@ -243,8 +243,12 @@ export default class HttpServer { // cause an `Unhandled promise rejection` if we try) return; } - const endTime = Date.now(); - const data = connector.format(result, payload, endTime - startTime); + const endTime = performance.now(); + const data = connector.format( + result, + payload, + Math.floor(endTime - startTime) + ); if (types.isGeneratorObject(data)) { sendChunkedResponse( response, From f8f444186e68a6c838d38328e24da033175d71e1 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 1 May 2023 13:24:34 +1200 Subject: [PATCH 09/15] Change interface to match expected shape --- src/chains/ethereum/ethereum/src/api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index a1f572cb0e..019a10dffd 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -59,7 +59,7 @@ type TransactionSimulationTransaction = Ethereum.Transaction & { }; type TransactionSimulationArgs = { - transactions: TransactionSimulationTransaction[]; + transactions: [TransactionSimulationTransaction[]]; overrides?: Ethereum.Call.Overrides; block?: QUANTITY | Ethereum.Tag; }; @@ -2902,11 +2902,11 @@ export default class EthereumApi implements Api { * @param {TransactionSimulationArgs} args * @returns Promise */ - async evm_simulateTransaction( + async evm_simulateTransactions( args: TransactionSimulationArgs ): Promise { // todo: need to be able to pass in multiple transactions - const transaction = args.transactions[0]; + const transaction = args.transactions[0][0]; const blockNumber = args.block || "latest"; const overrides = args.overrides; From f94390a85a2040c49075558cfc5f5f09c7f50381 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Mon, 1 May 2023 16:32:40 +1200 Subject: [PATCH 10/15] Really really hacky and terrible state diff --- src/chains/ethereum/ethereum/src/api.ts | 55 +++++++++++++--- .../ethereum/ethereum/src/blockchain.ts | 62 +++++++++++++++++-- 2 files changed, 104 insertions(+), 13 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 019a10dffd..37dee8623f 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -71,6 +71,21 @@ type StorageChange = { 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; @@ -78,6 +93,7 @@ type TransactionSimulationResult = { receipts?: Data[]; trace?: []; storageChanges: StorageChange[]; + stateChanges: StateChange[]; }; async function simulateTransaction( @@ -88,7 +104,11 @@ async function simulateTransaction( overrides: Ethereum.Call.Overrides = {} ): Promise<{ result: any; - storageChange: Map; + storageChanges: Map; + stateChanges: Map< + Buffer, + [[Buffer, Buffer, Buffer, Buffer], [Buffer, Buffer, Buffer, Buffer]] + >; }> { // EVMResult const common = blockchain.common; @@ -2910,7 +2930,7 @@ export default class EthereumApi implements Api { const blockNumber = args.block || "latest"; const overrides = args.overrides; - const { result, storageChange } = await simulateTransaction( + const { result, storageChanges, stateChanges } = await simulateTransaction( this.#blockchain, this.#options, transaction, @@ -2918,10 +2938,10 @@ export default class EthereumApi implements Api { overrides ); - const changes = []; - for (const key of storageChange.keys()) { - const [contractAddress, from, to] = storageChange.get(key); - changes.push({ + 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), @@ -2929,6 +2949,26 @@ export default class EthereumApi implements Api { }); } + 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.gas); const logs = result.logs?.map(([addr, topics, data]) => ({ @@ -2945,7 +2985,8 @@ export default class EthereumApi implements Api { receipts: undefined, //todo: populate trace trace: undefined, - storageChanges: changes + storageChanges: parsedStorageChanges, + stateChanges: parsedStateChanges }; } diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 30aaa5b3c0..fd77641915 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -76,7 +76,7 @@ import { maybeGetLogs } from "@ganache/console.log"; import { dumpTrieStorageDetails } from "./helpers/storage-range-at"; import { GanacheStateManager } from "./state-manager"; import { TrieDB } from "./trie-db"; -import { Trie } from "@ethereumjs/trie"; +import { LeafNode, Trie } from "@ethereumjs/trie"; import { removeEIP3860InitCodeSizeLimitCheck } from "./helpers/common-helpers"; import { bigIntToBuffer } from "@ganache/utils"; @@ -1108,8 +1108,11 @@ export default class Blockchain extends Emittery { overrides: CallOverrides ) { let result: EVMResult; - const storageChange = new Map(); - + 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 @@ -1148,7 +1151,7 @@ export default class Blockchain extends Emittery { common ); const stateManager = vm.stateManager as GanacheStateManager; - + const originalStateRoot = await stateManager.getStateRoot(); // 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(); @@ -1195,7 +1198,7 @@ export default class Blockchain extends Emittery { }*/ const from = decode(await storageTrie.get(key)); - storageChange.set(key, [ + storageChanges.set(key, [ event.codeAddress.toBuffer(), from.length === 0 ? Buffer.alloc(32) : Data.toBuffer(from, 32), value @@ -1247,6 +1250,48 @@ export default class Blockchain extends Emittery { value: transaction.value == null ? 0n : transaction.value.toBigInt(), block: transaction.block as any }); + + const cache = stateManager["_cache"]["_cache"] as any; + let addresses = new Map(); + cache.forEach(i => { + const addr = Buffer.from(i[0], "hex"); + const value = decode(i[1].val); + addresses.set(addr, value); + }); + + await stateManager.setStateRoot(originalStateRoot); + const keys = Array.from(addresses.keys()); + const accounts = await Promise.all( + keys.map(async address => { + const after = addresses.get(address); + const beforeAccount = await stateManager.getAccount( + Address.from(address) + ); + const before = [ + Quantity.toBuffer(beforeAccount.nonce), + Quantity.toBuffer(beforeAccount.balance), + Quantity.toBuffer(beforeAccount.storageRoot), + Quantity.toBuffer(beforeAccount.codeHash) + ] as [Buffer, Buffer, Buffer, Buffer]; + + return { + address, + before, + after + }; + }) + ); + accounts.forEach(account => { + const isChanged = !( + account.after[0].equals(account.before[0]) && + account.after[1].equals(account.before[1]) && + account.after[2].equals(account.before[2]) && + account.after[3].equals(account.before[3]) + ); + if (isChanged) { + stateChanges.set(account.address, [account.before, account.after]); + } + }); } else { result = { execResult: { @@ -1256,13 +1301,18 @@ 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 { result: result.execResult, storageChange }; + return { + result: result.execResult, + storageChanges, + stateChanges + }; } } From 6beccbacc81059a4f8e3f95418edb2e61a34c455 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 2 May 2023 16:10:08 +1200 Subject: [PATCH 11/15] =?UTF-8?q?Maybe=20fix=20gas=20pricing=20for=20trans?= =?UTF-8?q?action=20simulation=20=F0=9F=A4=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chains/ethereum/ethereum/src/api.ts | 4 ++-- src/chains/ethereum/ethereum/src/blockchain.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 37dee8623f..0e4c69b00e 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -176,7 +176,8 @@ async function simulateTransaction( const tip = a < maxPriorityFeePerGas ? a : maxPriorityFeePerGas; gasPrice = Quantity.from(baseFeePerGasBigInt + tip); } else { - gasPrice = Quantity.from(0); + //todo: this used to be 0 - need to validate this change + gasPrice = Quantity.from(baseFeePerGasBigInt); } } @@ -2929,7 +2930,6 @@ export default class EthereumApi implements Api { const transaction = args.transactions[0][0]; const blockNumber = args.block || "latest"; const overrides = args.overrides; - const { result, storageChanges, stateChanges } = await simulateTransaction( this.#blockchain, this.#options, diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index fd77641915..559c5280ca 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1151,7 +1151,6 @@ export default class Blockchain extends Emittery { common ); const stateManager = vm.stateManager as GanacheStateManager; - const originalStateRoot = await stateManager.getStateRoot(); // 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(); @@ -1259,12 +1258,11 @@ export default class Blockchain extends Emittery { addresses.set(addr, value); }); - await stateManager.setStateRoot(originalStateRoot); const keys = Array.from(addresses.keys()); const accounts = await Promise.all( keys.map(async address => { const after = addresses.get(address); - const beforeAccount = await stateManager.getAccount( + const beforeAccount = await this.vm.stateManager.getAccount( Address.from(address) ); const before = [ From dffaca15db63b6752d7c59ea045c0d48711098ae Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 3 May 2023 10:48:08 +1200 Subject: [PATCH 12/15] Fix nonce double increment, but potentially introduce bug. Add intrinsic gas to resulting gas output. --- src/chains/ethereum/ethereum/src/api.ts | 7 ++++--- .../ethereum/ethereum/src/blockchain.ts | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 0e4c69b00e..a5e018508e 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -151,7 +151,9 @@ async function simulateTransaction( // 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); + gasPrice = hasGasPrice + ? Quantity.Zero + : Quantity.from(transaction.gasPrice); } else { const hasMaxFeePerGas = typeof transaction.maxFeePerGas !== "undefined"; const hasMaxPriorityFeePerGas = @@ -176,8 +178,7 @@ async function simulateTransaction( const tip = a < maxPriorityFeePerGas ? a : maxPriorityFeePerGas; gasPrice = Quantity.from(baseFeePerGasBigInt + tip); } else { - //todo: this used to be 0 - need to validate this change - gasPrice = Quantity.from(baseFeePerGasBigInt); + gasPrice = Quantity.Zero; } } diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 559c5280ca..c95d982023 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1126,9 +1126,8 @@ export default class Blockchain extends Emittery { BigInt(transaction.block.header.number.toString()) ) : this.common; - - 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", { @@ -1208,7 +1207,6 @@ export default class Blockchain extends Emittery { const ganacheStepEvent = makeStepEvent(transactionContext, event); this.emit("ganache:vm:tx:step", ganacheStepEvent); }); - const caller = transaction.from.toBuffer(); const callerAddress = new Address(caller); @@ -1233,11 +1231,16 @@ 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 result = await vm.evm.runCall({ @@ -1280,6 +1283,7 @@ export default class Blockchain extends Emittery { }) ); accounts.forEach(account => { + // nonce, balance, storageRoot, codeHash const isChanged = !( account.after[0].equals(account.before[0]) && account.after[1].equals(account.before[1]) && @@ -1306,6 +1310,7 @@ export default class Blockchain extends Emittery { if (result.execResult.exceptionError) { throw new CallError(result); } else { + result.execResult.gas = (result.execResult.gas || 0n) + intrinsicGas; return { result: result.execResult, storageChanges, From ac41fe32a8d7e2d8cc0c94264f643152aeb0e8d0 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 3 May 2023 11:18:06 +1200 Subject: [PATCH 13/15] make --fork.disableCache only apply to the peristent cache --- .../src/forking/handlers/base-handler.ts | 56 +++++++++---------- .../ethereum/options/src/fork-options.ts | 4 +- 2 files changed, 28 insertions(+), 32 deletions(-) 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/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: { From 6ff46e38d0fab9eca92e886b713f021408a2c40d Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Wed, 3 May 2023 12:28:43 +1200 Subject: [PATCH 14/15] Add duration to error response (on http only) --- src/chains/ethereum/ethereum/src/connector.ts | 16 +++++++++++++--- src/packages/core/src/servers/http-server.ts | 8 +++++++- src/packages/utils/src/things/jsonrpc.ts | 10 +++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/connector.ts b/src/chains/ethereum/ethereum/src/connector.ts index 8eaa310f59..0ea59a8cb0 100644 --- a/src/chains/ethereum/ethereum/src/connector.ts +++ b/src/chains/ethereum/ethereum/src/connector.ts @@ -121,7 +121,7 @@ export class Connector< 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, durationMs); } @@ -161,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/packages/core/src/servers/http-server.ts b/src/packages/core/src/servers/http-server.ts index deb2bc2337..1cd6704af9 100644 --- a/src/packages/core/src/servers/http-server.ts +++ b/src/packages/core/src/servers/http-server.ts @@ -276,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/utils/src/things/jsonrpc.ts b/src/packages/utils/src/things/jsonrpc.ts index 8525847b12..d7d22c9a7a 100644 --- a/src/packages/utils/src/things/jsonrpc.ts +++ b/src/packages/utils/src/things/jsonrpc.ts @@ -26,6 +26,7 @@ export type JsonRpcError = JsonRpc & { readonly message: any; }; readonly result?: any; + readonly durationMs: number; }; const jsonrpc = "2.0" as const; @@ -57,7 +58,8 @@ export const makeResponse = ( 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, @@ -76,13 +78,15 @@ export const makeError = ( id, jsonrpc, error: details, - result + result, + durationMs: durationMs }; } else { return { id, jsonrpc, - error: details + error: details, + durationMs: durationMs }; } }; From 9b8a7160fcb4aaf0c7df4cae66d605c0b68e8f5a Mon Sep 17 00:00:00 2001 From: jeffsmale90 <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 9 May 2023 17:03:29 +1200 Subject: [PATCH 15/15] poc/transaction simulation correct block (#4397) --- src/chains/ethereum/ethereum/src/api.ts | 37 ++-- .../ethereum/ethereum/src/blockchain.ts | 170 +++++++++++------- .../ethereum/ethereum/src/forking/trie.ts | 4 +- 3 files changed, 128 insertions(+), 83 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index a5e018508e..c112eab492 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -182,15 +182,20 @@ async function simulateTransaction( } } + const incr = + typeof options.miner.timestampIncrement === "string" + ? 12n + : options.miner.timestampIncrement.toBigInt(); + const block = new RuntimeBlock( blockchain.common, - parentHeader.number, - parentHeader.parentHash, + Quantity.from(parentHeader.number.toNumber() + 1), + parentBlock.hash(), blockchain.coinbase, gas, parentHeader.gasUsed, - parentHeader.timestamp, - options.miner.difficulty, + Quantity.from(parentHeader.timestamp.toBigInt() + incr), + Quantity.Zero, //options.miner.difficulty, parentHeader.totalDifficulty, blockchain.getMixHash(parentHeader.parentHash.toBuffer()), baseFeePerGasBigInt, @@ -210,6 +215,7 @@ async function simulateTransaction( data, block }; + //const _result = await blockchain.runTransaction(tx, parentBlock, parentBlock); const result = await blockchain.simulateTransaction( simulatedTransaction, @@ -2930,14 +2936,17 @@ export default class EthereumApi implements Api { // 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; - const { result, storageChanges, stateChanges } = await simulateTransaction( - this.#blockchain, - this.#options, - transaction, - blockNumber, - 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()) { @@ -2971,7 +2980,7 @@ export default class EthereumApi implements Api { } const returnValue = Data.from(result.returnValue || "0x"); - const gas = Quantity.from(result.gas); + 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)), @@ -2987,7 +2996,9 @@ export default class EthereumApi implements Api { //todo: populate trace trace: undefined, storageChanges: parsedStorageChanges, - stateChanges: parsedStateChanges + stateChanges: parsedStateChanges, + //@ts-ignore + timings }; } diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index c95d982023..e09d9230cb 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -76,7 +76,7 @@ import { maybeGetLogs } from "@ganache/console.log"; import { dumpTrieStorageDetails } from "./helpers/storage-range-at"; import { GanacheStateManager } from "./state-manager"; import { TrieDB } from "./trie-db"; -import { LeafNode, Trie } from "@ethereumjs/trie"; +import { Trie } from "@ethereumjs/trie"; import { removeEIP3860InitCodeSizeLimitCheck } from "./helpers/common-helpers"; import { bigIntToBuffer } from "@ganache/utils"; @@ -1107,6 +1107,12 @@ export default class Blockchain extends Emittery { parentBlock: Block, overrides: CallOverrides ) { + 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< @@ -1120,12 +1126,16 @@ 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 intrinsicGas = calculateIntrinsicGas(data, hasToAddress, common); const gasLeft = gasLimit - intrinsicGas; @@ -1149,18 +1159,19 @@ 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", async (event: InterpreterStep) => { - const logs = maybeGetLogs(event); - if (logs) { - options.logging.logger.log(...logs); - this.emit("ganache:vm:tx:console.log", { - context: transactionContext, - logs - }); + if ( + event.opcode.name === "CALL" || + event.opcode.name === "DELEGATECALL" || + event.opcode.name === "STATICCALL" || + event.opcode.name === "JUMP" + ) { + //console.log(event.opcode.name); } if (event.opcode.name === "SSTORE") { @@ -1168,7 +1179,7 @@ export default class Blockchain extends Emittery { const keyBigInt = event.stack[stackLength - 1]; const key = keyBigInt === 0n - ? Buffer.alloc(32) + ? 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]; @@ -1180,33 +1191,24 @@ export default class Blockchain extends Emittery { event.codeAddress.toBuffer() ); - /* - // if the value of a given address and slot has it's value changed multiple times, - // the "from" value will return the stale data in subsequent state changes we may - // be able to just change it's root, but maybe it's more efficient just to get a - // new trie - - if (storageTrieByAddress.has(event.codeAddress)) { - storageTrie = storageTrieByAddress.get(event.codeAddress); - } else { - storageTrie = await stateManager.getStorageTrie( - event.codeAddress.toBuffer() - ); - storageTrieByAddress.set(event.codeAddress, storageTrie); - }*/ - 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 ]); } - - if (!this.#emitStepEvent) return; - const ganacheStepEvent = makeStepEvent(transactionContext, event); - this.emit("ganache:vm:tx:step", ganacheStepEvent); }); + const caller = transaction.from.toBuffer(); const callerAddress = new Address(caller); @@ -1243,6 +1245,8 @@ export default class Blockchain extends Emittery { 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(), @@ -1252,47 +1256,47 @@ export default class Blockchain extends Emittery { value: transaction.value == null ? 0n : transaction.value.toBigInt(), block: transaction.block as any }); - - const cache = stateManager["_cache"]["_cache"] as any; - let addresses = new Map(); - cache.forEach(i => { - const addr = Buffer.from(i[0], "hex"); - const value = decode(i[1].val); - addresses.set(addr, value); + timings.push({ + time: performance.now(), + label: "finished running transaction" }); - const keys = Array.from(addresses.keys()); - const accounts = await Promise.all( - keys.map(async address => { - const after = addresses.get(address); - const beforeAccount = await this.vm.stateManager.getAccount( - Address.from(address) - ); - const before = [ - Quantity.toBuffer(beforeAccount.nonce), - Quantity.toBuffer(beforeAccount.balance), - Quantity.toBuffer(beforeAccount.storageRoot), - Quantity.toBuffer(beforeAccount.codeHash) - ] as [Buffer, Buffer, Buffer, Buffer]; - - return { - address, - before, - after - }; - }) - ); - accounts.forEach(account => { - // nonce, balance, storageRoot, codeHash - const isChanged = !( - account.after[0].equals(account.before[0]) && - account.after[1].equals(account.before[1]) && - account.after[2].equals(account.before[2]) && - account.after[3].equals(account.before[3]) + 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(); + }) ); - if (isChanged) { - stateChanges.set(account.address, [account.before, account.after]); - } + }); + + await Promise.all(asyncAccounts); + + timings.push({ + time: performance.now(), + label: "finished building state diff" }); } else { result = { @@ -1310,11 +1314,41 @@ export default class Blockchain extends Emittery { if (result.execResult.exceptionError) { throw new CallError(result); } else { - result.execResult.gas = (result.execResult.gas || 0n) + intrinsicGas; + 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 + stateChanges, + timings: timingsSummary }; } } 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: