From c7e23a331d3c82225657b455bc6e40526cd28fe2 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Thu, 1 Jun 2023 14:16:06 -0400 Subject: [PATCH 01/13] allow latest and later --- .../src/data-managers/block-manager.ts | 29 ++++++++++++++++++- .../ethereum/ethereum/src/forking/fork.ts | 16 ++++++++-- .../src/forking/handlers/base-handler.ts | 3 +- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts b/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts index 7e3a4e405c..85c823b250 100644 --- a/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts +++ b/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts @@ -1,6 +1,6 @@ import Manager from "./manager"; import { Tag, QUANTITY } from "@ganache/ethereum-utils"; -import { Quantity, Data, BUFFER_ZERO } from "@ganache/utils"; +import { Quantity, Data, BUFFER_ZERO, unref } from "@ganache/utils"; import type { Common } from "@ethereumjs/common"; import Blockchain from "../blockchain"; import { @@ -57,6 +57,33 @@ export default class BlockManager extends Manager { ) { const bm = new BlockManager(blockchain, common, blockIndexes, base); await bm.updateTaggedBlocks(); + if (blockchain.fallback) { + // a hack to ensure `latest` is kept up to date. + // this just polls for `latest` every 7 seconds + unref( + setInterval(async () => { + const json = await blockchain.fallback.request( + "eth_getBlockByNumber", + ["latest", true], + { disableCache: true } + ); + if (json == null) { + return null; + } else { + const common = blockchain.fallback.getCommonForBlockNumber( + bm.#common, + BigInt(json.number) + ); + console.log("latest is now", json.number); + + bm.latest = new Block( + BlockManager.rawFromJSON(json, common), + common + ); + } + }, 7000) + ); + } return bm; } diff --git a/src/chains/ethereum/ethereum/src/forking/fork.ts b/src/chains/ethereum/ethereum/src/forking/fork.ts index bb38d29be6..8bc4517579 100644 --- a/src/chains/ethereum/ethereum/src/forking/fork.ts +++ b/src/chains/ethereum/ethereum/src/forking/fork.ts @@ -262,7 +262,10 @@ export class Fork { } public isValidForkBlockNumber(blockNumber: Quantity) { - return blockNumber.toBigInt() <= this.blockNumber.toBigInt(); + // TODO: this is a temporary fix for using ganache for remote transaction + // simulations. it breaks lots of things for normal ganache usage. + return true; + // return blockNumber.toBigInt() <= this.blockNumber.toBigInt(); } public selectValidForkBlockNumber(blockNumber: Quantity) { @@ -281,8 +284,14 @@ export class Fork { * @param common - * @param blockNumber - */ - public getCommonForBlockNumber(common: Common, blockNumber: BigInt) { - if (blockNumber <= this.blockNumber.toBigInt()) { + public getCommonForBlockNumber( + common: Common, + blockNumber: BigInt, + allowFuture = false + ) { + // if we are allowed to get a future hardfork block, then we should try to + // get a common for hardforks that will be activate at those block numbers + if (blockNumber <= this.blockNumber.toBigInt() || allowFuture) { // we are at or before our fork block let forkCommon: Common; @@ -310,6 +319,7 @@ export class Fork { { baseChain: 1 } ); } + (forkCommon as any).on = () => {}; return forkCommon; } else { return common; 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 eeb5afa091..3d1ea418a0 100644 --- a/src/chains/ethereum/ethereum/src/forking/handlers/base-handler.ts +++ b/src/chains/ethereum/ethereum/src/forking/handlers/base-handler.ts @@ -196,7 +196,8 @@ export class BaseHandler { if (hasOwn(response, "result")) { // cache non-error responses only - this.valueCache.set(key, raw); + // don't set the cache when "latest" is requested. + if (!key.includes("latest")) this.valueCache.set(key, raw); if (!options.disableCache && this.persistentCache) { // swallow errors for the persistentCache, since it's not vital that // it always works From e024f68bc21cb8730b7b1c778b329e4526a8ff93 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Tue, 13 Jun 2023 21:13:35 -0400 Subject: [PATCH 02/13] WIP docs and API changes --- src/chains/ethereum/ethereum/src/api.ts | 184 ++++++------ .../ethereum/ethereum/src/blockchain.ts | 87 +++--- .../src/data-managers/block-manager.ts | 4 +- src/packages/sim/app.js | 14 +- src/packages/sim/docs.html | 263 ++++++++++++++++++ src/packages/sim/index.html | 26 +- src/packages/sim/index.ts | 21 +- src/packages/sim/main.css | 2 +- 8 files changed, 439 insertions(+), 162 deletions(-) create mode 100644 src/packages/sim/docs.html diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 9c775714c7..6876ce222b 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -58,14 +58,12 @@ type TransactionSimulationTransaction = Ethereum.Transaction & { traceTypes?: string[]; }; -type TraceType = "full" | "call" | "none"; -type GasEstimateType = "full" | "call-depth" | "none"; -type TransactionSimulationArgs = { +type TraceType = boolean; +type TransactionSimulationArgs = { transactions: TransactionSimulationTransaction[]; overrides?: Ethereum.Call.Overrides; - block?: QUANTITY | Ethereum.Tag; trace?: TraceType; - gasEstimation?: GasEstimateType; + estimateGas?: Estimate; }; type Log = [address: Address, topics: DATA[], data: DATA]; @@ -90,27 +88,53 @@ type StateChange = { codeHash: Data; }; }; -type GasBreakdown = { +export type GasBreakdown = { + /** + * The total amount of gas used by the transaction. + */ + total: Quantity, + + /** + * Total gas used minus the refund. This is what etherscan reports as `Gas Usage`. + */ + actual: Quantity; + + /** + * The amount of gas refunded to the sender. + */ + refund: Quantity; + + /** + * The amount of gas the EVM requires before it would attempt to run the + * transaction. + */ intrinsic: Quantity; + + /** + * The amount of gas used by the transaction's actual execution. + */ execution: Quantity; - refund: Quantity; - actualCost: Quantity; + + /** + * The minimum amount of gas required to run the transaction. + */ + estimate: Estimate extends true ? Quantity : undefined; }; -type TraceEntry = { +export type TraceEntry = { opcode: Data; - type: string; + name: string; from: Address; to: Address; value: Quantity; input: Data; pc: number; target?: string; - decodedInput?: []; + decodedInput?: {type: string, value: Quantity | Data }[]; }; -type TransactionSimulationResult = { +type TransactionSimulationResult = { returnValue: Data; - gas: GasBreakdown; + gas: GasBreakdown; logs: Log[]; storageChanges: StorageChange[]; stateChanges: StateChange[]; @@ -119,10 +143,9 @@ type TransactionSimulationResult = { gasEstimate?: Quantity; }; -type InternalTransactionSimulationResult = { +export type InternalTransactionSimulationResult = { result: any; - gasBreakdown: any; - gasEstimate?: bigint; + gas: GasBreakdown; storageChanges: { address: Address; key: Buffer; @@ -133,27 +156,21 @@ type InternalTransactionSimulationResult = { Buffer, [[Buffer, Buffer, Buffer, Buffer], [Buffer, Buffer, Buffer, Buffer]] >; - trace?: { - opcode: Buffer; - pc: number; - type: string; - stack: Buffer[]; - }[]; + trace?: TraceEntry[]; }; -async function simulateTransaction( +async function simulateTransactions( blockchain: Blockchain, options: EthereumInternalOptions, transactions: Ethereum.Call.Transaction[], - blockNumber: QUANTITY | Ethereum.Tag = Tag.latest, + blockNumber: QUANTITY | Ethereum.Tag, overrides: Ethereum.Call.Overrides = {}, - includeTrace: boolean = false, - includeGasEstimate: boolean = false -): Promise { + includeTrace: boolean, + includeGasEstimate: Estimate +): Promise[]> { const blocks = blockchain.blocks; const parentBlock = await blocks.get(blockNumber); const parentHeader = parentBlock.header; - // EVMResult const simulationBlockNumber = parentHeader.number.toBigInt() + 1n; const common = blockchain.fallback ? blockchain.fallback.getCommonForBlockNumber( @@ -161,9 +178,33 @@ async function simulateTransaction( simulationBlockNumber ) : blockchain.common; + // TODO: why do we do this? can we not? common.setHardfork("shanghai"); - let cummulativeGas = 0n; + let cumulativeGas = 0n; + + const incr = + typeof options.miner.timestampIncrement === "string" + ? 12n + : options.miner.timestampIncrement.toBigInt(); + + const baseFeePerGasBigInt = Block.calcNextBaseFee(parentBlock); + const timestamp = Quantity.from(parentHeader.timestamp.toBigInt() + incr); + + const block = new RuntimeBlock( + common, + Quantity.from(simulationBlockNumber), + parentBlock.hash(), + blockchain.coinbase, + Quantity.Zero, // we'll fill this with cumulativeGas in later + parentHeader.gasUsed, + timestamp, + Quantity.Zero, //options.miner.difficulty, + parentHeader.totalDifficulty, + blockchain.getMixHash(parentHeader.parentHash.toBuffer()), + baseFeePerGasBigInt, + KECCAK256_RLP + ); const simulationTransactions = transactions.map(transaction => { let txGas: Quantity; @@ -242,7 +283,7 @@ async function simulateTransaction( transaction.value == null ? null : Quantity.from(transaction.value); // add this transaction's gas to the block gas - cummulativeGas += txGas.toBigInt(); + cumulativeGas += txGas.toBigInt(); const simulatedTransaction = { gas: txGas, @@ -251,37 +292,15 @@ async function simulateTransaction( gasPrice, value, data, - block: undefined + block }; return simulatedTransaction; }); - const incr = - typeof options.miner.timestampIncrement === "string" - ? 12n - : options.miner.timestampIncrement.toBigInt(); - - // todo: calculate baseFeePerGas - const baseFeePerGasBigInt = parentBlock.header.baseFeePerGas.toBigInt(); - const timestamp = Quantity.from(parentHeader.timestamp.toBigInt() + incr); + block.header.gasLimit = cumulativeGas; - const block = new RuntimeBlock( - common, - Quantity.from(simulationBlockNumber), - parentBlock.hash(), - blockchain.coinbase, - Quantity.from(cummulativeGas), - parentHeader.gasUsed, - timestamp, - Quantity.Zero, //options.miner.difficulty, - parentHeader.totalDifficulty, - blockchain.getMixHash(parentHeader.parentHash.toBuffer()), - baseFeePerGasBigInt, - KECCAK256_RLP - ); - - const results = blockchain.simulateTransactions( + const results = await blockchain.simulateTransactions( common, simulationTransactions, block, @@ -3021,37 +3040,32 @@ export default class EthereumApi implements Api { * @param {TransactionSimulationArgs} args * @returns Promise */ - async evm_simulateTransactions( - args: TransactionSimulationArgs - ): Promise { - // todo: need to be able to pass in multiple transactions - const transactions = args.transactions; - const blockNumber = args.block || "latest"; + @assertArgLength(1, 2) + async evm_simulateTransactions( + { overrides, transactions, trace, estimateGas }: TransactionSimulationArgs, + blockNumber: QUANTITY | Ethereum.Tag = Tag.latest + ): Promise[]> { - const overrides = args.overrides; - const includeTrace = args.trace === "full" || args.trace === "call"; - const includeGasEstimation = - args.gasEstimation === "full" || args.gasEstimation === "call-depth"; + const includeTrace = trace === true; - //@ts-ignore - const simulatedTransactionResults = await simulateTransaction( + const simulatedTransactionResults = await simulateTransactions( this.#blockchain, this.#options, transactions, blockNumber, overrides, includeTrace, - includeGasEstimation + //@ts-ignore + estimateGas === true ); return simulatedTransactionResults.map( ({ trace, - gasBreakdown, + gas, result, storageChanges, - stateChanges, - gasEstimate + stateChanges }) => { const parsedStorageChanges = storageChanges.map(change => ({ key: Data.from(change.key), @@ -3081,12 +3095,6 @@ export default class EthereumApi implements Api { } const returnValue = Data.from(result.returnValue || "0x"); - const gas = { - intrinsic: Quantity.from(gasBreakdown.intrinsicGas), - execution: Quantity.from(gasBreakdown.executionGas), - refund: Quantity.from(gasBreakdown.refund), - actualCost: Quantity.from(gasBreakdown.actualGasCost) - }; const logs = result.logs?.map(([addr, topics, data]) => ({ address: Data.from(addr), topics: topics?.map(t => Data.from(t)), @@ -3100,31 +3108,13 @@ export default class EthereumApi implements Api { error, returnValue, gas, - gasEstimate: gasEstimate ? Quantity.from(gasEstimate) : undefined, logs, //todo: populate receipts receipts: undefined, storageChanges: parsedStorageChanges, stateChanges: parsedStateChanges, trace: includeTrace - ? trace.map((t: any) => { - return { - opcode: Data.from(t.opcode), - type: t.type, - from: Address.from(t.from), - to: Address.from(t.to), - target: t.target, - value: - t.value === undefined ? undefined : Quantity.from(t.value), - input: Data.from(t.input), - decodedInput: t.decodedInput?.map(({ type, value }) => ({ - type, - // todo: some values will be Quantity rather - value: Data.from(value) - })), - pc: t.pc - }; - }) + ? trace : undefined }; } @@ -3189,7 +3179,7 @@ export default class EthereumApi implements Api { ): Promise { //cos I've broken it real good //@ts-ignore - const { result } = await simulateTransaction( + const { result } = await simulateTransactions( this.#blockchain, this.#options, //@ts-ignore diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index e2d05c5e8c..de0cdd2d13 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -81,6 +81,7 @@ import { TrieDB } from "./trie-db"; import { Trie } from "@ethereumjs/trie"; import { Interpreter, RunState } from "@ethereumjs/evm/dist/interpreter"; import { GasTracer } from "./helpers/gas"; +import { GasBreakdown, InternalTransactionSimulationResult, TraceEntry } from "./api"; const mclInitPromise = mcl.init(mcl.BLS12_381).then(() => { mcl.setMapToMode(mcl.IRTF); // set the right map mode; otherwise mapToG2 will return wrong values. @@ -1123,14 +1124,14 @@ export default class Blockchain extends Emittery { } } - public async simulateTransactions( + public async simulateTransactions( common: Common, transactions: SimulationTransaction[], runtimeBlock: RuntimeBlock, parentBlock: Block, overrides: CallOverrides, includeTrace: boolean, - includeGasEstimate: boolean + includeGasEstimate: Estimate ) { const stateTrie = this.trie.copy(false); stateTrie.setContext( @@ -1171,10 +1172,10 @@ export default class Blockchain extends Emittery { const runningEncodedAccounts = {}; const runningRawStorageSlots = {}; - const results = new Array(transactions.length); + const results: InternalTransactionSimulationResult[] = new Array(transactions.length); for (let i = 0; i < transactions.length; i++) { const transaction = transactions[i]; - const trace = []; + const trace: TraceEntry[] = []; const storageChanges: { address: Address; key: Buffer; @@ -1262,14 +1263,9 @@ export default class Blockchain extends Emittery { opCode === opcode.DELEGATECALL || opCode === opcode.STATICCALL) ) { - // done: drop non-call opcodes from the trace - // done: decompose the stack to parameter values - // todo: build call stack and recompute gas left and return value - // use the call stack to compute 63/64th rules - // implement additional edgecases for gas estimation // It'd be nice to show call heirarchy, either with nested calls or similar - let inLength, inOffset, value, toAddr; + let inLength: bigint, inOffset: bigint, value: bigint, toAddr: bigint; if (opCode === opcode.CALL || opCode === opcode.CALLCODE) { [inLength, inOffset, value, toAddr] = stack._store.slice(-5, -1); } else { @@ -1293,7 +1289,7 @@ export default class Blockchain extends Emittery { data.length >= 4 ? data.readUIntBE(0, 4) : 0; const target = fourBytes.get(functionSelector); - let decodedInput; + let decodedInput: {type: string, value: Quantity | Data}[]; if (target) { const parameters = target .slice(target.indexOf("(") + 1, target.length - 1) @@ -1301,23 +1297,23 @@ export default class Blockchain extends Emittery { if (parameters.length > 0 && parameters[0] !== "") { try { const decoded = rawDecode(parameters, data.subarray(4)); - decodedInput = Array(parameters.length); + decodedInput = Array(parameters.length) as any; for (let i = 0; i < parameters.length; i++) { const type = parameters[i]; const rawValue = decoded[i]; - let value: Buffer; + let value: Data | Quantity; if (Buffer.isBuffer(rawValue)) { - value = rawValue; + value = Data.from(rawValue); } else { switch (typeof rawValue) { case "string": - value = Buffer.from(rawValue, "hex"); + value = Data.from(Buffer.from(rawValue, "hex")); break; case "bigint": - value = bigIntToBuffer(rawValue); + value = Quantity.from(bigIntToBuffer(rawValue)); break; default: - value = Buffer.from(rawValue.toString(16), "hex"); + value = Data.from(Buffer.from(rawValue.toString(16), "hex")); break; } } @@ -1338,15 +1334,13 @@ export default class Blockchain extends Emittery { } } trace.push({ - opcode: Buffer.from([opCode]), - type: opcode[opCode], - from: codeAddress.buf, - to, - gas: 0n, - gasUsed: 0n, - value: value, - input: data, + opcode: Data.from(Buffer.from([opCode])), + name: opcode[opCode], + from: Address.from(codeAddress.buf), + to: Address.from(to), target, + value: value === undefined ? undefined : Quantity.from(value), + input: Data.from(data), decodedInput, pc: programCounter }); @@ -1451,25 +1445,25 @@ export default class Blockchain extends Emittery { ? maxRefund : result.execResult.gasRefund; - const gasBreakdown = { - intrinsicGas, - executionGas: result.execResult.executionGasUsed, - refund: actualRefund, - actualGasCost: totalGasSpent - actualRefund - }; + const gasBreakdown: GasBreakdown = { + total: Quantity.from(totalGasSpent), - let gasEstimate: bigint | undefined; - if (gasTracer) { - gasEstimate = gasTracer.computeGasLimit() + intrinsicGas; - } + actual: Quantity.from(totalGasSpent - actualRefund), + refund: Quantity.from(actualRefund), + + intrinsic: Quantity.from(intrinsicGas), + execution: Quantity.from(result.execResult.executionGasUsed), + + // @ts-ignore + estimate: gasTracer ? Quantity.from(gasTracer.computeGasLimit() + intrinsicGas) : undefined, + }; results[i] = { result: result.execResult, - gasBreakdown, + gas: gasBreakdown, storageChanges, stateChanges, - trace, - gasEstimate + trace }; } else { results[i] = { @@ -1478,16 +1472,19 @@ export default class Blockchain extends Emittery { exceptionError: new VmError(ERROR.OUT_OF_GAS), returnValue: BUFFER_EMPTY }, - gasBreakdown: { - intrinsicGas, - executionGas: 0n, - refund: 0n, - actualGasCost: 0n + gas: { + actual: Quantity.Zero, + refund: Quantity.Zero, + + intrinsic: Quantity.from(intrinsicGas), + execution: Quantity.Zero, + + // @ts-ignore + estimate: includeGasEstimate ? Quantity.Zero : undefined, }, storageChanges, stateChanges, - trace, - gasEstimate: 0n + trace }; } diff --git a/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts b/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts index 85c823b250..3ff5f55424 100644 --- a/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts +++ b/src/chains/ethereum/ethereum/src/data-managers/block-manager.ts @@ -58,7 +58,7 @@ export default class BlockManager extends Manager { const bm = new BlockManager(blockchain, common, blockIndexes, base); await bm.updateTaggedBlocks(); if (blockchain.fallback) { - // a hack to ensure `latest` is kept up to date. + // hack: a hack to ensure `latest` is kept up to date. // this just polls for `latest` every 7 seconds unref( setInterval(async () => { @@ -74,7 +74,7 @@ export default class BlockManager extends Manager { bm.#common, BigInt(json.number) ); - console.log("latest is now", json.number); + console.log("latest is now", parseInt(json.number)); bm.latest = new Block( BlockManager.rawFromJSON(json, common), diff --git a/src/packages/sim/app.js b/src/packages/sim/app.js index 35b90c1f1f..946fe4b288 100644 --- a/src/packages/sim/app.js +++ b/src/packages/sim/app.js @@ -92,17 +92,19 @@ document.addEventListener('DOMContentLoaded', () => { // `evm_simulateTransactions` call: transactions.addEventListener('change', formatJson); advancedOptions.addEventListener('change', formatJson); - let pre = ""; + const preFetchCache = new Set(); advancedOptions.addEventListener('change', async () => { // prefetch when advanced options change try { const jsonRPC = JSON.stringify(JSON.parse(requestElement.dataset.json)); - if (jsonRPC === pre) return; - pre = jsonRPC; + if (preFetchCache.has(jsonRPC)) return; + preFetchCache.add(jsonRPC); + console.log("prefetch"); + await fetch('/simulate', { method: 'POST', headers: { - 'Content-Type': 'applicaiton/json', + 'Content-Type': 'application/json', }, body: jsonRPC, }); @@ -114,6 +116,8 @@ document.addEventListener('DOMContentLoaded', () => { const responseElement = document.getElementById("responseBody"); document.querySelector("form").addEventListener('submit', async (event) => { + preFetchCache.clear(); + event.preventDefault(); // disable the submit button: document.querySelector("form button").disabled = true; @@ -126,7 +130,7 @@ document.addEventListener('DOMContentLoaded', () => { const response = await fetch('/simulate', { method: 'POST', headers: { - 'Content-Type': 'applicaiton/json', + 'Content-Type': 'application/json', }, body: JSON.stringify(jsonRPC), }); diff --git a/src/packages/sim/docs.html b/src/packages/sim/docs.html new file mode 100644 index 0000000000..7143bf1e31 --- /dev/null +++ b/src/packages/sim/docs.html @@ -0,0 +1,263 @@ + + + + + + Ethereum Transactions + + + + + + + + + + +
+
+ + + + + + + ganache logo +

Ganache

+
+ +
+
+
+
+

Simulator Documentation

+
+

The endpoint to use is {URL}. The RPC method is named evm_simulateTransactions.

+

Send requests just as you would any Ethereum JSON-RPC 2.0 request.

+

RPC requests temporarily return a top level `durationMs` property (alongside id, jsonrpc, and result/error) which represents the amount of time + the request took to process in milliseconds. You can use this value to better understand how RTT to the RPC host will affect users in different areas of the world. +

+
+
+
+

Request

+
+

+ Params +

+
+
+
+
+
[
+    {
+        transactions: Transaction[],
+        estimateGas?: boolean = false,
+        trace?: boolean = false
+    },
+    /**
+     * The block number the transaction should be simulated after. This is how
+     * `eth_call` works, but differs from how Tenderly works.
+     */
+    blockNumberHex | TAG = "latest"
+]
+
+
+
+
+

+ Transaction +

+
+
+
+
+
{
+    from: string,
+    to?: string,
+    /**
+     * The maximum amount of gas to use for the transaction. This value is also used as an upper limit for gas estimations.
+     */
+    gas?: string,
+    gasPrice?: string,
+    value?: string,
+    data?: string,
+}
+
+
+
+
+
+
+
+

Response

+
+
+
+
+
+
{
+    error?: {
+        code: number;
+        message: string;
+    };
+    returnValue?: string;
+    gas: GasBreakdown;
+    logs: Log[];
+    storageChanges: StorageChange[];
+    stateChanges: StateChange[];
+    receipts?: string[];
+    /**
+     * The trace of the transaction. This is only returned if `trace` is set to true.
+     */
+    trace?: TraceEntry[];
+}
+
+
+
+
+

+ GasBreakdown +

+
+
+
+
+
{
+    /**
+     * The total amount of gas used by the transaction.
+     */
+    total: Quantity,
+
+    /**
+     * Total gas used minus the refund. This is what etherscan reports as `Gas Usage`.
+     */
+    actual: Quantity;
+
+    /**
+     * The amount of gas refunded to the sender.
+     */
+    refund: Quantity;
+
+    /**
+     * The amount of gas the EVM requires before it would attempt to run the
+     * transaction.
+     */
+    intrinsic: Quantity;
+
+    /**
+     * The amount of gas used by the transaction's actual execution.
+     */
+    execution: Quantity;
+
+    /**
+     * The minimum amount of gas required to run the transaction. This is only returned if `estimateGas` is set to true.
+     */
+    estimate?: Estimate extends true ? Quantity : undefined;
+}
+
+
+
+
+

+ StorageChange +

+
+
+
+
+
{
+    key: string;
+    address: string;
+    before: string;
+    after: string;
+}
+
+
+
+
+

+ StateChange +

+
+
+
+
+
{
+    address: string;
+    from: {
+        nonce: string;
+        balance: string;
+        storageRoot: string;
+        codeHash: string;
+    };
+    to: {
+        nonce: string;
+        balance: string;
+        storageRoot: string;
+        codeHash: string;
+    };
+}
+
+
+
+
+

+ TraceEntry +

+
+
+
+
+
{
+    /**
+     * The opcode of the trace entry.
+     * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL
+     *
+     */
+    opcode: string;
+    /**
+     * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL)
+     */
+    name: string;
+    from: string;
+    to: string;
+    value: string;
+    input: string;
+    pc: number;
+    target?: string;
+    decodedInput?: {type: string, value: string }[];
+}
+
+
+
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/packages/sim/index.html b/src/packages/sim/index.html index d10f82b859..c5b3c5c47a 100644 --- a/src/packages/sim/index.html +++ b/src/packages/sim/index.html @@ -47,13 +47,21 @@ ganache logo

Ganache

+

Transactions Simulator

-
+
@@ -159,6 +167,7 @@

Transactions Simulator

id="gasLimit" name="gasLimit" required + placeholder="50000000" value="50000000" pattern="^[0-9]+$|^(0x[a-fA-F0-9]+)$" /> @@ -221,7 +230,9 @@

Transactions Simulator

The string "latest" or a block number as an - integer or 0x prefixed hex. + integer or 0x prefixed hex. The transasctions will + be simulated in the block immediately following + the specified block number.
@@ -230,6 +241,7 @@

Transactions Simulator

id="blockNumber" name="block" required + placeholder="latest" value="latest" pattern="^latest$|^[0-9]+$|^(0x[a-fA-F0-9]+)$" /> @@ -249,11 +261,13 @@

Transactions Simulator

name="gasEstimation" required > - + + -
@@ -274,7 +288,7 @@

Transactions Simulator

> - +
diff --git a/src/packages/sim/index.ts b/src/packages/sim/index.ts index 1f192487db..1e2d9d8c27 100644 --- a/src/packages/sim/index.ts +++ b/src/packages/sim/index.ts @@ -1,6 +1,11 @@ import http from "http"; import fs from "fs"; +let remote = false; +const hostname = remote ? "3.140.186.190" : "localhost"; +const port = remote ? 8080 : 8545 + const index = fs.readFileSync(__dirname + "/index.html"); +const docs = fs.readFileSync(__dirname + "/docs.html"); const results = fs.readFileSync(__dirname + "/results.html"); const css = fs.readFileSync(__dirname + "/main.css"); const rootCss = fs.readFileSync(__dirname + "/root.css"); @@ -9,12 +14,16 @@ const jsonview = fs.readFileSync(__dirname + "/jsonview.js"); const chevron = fs.readFileSync(__dirname + "/chevron.svg"); const ganache = fs.readFileSync(__dirname + "/ganache.svg"); -const port = 3000; + const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/") { res.writeHead(200, { "Content-Type": "text/html" }); res.end(index); + } else if (req.method === "GET" && req.url === "/docs") { + res.writeHead(200, { "Content-Type": "text/html" }); + const docsHtml = docs.toString("utf-8").replace("{URL}", `http://${hostname}:${port}`) + res.end(docsHtml); } else if (req.method === "GET" && req.url === "/jsonview.js") { res.writeHead(200, { "Content-Type": "application/javascript" }); res.end(jsonview); @@ -41,10 +50,10 @@ const server = http.createServer((req, res) => { // send the POST request to the simulation server // we just take the body from the request and send it to the simulation server // and then return the result directly to the user: - let remote = false; + const options = { - hostname: remote ? "3.140.186.190" : "localhost", - port: remote ? 8080 : 8545, + hostname: hostname, + port: port, path: "/", method: "POST", headers: { @@ -79,6 +88,6 @@ const server = http.createServer((req, res) => { } }); -server.listen(port, () => { - console.log(`Server is running on http://localhost:${port}`); +server.listen(3000, () => { + console.log(`Server is running on http://localhost:${3000}`); }); diff --git a/src/packages/sim/main.css b/src/packages/sim/main.css index 503d6e559d..c4f992c0a6 100644 --- a/src/packages/sim/main.css +++ b/src/packages/sim/main.css @@ -80,7 +80,7 @@ button.run-button { margin: 1rem auto; position: relative; } -.content { +.content.split { grid-gap: 2rem; display: grid; gap: 2rem; From 7f9aff12c6d7aa6f5f0eca3f375fcd9e94d76126 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Wed, 14 Jun 2023 17:05:23 -0400 Subject: [PATCH 03/13] docs --- src/chains/ethereum/ethereum/src/api.ts | 28 +- .../ethereum/ethereum/src/blockchain.ts | 45 ++- .../src/forking/handlers/http-handler.ts | 17 +- src/packages/ganache/npm-shrinkwrap.json | 4 +- src/packages/ganache/package.json | 2 +- src/packages/sim/docs.html | 263 --------------- src/packages/sim/docs.md | 300 ++++++++++++++++++ src/packages/sim/index.html | 12 +- src/packages/sim/index.ts | 9 +- src/packages/sim/types.ts | 179 +++++++++++ 10 files changed, 545 insertions(+), 314 deletions(-) delete mode 100644 src/packages/sim/docs.html create mode 100644 src/packages/sim/docs.md create mode 100644 src/packages/sim/types.ts diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 6876ce222b..90359b75aa 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -92,7 +92,7 @@ export type GasBreakdown = { /** * The total amount of gas used by the transaction. */ - total: Quantity, + total: Quantity; /** * Total gas used minus the refund. This is what etherscan reports as `Gas Usage`. @@ -127,10 +127,10 @@ export type TraceEntry = { from: Address; to: Address; value: Quantity; - input: Data; pc: number; - target?: string; - decodedInput?: {type: string, value: Quantity | Data }[]; + data: Data; + signature?: string; + args?: { type: string; value: Quantity | Data }[]; }; type TransactionSimulationResult = { returnValue: Data; @@ -3042,10 +3042,14 @@ export default class EthereumApi implements Api { */ @assertArgLength(1, 2) async evm_simulateTransactions( - { overrides, transactions, trace, estimateGas }: TransactionSimulationArgs, + { + overrides, + transactions, + trace, + estimateGas + }: TransactionSimulationArgs, blockNumber: QUANTITY | Ethereum.Tag = Tag.latest ): Promise[]> { - const includeTrace = trace === true; const simulatedTransactionResults = await simulateTransactions( @@ -3060,13 +3064,7 @@ export default class EthereumApi implements Api { ); return simulatedTransactionResults.map( - ({ - trace, - gas, - result, - storageChanges, - stateChanges - }) => { + ({ trace, gas, result, storageChanges, stateChanges }) => { const parsedStorageChanges = storageChanges.map(change => ({ key: Data.from(change.key), address: Address.from(change.address.buf), @@ -3113,9 +3111,7 @@ export default class EthereumApi implements Api { receipts: undefined, storageChanges: parsedStorageChanges, stateChanges: parsedStateChanges, - trace: includeTrace - ? trace - : undefined + trace: includeTrace ? trace : undefined }; } ); diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index de0cdd2d13..0e71c023a0 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -81,7 +81,11 @@ import { TrieDB } from "./trie-db"; import { Trie } from "@ethereumjs/trie"; import { Interpreter, RunState } from "@ethereumjs/evm/dist/interpreter"; import { GasTracer } from "./helpers/gas"; -import { GasBreakdown, InternalTransactionSimulationResult, TraceEntry } from "./api"; +import { + GasBreakdown, + InternalTransactionSimulationResult, + TraceEntry +} from "./api"; const mclInitPromise = mcl.init(mcl.BLS12_381).then(() => { mcl.setMapToMode(mcl.IRTF); // set the right map mode; otherwise mapToG2 will return wrong values. @@ -1172,7 +1176,9 @@ export default class Blockchain extends Emittery { const runningEncodedAccounts = {}; const runningRawStorageSlots = {}; - const results: InternalTransactionSimulationResult[] = new Array(transactions.length); + const results: InternalTransactionSimulationResult[] = new Array( + transactions.length + ); for (let i = 0; i < transactions.length; i++) { const transaction = transactions[i]; const trace: TraceEntry[] = []; @@ -1265,7 +1271,10 @@ export default class Blockchain extends Emittery { ) { // It'd be nice to show call heirarchy, either with nested calls or similar - let inLength: bigint, inOffset: bigint, value: bigint, toAddr: bigint; + let inLength: bigint, + inOffset: bigint, + value: bigint, + toAddr: bigint; if (opCode === opcode.CALL || opCode === opcode.CALLCODE) { [inLength, inOffset, value, toAddr] = stack._store.slice(-5, -1); } else { @@ -1287,17 +1296,17 @@ export default class Blockchain extends Emittery { const to = bigIntToBuffer(toAddr); const functionSelector = data.length >= 4 ? data.readUIntBE(0, 4) : 0; - const target = fourBytes.get(functionSelector); + const signature = fourBytes.get(functionSelector); - let decodedInput: {type: string, value: Quantity | Data}[]; - if (target) { - const parameters = target - .slice(target.indexOf("(") + 1, target.length - 1) + let args: { type: string; value: Quantity | Data }[]; + if (signature) { + const parameters = signature + .slice(signature.indexOf("(") + 1, signature.length - 1) .split(","); if (parameters.length > 0 && parameters[0] !== "") { try { const decoded = rawDecode(parameters, data.subarray(4)); - decodedInput = Array(parameters.length) as any; + args = Array(parameters.length) as any; for (let i = 0; i < parameters.length; i++) { const type = parameters[i]; const rawValue = decoded[i]; @@ -1313,12 +1322,14 @@ export default class Blockchain extends Emittery { value = Quantity.from(bigIntToBuffer(rawValue)); break; default: - value = Data.from(Buffer.from(rawValue.toString(16), "hex")); + value = Data.from( + Buffer.from(rawValue.toString(16), "hex") + ); break; } } - decodedInput[i] = { + args[i] = { type, value }; @@ -1338,10 +1349,10 @@ export default class Blockchain extends Emittery { name: opcode[opCode], from: Address.from(codeAddress.buf), to: Address.from(to), - target, + signature, value: value === undefined ? undefined : Quantity.from(value), - input: Data.from(data), - decodedInput, + data: Data.from(data), + args, pc: programCounter }); } @@ -1455,7 +1466,9 @@ export default class Blockchain extends Emittery { execution: Quantity.from(result.execResult.executionGasUsed), // @ts-ignore - estimate: gasTracer ? Quantity.from(gasTracer.computeGasLimit() + intrinsicGas) : undefined, + estimate: gasTracer + ? Quantity.from(gasTracer.computeGasLimit() + intrinsicGas) + : undefined }; results[i] = { @@ -1480,7 +1493,7 @@ export default class Blockchain extends Emittery { execution: Quantity.Zero, // @ts-ignore - estimate: includeGasEstimate ? Quantity.Zero : undefined, + estimate: includeGasEstimate ? Quantity.Zero : undefined }, storageChanges, stateChanges, diff --git a/src/chains/ethereum/ethereum/src/forking/handlers/http-handler.ts b/src/chains/ethereum/ethereum/src/forking/handlers/http-handler.ts index 25118446d4..1f9382410d 100644 --- a/src/chains/ethereum/ethereum/src/forking/handlers/http-handler.ts +++ b/src/chains/ethereum/ethereum/src/forking/handlers/http-handler.ts @@ -10,10 +10,19 @@ import { BaseHandler } from "./base-handler"; import { Handler } from "../types"; import Deferred from "../deferred"; -const net = require("net"); -// work around a node v20 bug: https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 -if (net.setDefaultAutoSelectFamily) { - net.setDefaultAutoSelectFamily(false); +// Work around a node v20.0.0, v20.1.0, and v20.1.2 bug. The issue was fixed +// in v20.3.0. +// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 +// Safe to remove once support for Node v20 is dropped. +if ( + // webpack will replace process.env.IS_BROWSER with a boolean + !process.env.IS_BROWSER && + process.versions && + // check for `node` in case we want to use this in deno/bun/etc + process.versions.node && + process.versions.node.match(/20\.[0-2]\.0/) +) { + require("net").setDefaultAutoSelectFamily(false); } const { JSONRPC_PREFIX } = BaseHandler; diff --git a/src/packages/ganache/npm-shrinkwrap.json b/src/packages/ganache/npm-shrinkwrap.json index 71808fbf37..57d5283b04 100644 --- a/src/packages/ganache/npm-shrinkwrap.json +++ b/src/packages/ganache/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "ganache", - "version": "7.8.0-transaction-simulation", + "version": "7.8.0-tx-sim.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ganache", - "version": "7.8.0-transaction-simulation", + "version": "7.8.0-tx-sim.0", "license": "MIT", "dependencies": { "@trufflesuite/bigint-buffer": "1.1.10", diff --git a/src/packages/ganache/package.json b/src/packages/ganache/package.json index 0256ae34b2..90bc9daf2b 100644 --- a/src/packages/ganache/package.json +++ b/src/packages/ganache/package.json @@ -1,6 +1,6 @@ { "name": "ganache", - "version": "7.8.0-transaction-simulation", + "version": "7.8.0-tx-sim.0", "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/sim/docs.html b/src/packages/sim/docs.html deleted file mode 100644 index 7143bf1e31..0000000000 --- a/src/packages/sim/docs.html +++ /dev/null @@ -1,263 +0,0 @@ - - - - - - Ethereum Transactions - - - - - - - - - - -
-
- - - - - - - ganache logo -

Ganache

-
- -
-
-
-
-

Simulator Documentation

-
-

The endpoint to use is {URL}. The RPC method is named evm_simulateTransactions.

-

Send requests just as you would any Ethereum JSON-RPC 2.0 request.

-

RPC requests temporarily return a top level `durationMs` property (alongside id, jsonrpc, and result/error) which represents the amount of time - the request took to process in milliseconds. You can use this value to better understand how RTT to the RPC host will affect users in different areas of the world. -

-
-
-
-

Request

-
-

- Params -

-
-
-
-
-
[
-    {
-        transactions: Transaction[],
-        estimateGas?: boolean = false,
-        trace?: boolean = false
-    },
-    /**
-     * The block number the transaction should be simulated after. This is how
-     * `eth_call` works, but differs from how Tenderly works.
-     */
-    blockNumberHex | TAG = "latest"
-]
-
-
-
-
-

- Transaction -

-
-
-
-
-
{
-    from: string,
-    to?: string,
-    /**
-     * The maximum amount of gas to use for the transaction. This value is also used as an upper limit for gas estimations.
-     */
-    gas?: string,
-    gasPrice?: string,
-    value?: string,
-    data?: string,
-}
-
-
-
-
-
-
-
-

Response

-
-
-
-
-
-
{
-    error?: {
-        code: number;
-        message: string;
-    };
-    returnValue?: string;
-    gas: GasBreakdown;
-    logs: Log[];
-    storageChanges: StorageChange[];
-    stateChanges: StateChange[];
-    receipts?: string[];
-    /**
-     * The trace of the transaction. This is only returned if `trace` is set to true.
-     */
-    trace?: TraceEntry[];
-}
-
-
-
-
-

- GasBreakdown -

-
-
-
-
-
{
-    /**
-     * The total amount of gas used by the transaction.
-     */
-    total: Quantity,
-
-    /**
-     * Total gas used minus the refund. This is what etherscan reports as `Gas Usage`.
-     */
-    actual: Quantity;
-
-    /**
-     * The amount of gas refunded to the sender.
-     */
-    refund: Quantity;
-
-    /**
-     * The amount of gas the EVM requires before it would attempt to run the
-     * transaction.
-     */
-    intrinsic: Quantity;
-
-    /**
-     * The amount of gas used by the transaction's actual execution.
-     */
-    execution: Quantity;
-
-    /**
-     * The minimum amount of gas required to run the transaction. This is only returned if `estimateGas` is set to true.
-     */
-    estimate?: Estimate extends true ? Quantity : undefined;
-}
-
-
-
-
-

- StorageChange -

-
-
-
-
-
{
-    key: string;
-    address: string;
-    before: string;
-    after: string;
-}
-
-
-
-
-

- StateChange -

-
-
-
-
-
{
-    address: string;
-    from: {
-        nonce: string;
-        balance: string;
-        storageRoot: string;
-        codeHash: string;
-    };
-    to: {
-        nonce: string;
-        balance: string;
-        storageRoot: string;
-        codeHash: string;
-    };
-}
-
-
-
-
-

- TraceEntry -

-
-
-
-
-
{
-    /**
-     * The opcode of the trace entry.
-     * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL
-     *
-     */
-    opcode: string;
-    /**
-     * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL)
-     */
-    name: string;
-    from: string;
-    to: string;
-    value: string;
-    input: string;
-    pc: number;
-    target?: string;
-    decodedInput?: {type: string, value: string }[];
-}
-
-
-
-
-
-
-
-
-
- - - - - \ No newline at end of file diff --git a/src/packages/sim/docs.md b/src/packages/sim/docs.md new file mode 100644 index 0000000000..6ebd51573c --- /dev/null +++ b/src/packages/sim/docs.md @@ -0,0 +1,300 @@ +# Simulate Transactions Documentation + +The RPC method is named `evm_simulateTransactions`. + +Send requests just as you would any Ethereum JSON-RPC 2.0 request. + +RPC requests temporarily return a top level `durationMs` property (alongside id, jsonrpc, and result/error) which represents the amount of time the request took to process in milliseconds. You can use this value to better understand how RTT to the RPC host will affect users in different areas of the world. + +## TypeScript Interface + +Also see [types.ts](./types.ts). + +```typescript +/** + * 0x prefixed hex pairs, e.g., `0x0123` + */ +export type DATA = string; + +/** + * 0x prefixed 20 byte ethereum address + */ +export type ADDRESS = DATA; + +/** + * 0x-prefixed compact hex string, e.g., `0x123` + */ +export type QUANTITY = string; + +/** + * Request params for sending an `evm_simulateTransactions` RPC request. + */ +export type SimulationRequestParams = [ + { + /** + * The transactions you want to simulate, in order. + */ + transactions: Transaction[]; + /** + * `true` to compute and return a gas estimate, otherwise `false`. + * The gas estimate returned here uses the `gas` limit set in each transaction. + * This means that if the transaction runs out of gas at the user-specified + * limit the gas estimate will reflect the amount of gas required to get + * to the point at which it ran out of gas. + */ + estimateGas?: boolean; + /** + * `true` to return a transaction CALL* trace, otherwise `false`. + */ + trace?: boolean; + + overrides: { + [address: ADDRESS]: StateOverride; + }; + }, + /** + * The block number the transaction should be simulated on top of. This is how + * `eth_call` works, but differs from how Tenderly works. + */ + QUANTITY | "latest" +]; + +/** + * Identical to the `eth_call` overrides argument + */ +export type StateOverride = Partial< + ( + | { state: { [slot: DATA]: DATA } } + | { stateDiff: { [slot: DATA]: DATA } } + ) & { + code: DATA; + nonce: QUANTITY; + balance: QUANTITY; + } +>; + +/** + * The transaction to simulate. + */ +export type Transaction = { + from: ADDRESS; + to?: ADDRESS; + /** + * This value is also used as an upper limit for gas estimations. + */ + gas?: QUANTITY; + gasPrice?: QUANTITY; + value?: QUANTITY; + data?: DATA; +}; + +/** + * The response to an `evm_simulateTransactions` RPC request. + */ +export type SimulationResponse = SimulationResult[]; + +export type SimulationResult = { + error?: { + code: number; + message: string; + }; + returnValue?: DATA; + gas: GasBreakdown; + logs: Log[]; + storageChanges: StorageChange[]; + stateChanges: StateChange[]; + receipts?: DATA[]; + /** + * The trace of the transaction. This is only returned if `trace` is set to true. + */ + trace?: TraceEntry[]; +}; + +export type Log = [address: ADDRESS, topics: DATA[], data: DATA]; + +export type GasBreakdown = { + /** + * The total amount of gas used by the transaction. + */ + total: QUANTITY; + + /** + * Total gas used minus the refund. This is what etherscan reports as `Gas Usage`. + */ + actual: QUANTITY; + + /** + * The amount of gas refunded to the sender. + */ + refund: QUANTITY; + + /** + * The amount of gas the EVM requires before it would attempt to run the + * transaction. + */ + intrinsic: QUANTITY; + + /** + * The amount of gas used by the transaction's actual execution. + */ + execution: QUANTITY; + + /** + * The minimum amount of gas required to run the transaction. This is only returned if `estimateGas` is set to true. + */ + estimate?: QUANTITY; +}; + +type StorageChange = { + key: DATA; + address: ADDRESS; + before: DATA; + after: DATA; +}; + +type StateChange = { + address: ADDRESS; + from: { + nonce: QUANTITY; + balance: QUANTITY; + storageRoot: DATA; + codeHash: DATA; + }; + to: { + nonce: QUANTITY; + balance: QUANTITY; + storageRoot: DATA; + codeHash: DATA; + }; +}; + +export type TraceEntry = { + /** + * The opcode of the trace entry. + * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL + * + */ + opcode: DATA; + /** + * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL) + */ + name: string; + from: ADDRESS; + to: ADDRESS; + value: QUANTITY; + pc: number; + data?: string; + /** + * Decoded function signature (via 4byte directory) + */ + signature?: string; + args?: { type: string; value: DATA | QUANTITY }[]; +}; +``` + +## Example Usage + +```typescript +/** + * Sends a JSON-RPC request to the `evm_simulateTransactions` endpoint. + * + * @param {SimulationRequestParams} params - The request parameters. + * @param {string} endpoint - The endpoint URL. + * @returns {Promise} - The response from the endpoint. + */ +async function sendSimulationRequest( + params: SimulationRequestParams, + endpoint: string +): Promise { + const data = { + jsonrpc: "2.0", + method: "evm_simulateTransactions", + params: params, + id: 1 + }; + + try { + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }); + + const json = await response.json(); + return json.result as SimulationResponse; + } catch (error) { + throw error; + } +} + +/** + * Logs information about gas usage and transaction success. + * + * @param {SimulationResponse} response - The simulation response. + */ +function logGasUsageAndSuccess(response: SimulationResponse) { + response.forEach((result: SimulationResult, index: number) => { + console.log(`Transaction ${index + 1} Gas Usage:`); + console.log("Total:", BigInt(result.gas.total).toLocaleString()); + console.log("Actual:", BigInt(result.gas.actual).toLocaleString()); + console.log("Refund:", BigInt(result.gas.refund).toLocaleString()); + console.log("Intrinsic:", BigInt(result.gas.intrinsic).toLocaleString()); + console.log("Execution:", BigInt(result.gas.execution).toLocaleString()); + if (result.gas.estimate) { + console.log("Estimate:", BigInt(result.gas.estimate).toLocaleString()); + } + + console.log( + `Transaction ${index + 1} Status:`, + !result.error ? "Successful" : "Failed" + ); + if (result.error) { + console.log("Failure Reason:", result.error.message); + } + console.log("---"); + }); + + console.log("Total Gas Usage:"); + const totalGas = response.reduce((acc: bigint, result: SimulationResult) => { + const gasUsed = BigInt(result.gas.actual); + return acc + gasUsed; + }, 0n); + console.log(totalGas.toLocaleString()); + console.log("---"); +} + +// Example usage: +const endpoint = "https://example.com/jsonrpc"; // Replace with your actual Ganache simulator endpoint URL +const simulationParams = [ + { + transactions: [ + { + from: "0xd7c2b5c77f0ba843d863e1ed488d40472de53ec9", + to: "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + gas: "0x8000000000000000", + data: "0x095ea7b300000000000000000000000082e0b8cdd80af5930c4452c684e71c861148ec8a000000000000000000000000000000000000000000000002817b497c9ca44000" + }, + { + from: "0xd7c2b5c77f0ba843d863e1ed488d40472de53ec9", + to: "0x82e0b8cdd80af5930c4452c684e71c861148ec8a", + gas: "0x8000000000000000", + data: "0x3ce33bff00000000000000000000000000000000000000000000000000000000000000800000000000000000000000007d1afa7b718fb893db30a3abc0cfc608aacfebb0000000000000000000000000000000000000000000000002817b497c9ca4400000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000b6c6966694164617074657200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000005d3675d698a3dd53e3457951e1debef717a29a720000000000000000000000005d3675d698a3dd53e3457951e1debef717a29a7200000000000000000000000000000000000000000000000000000000000000890000000000000000000000007d1afa7b718fb893db30a3abc0cfc608aacfebb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027bde5e48a43b22000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000059ceb33f8691e00000000000000000000000000e6b738da243e8fa2a0ed5915645789add5de5152000000000000000000000000000000000000000000000000000000000000009033619a2d20e0f32c0ab1598bd7c2b5c77f0ba843d863e1ed488d40472de53ec9000000897d1afa7b718fb893db30a3abc0cfc608aacfebb000000000000000027bde5e48a43b220000000000000000025b84aa3e4b74b24600000000000000000000000000000000000000000000000000000000000000000000000022b1cbb8d98a01a3b71d034bb899775a76eb1cc200000000000000000000000000000000" + } + ], + estimateGas: true, + trace: true + }, + "0x10ab97e" // block number to simulate on top of +] as SimulationRequestParams; + +sendSimulationRequest(simulationParams, endpoint) + .then(response => { + console.log("Simulation Response:", response); + logGasUsageAndSuccess(response); + }) + .catch(error => { + console.error("Error:", error); + }); +``` diff --git a/src/packages/sim/index.html b/src/packages/sim/index.html index c5b3c5c47a..cc11606285 100644 --- a/src/packages/sim/index.html +++ b/src/packages/sim/index.html @@ -48,12 +48,12 @@

Ganache

@@ -261,7 +261,9 @@

Transactions Simulator

name="gasEstimation" required > - + @@ -288,7 +290,9 @@

Transactions Simulator

> - +
diff --git a/src/packages/sim/index.ts b/src/packages/sim/index.ts index 1e2d9d8c27..c5b61c7546 100644 --- a/src/packages/sim/index.ts +++ b/src/packages/sim/index.ts @@ -2,10 +2,9 @@ import http from "http"; import fs from "fs"; let remote = false; const hostname = remote ? "3.140.186.190" : "localhost"; -const port = remote ? 8080 : 8545 +const port = remote ? 8080 : 8545; const index = fs.readFileSync(__dirname + "/index.html"); -const docs = fs.readFileSync(__dirname + "/docs.html"); const results = fs.readFileSync(__dirname + "/results.html"); const css = fs.readFileSync(__dirname + "/main.css"); const rootCss = fs.readFileSync(__dirname + "/root.css"); @@ -14,16 +13,10 @@ const jsonview = fs.readFileSync(__dirname + "/jsonview.js"); const chevron = fs.readFileSync(__dirname + "/chevron.svg"); const ganache = fs.readFileSync(__dirname + "/ganache.svg"); - - const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/") { res.writeHead(200, { "Content-Type": "text/html" }); res.end(index); - } else if (req.method === "GET" && req.url === "/docs") { - res.writeHead(200, { "Content-Type": "text/html" }); - const docsHtml = docs.toString("utf-8").replace("{URL}", `http://${hostname}:${port}`) - res.end(docsHtml); } else if (req.method === "GET" && req.url === "/jsonview.js") { res.writeHead(200, { "Content-Type": "application/javascript" }); res.end(jsonview); diff --git a/src/packages/sim/types.ts b/src/packages/sim/types.ts new file mode 100644 index 0000000000..9802a016f4 --- /dev/null +++ b/src/packages/sim/types.ts @@ -0,0 +1,179 @@ +/** + * 0x prefixed hex pairs, e.g., `0x0123` + */ +export type DATA = string; + +/** + * 0x prefixed 20 byte ethereum address + */ +export type ADDRESS = DATA; + +/** + * 0x-prefixed compact hex string, e.g., `0x123` + */ +export type QUANTITY = string; + +/** + * Request params for sending an `evm_simulateTransactions` RPC request. + */ +export type SimulationRequestParams = [ + { + /** + * The transactions you want to simulate, in order. + */ + transactions: Transaction[]; + /** + * `true` to compute and return a gas estimate, otherwise `false`. + * The gas estimate returned here uses the `gas` limit set in each transaction. + * This means that if the transaction runs out of gas at the user-specified + * limit the gas estimate will reflect the amount of gas required to get + * to the point at which it ran out of gas. + */ + estimateGas?: boolean; + /** + * `true` to return a transaction CALL* trace, otherwise `false`. + */ + trace?: boolean; + + overrides: { + [address: ADDRESS]: StateOverride; + }; + }, + /** + * The block number the transaction should be simulated on top of. This is how + * `eth_call` works, but differs from how Tenderly works. + */ + QUANTITY | "latest" +]; + +/** + * Identical to the `eth_call` overrides argument + */ +export type StateOverride = Partial< + ( + | { state: { [slot: DATA]: DATA } } + | { stateDiff: { [slot: DATA]: DATA } } + ) & { + code: DATA; + nonce: QUANTITY; + balance: QUANTITY; + } +>; + +/** + * The transaction to simulate. + */ +export type Transaction = { + from: ADDRESS; + to?: ADDRESS; + /** + * This value is also used as an upper limit for gas estimations. + */ + gas?: QUANTITY; + gasPrice?: QUANTITY; + value?: QUANTITY; + data?: DATA; +}; + +/** + * The response to an `evm_simulateTransactions` RPC request. + */ +export type SimulationResponse = SimulationResult[]; + +export type SimulationResult = { + error?: { + code: number; + message: string; + }; + returnValue?: DATA; + gas: GasBreakdown; + logs: Log[]; + storageChanges: StorageChange[]; + stateChanges: StateChange[]; + receipts?: DATA[]; + /** + * The trace of the transaction. This is only returned if `trace` is set to true. + */ + trace?: TraceEntry[]; +}; + +export type Log = [address: ADDRESS, topics: DATA[], data: DATA]; + +export type GasBreakdown = { + /** + * The total amount of gas used by the transaction. + */ + total: QUANTITY; + + /** + * Total gas used minus the refund. This is what etherscan reports as `Gas Usage`. + */ + actual: QUANTITY; + + /** + * The amount of gas refunded to the sender. + */ + refund: QUANTITY; + + /** + * The amount of gas the EVM requires before it would attempt to run the + * transaction. + */ + intrinsic: QUANTITY; + + /** + * The amount of gas used by the transaction's actual execution. + */ + execution: QUANTITY; + + /** + * The minimum amount of gas required to run the transaction. This is only returned if `estimateGas` is set to true. + */ + estimate?: QUANTITY; +}; + +type StorageChange = { + key: DATA; + address: ADDRESS; + before: DATA; + after: DATA; +}; + +type StateChange = { + address: ADDRESS; + from: { + nonce: QUANTITY; + balance: QUANTITY; + storageRoot: DATA; + codeHash: DATA; + }; + to: { + nonce: QUANTITY; + balance: QUANTITY; + storageRoot: DATA; + codeHash: DATA; + }; +}; + +export type TraceEntry = { + /** + * The opcode of the trace entry. + * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL + * + */ + opcode: DATA; + /** + * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL) + */ + name: string; + from: ADDRESS; + to: ADDRESS; + value: QUANTITY; + pc: number; + data?: string; + /** + * Decoded function signature (via 4byte directory) + */ + signature?: string; + args?: { type: string; value: DATA | QUANTITY }[]; +}; From dd2cc12f0de43313b5789428b199613f4534804e Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Wed, 14 Jun 2023 17:44:19 -0400 Subject: [PATCH 04/13] fix simulator --- src/packages/sim/app.js | 4 ++-- src/packages/sim/index.html | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/packages/sim/app.js b/src/packages/sim/app.js index 946fe4b288..159eaa4c28 100644 --- a/src/packages/sim/app.js +++ b/src/packages/sim/app.js @@ -48,7 +48,7 @@ document.addEventListener('DOMContentLoaded', () => { const value = element.value.trim(); if (value) { if ("SELECT" === element.tagName) { - tx[element.name] = value; + tx[element.name] = value === "true"; } else { if (element.getAttribute("pattern")) { tx[element.name] = value.toLowerCase().startsWith("0x") ? value : "0x" + parseInt(value).toString(16); @@ -65,7 +65,7 @@ document.addEventListener('DOMContentLoaded', () => { const value = element.value.trim(); if (value) { if ("SELECT" === element.tagName) { - json.params[0][element.name] = value; + json.params[0][element.name] = value === "true"; } else { if (element.getAttribute("pattern")) { if (element.name === "block" && value.toLowerCase() === "latest") { diff --git a/src/packages/sim/index.html b/src/packages/sim/index.html index cc11606285..6e42302662 100644 --- a/src/packages/sim/index.html +++ b/src/packages/sim/index.html @@ -261,13 +261,13 @@

Transactions Simulator

name="gasEstimation" required > - - - @@ -288,11 +288,9 @@

Transactions Simulator

name="trace" required > - - - + + +
From bc10ad7844dea37083b5d2a18f4e6d93efc83ddf Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 15 Jun 2023 16:17:16 +1200 Subject: [PATCH 05/13] Add comments to clarify calculations in node traversal, within minimum gas algorithm --- .../ethereum/ethereum/src/helpers/gas.ts | 75 +++++++++++++------ 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/helpers/gas.ts b/src/chains/ethereum/ethereum/src/helpers/gas.ts index ff44bf61d7..adc124d19f 100644 --- a/src/chains/ethereum/ethereum/src/helpers/gas.ts +++ b/src/chains/ethereum/ethereum/src/helpers/gas.ts @@ -170,12 +170,11 @@ function max(a: bigint, b: bigint) { /** * Computes the actual and required gas costs of the node. * - * @param child The node to compute the gas costs of. + * @param node The node to compute the gas costs of. */ -function _computeGas(child: Node) { - const { children, cost, minimum, stipend } = child; +function _computeGas(node: Node) { + const { children, cost, minimum, stipend } = node; if (children.length === 0) { - (child as any).computed = minimum; return { cost: max(cost - stipend, 0n), minimum: minimum @@ -183,13 +182,52 @@ function _computeGas(child: Node) { } else { let totalMinimum = 0n; let totalCost = 0n; + + // At any point, `totalMinimum` represents the total minimum gas required to + // execute the nodes in `this.children` up to that point. This node's cost + // will be considered at the end. + // + + // As we iterate over a child, `totalMinimum` becomes the total minimum gas + // required to execute the nodes in `this.children` up to and including that + // node. At this point, `totalMinimum` may be greater than (`totalCost` + + // `minimum`) if previous nodes had a signficant `minimum > cost` overhead. + // ie. + // + // Given nodes: + // { cost: 10, minimum: 10 }, + // { cost: 10, minimum: 50 }, + // { cost: 10, minimum: 30 }, + // + // Before executing the third node: + // + // `totalCost` = 20 + // `this.minimum` = 30 + // `totalMinimum` = 60 + // + // therefore, + // `totalMinimum` := max(totalCost + minimum, totalMinimum) + // := max(20 + 30, 60) = 60 + for (const child of children) { const { cost: childCost, minimum } = _computeGas(child); totalMinimum = max(totalCost + minimum, totalMinimum); - // we need to carry the _actual_ cost forward, as that is what we spend + // we need to carry the _actual_ cost forward, as that is what we spend, + // and is needed to calculate subsequent nodes totalCost += childCost; } + // This node's `stipend` is available (in it's entirety - 1/64th is not + // "withheld") to it's children (in the node's call frame). Therefore, the + // `totalMinimum` is decremented by the `stipend` amount. The stipend is not + // allowed to reduce the `totalMinimum` to < 0, because the actual _final_ + // `totalMinimum` must be at least this node's `minimum` value. The + // `totalCost` is also decremented by the `stipend` amount, because the + // `stipend` is "free gas". No worries if the `totalCost` becomes < 0, + // because the the `totalMinimum` will ensure that `gasLeft` never + // decrements below + // 0. + if (stipend !== 0n) { totalMinimum = max(0n, totalMinimum - stipend); totalCost -= stipend; @@ -202,7 +240,10 @@ function _computeGas(child: Node) { // `currentGasLeft = (availableGasLeft * 64) / 63 // See: https://www.wolframalpha.com/input?i=x+-+%28x%2F64%29+%3D+y const sixtyFloorths = computeAllButOneSixtyFourth(totalMinimum); - (child as any).computed = sixtyFloorths + minimum; + + // the minimum gas required for this node and it's children, is the + // children's minimum gas, plus withheld (1/64th) gas, plus the minimum gas + // to execute this node. return { cost: totalCost + cost, minimum: sixtyFloorths + minimum @@ -224,11 +265,11 @@ function computeAllButOneSixtyFourth(minimumGas: bigint) { // Because of 1/64th flooring there is precision loss when we want to // reverse it. This means there are sometimes two numbers that resolve - // to the same "all by 1/64th" number. We should always pick the smaller + // to the same "all but 1/64th" number. We should always pick the smaller // of the two, since they both will compute the same "all by 1/64th" // anyway. // Two example `gasLeft`s that will result in the same "all by 1/64th" - // number are `1023` and `1024`. The "all by 1/64th" number is `1008`. + // number are `1023` and `1024`. The "all but 1/64th" number is `1008`. //https://www.wolframalpha.com/input?i=x+-+%E2%8C%8A%28x%2F64%29%E2%8C%8B+%3D+1008 return allButOneSixtyFourths % 64n === 0n ? allButOneSixtyFourths - 1n @@ -346,14 +387,7 @@ export class GasTracer { // 1. a depth increasing opcode that updates `this.node` to `this.node`'s last child. // 2. a depth decreasing opcode that updates `this.node` to `this.node`'s `parent`. // In otherwords: `depth` and `node` don't need to change. - appendNewCallNode( - -1, - gasUsed, - gasUsed, - 0n, - callNode, - `PRECOMPILE` - ); + appendNewCallNode(-1, gasUsed, gasUsed, 0n, callNode, `PRECOMPILE`); } /** @@ -456,14 +490,7 @@ export class GasTracer { } // add the new node `this.node`'s call tree - appendNewCallNode( - opcode, - fee, - minimum, - stipend, - this.node, - name - ); + appendNewCallNode(opcode, fee, minimum, stipend, this.node, name); } /** From c31569f146dd03e42320f9ea075c8c260d377995 Mon Sep 17 00:00:00 2001 From: jeffsmale90 <6363749+jeffsmale90@users.noreply.github.com> Date: Sat, 17 Jun 2023 06:46:02 +1200 Subject: [PATCH 06/13] Simulate transactions only until a reversion occurs (#4445) --- src/chains/ethereum/ethereum/src/blockchain.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 0e71c023a0..2499ab1dea 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1179,7 +1179,16 @@ export default class Blockchain extends Emittery { const results: InternalTransactionSimulationResult[] = new Array( transactions.length ); - for (let i = 0; i < transactions.length; i++) { + + // We only simulate transactions until a reversion occurs. We hang onto `i`, so that we know how many we simulated. + let i = 0; + for ( + ; + i < transactions.length && + // break out as soon as we receive an error + (i === 0 || results[i - 1].result.exceptionError === undefined); + i++ + ) { const transaction = transactions[i]; const trace: TraceEntry[] = []; const storageChanges: { @@ -1507,7 +1516,7 @@ export default class Blockchain extends Emittery { await vm.eei.cleanupTouchedAccounts(); } - return results; + return i < transactions.length ? results.slice(0, i) : results; } /** From 83528761f44adc0879c8c2e4e9a8d19cb284887c Mon Sep 17 00:00:00 2001 From: jeffsmale90 <6363749+jeffsmale90@users.noreply.github.com> Date: Sat, 17 Jun 2023 06:46:15 +1200 Subject: [PATCH 07/13] Extend trace to include CREATE, CREATE2, JUMP, JUMPI ops (#4444) --- src/chains/ethereum/ethereum/src/api.ts | 17 +- .../ethereum/ethereum/src/blockchain.ts | 216 ++++++++++-------- 2 files changed, 135 insertions(+), 98 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 90359b75aa..266a3514d2 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -124,14 +124,23 @@ export type GasBreakdown = { export type TraceEntry = { opcode: Data; name: string; - from: Address; - to: Address; - value: Quantity; pc: number; - data: Data; signature?: string; +} & (CALLTraceEntry | JUMPTraceEntry | {}); // {} because CREATE and CREATE2 materialize as just the base TraceEntry + +type CALLTraceEntry = { + from?: Address; + to?: Address; + value?: Quantity; + data?: Data; args?: { type: string; value: Quantity | Data }[]; }; + +type JUMPTraceEntry = { + destination: Quantity; + condition?: Quantity; +}; + type TransactionSimulationResult = { returnValue: Data; gas: GasBreakdown; diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 2499ab1dea..491a2c20a8 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -101,13 +101,17 @@ const opcode = { CALLCODE: 0xf2, DELEGATECALL: 0xf4, STATICCALL: 0xfa, + CREATE: 0xf0, + CREATE2: 0xf5, 0x55: "SSTORE", 0x56: "JUMP", 0x57: "JUMPI", 0xf1: "CALL", 0xf2: "CALLCODE", 0xf4: "DELEGATECALL", - 0xfa: "STATICCALL" + 0xfa: "STATICCALL", + 0xf0: "CREATE", + 0xf5: "CREATE2" }; export enum Status { @@ -1251,7 +1255,7 @@ export default class Blockchain extends Emittery { const stepHandler = async (interpreter: Interpreter) => { const runState = (interpreter as any)._runState as RunState; - const { opCode, stack, env, programCounter } = runState; + const { opCode, stack, env } = runState; const codeAddress = env.codeAddress; if (opCode === opcode.SSTORE) { @@ -1271,99 +1275,11 @@ export default class Blockchain extends Emittery { keyBigInt, valueBigInt ]; - } else if ( - includeTrace && - (opCode === opcode.CALL || - opCode === opcode.CALLCODE || - opCode === opcode.DELEGATECALL || - opCode === opcode.STATICCALL) - ) { - // It'd be nice to show call heirarchy, either with nested calls or similar - - let inLength: bigint, - inOffset: bigint, - value: bigint, - toAddr: bigint; - if (opCode === opcode.CALL || opCode === opcode.CALLCODE) { - [inLength, inOffset, value, toAddr] = stack._store.slice(-5, -1); - } else { - [inLength, inOffset, toAddr] = stack._store.slice(-4, -1); + } else if (includeTrace) { + const traceElement = this.makeTraceElement(runState); + if (traceElement) { + trace.push(traceElement); } - const dataLength = Number(inLength); - const data = - dataLength === 0 ? BUFFER_EMPTY : Buffer.allocUnsafe(dataLength); - if (dataLength > 0) { - const dataOffset = Number(inOffset); - - runState.memory._store.copy( - data, - 0, - dataOffset, - dataLength + dataOffset - ); - } - const to = bigIntToBuffer(toAddr); - const functionSelector = - data.length >= 4 ? data.readUIntBE(0, 4) : 0; - const signature = fourBytes.get(functionSelector); - - let args: { type: string; value: Quantity | Data }[]; - if (signature) { - const parameters = signature - .slice(signature.indexOf("(") + 1, signature.length - 1) - .split(","); - if (parameters.length > 0 && parameters[0] !== "") { - try { - const decoded = rawDecode(parameters, data.subarray(4)); - args = Array(parameters.length) as any; - for (let i = 0; i < parameters.length; i++) { - const type = parameters[i]; - const rawValue = decoded[i]; - let value: Data | Quantity; - if (Buffer.isBuffer(rawValue)) { - value = Data.from(rawValue); - } else { - switch (typeof rawValue) { - case "string": - value = Data.from(Buffer.from(rawValue, "hex")); - break; - case "bigint": - value = Quantity.from(bigIntToBuffer(rawValue)); - break; - default: - value = Data.from( - Buffer.from(rawValue.toString(16), "hex") - ); - break; - } - } - - args[i] = { - type, - value - }; - } - } catch (er) { - console.error( - er, - parameters, - Data.from(data.subarray(4)), - typeof value - ); - } - } - } - trace.push({ - opcode: Data.from(Buffer.from([opCode])), - name: opcode[opCode], - from: Address.from(codeAddress.buf), - to: Address.from(to), - signature, - value: value === undefined ? undefined : Quantity.from(value), - data: Data.from(data), - args, - pc: programCounter - }); } }; @@ -1519,6 +1435,118 @@ export default class Blockchain extends Emittery { return i < transactions.length ? results.slice(0, i) : results; } + private makeTraceElement(runState: RunState): TraceEntry | undefined { + // It'd be nice to show call heirarchy, either with nested calls or similar + const { opCode, env, programCounter, stack } = runState; + switch (opCode) { + case opcode.CALL: + case opcode.STATICCALL: + case opcode.CALLCODE: + case opcode.DELEGATECALL: + let inLength: bigint, inOffset: bigint, value: bigint, toAddr: bigint; + if (opCode === opcode.CALL || opCode === opcode.CALLCODE) { + [inLength, inOffset, value, toAddr] = stack._store.slice(-5, -1); + } else { + [inLength, inOffset, toAddr] = stack._store.slice(-4, -1); + } + const dataLength = Number(inLength); + const data = + dataLength === 0 ? BUFFER_EMPTY : Buffer.allocUnsafe(dataLength); + if (dataLength > 0) { + const dataOffset = Number(inOffset); + + runState.memory._store.copy( + data, + 0, + dataOffset, + dataLength + dataOffset + ); + } + const to = bigIntToBuffer(toAddr); + const functionSelector = data.length >= 4 ? data.readUIntBE(0, 4) : 0; + const signature = fourBytes.get(functionSelector); + + let args: { type: string; value: Quantity | Data }[]; + if (signature) { + const parameters = signature + .slice(signature.indexOf("(") + 1, signature.length - 1) + .split(","); + if (parameters.length > 0 && parameters[0] !== "") { + try { + const decoded = rawDecode(parameters, data.subarray(4)); + args = Array(parameters.length) as any; + for (let i = 0; i < parameters.length; i++) { + const type = parameters[i]; + const rawValue = decoded[i]; + let value: Data | Quantity; + if (Buffer.isBuffer(rawValue)) { + value = Data.from(rawValue); + } else { + switch (typeof rawValue) { + case "string": + value = Data.from(Buffer.from(rawValue, "hex")); + break; + case "bigint": + value = Quantity.from(bigIntToBuffer(rawValue)); + break; + default: + value = Data.from( + Buffer.from(rawValue.toString(16), "hex") + ); + break; + } + } + + args[i] = { + type, + value + }; + } + } catch (er) { + console.error( + er, + parameters, + Data.from(data.subarray(4)), + typeof value + ); + } + } + } + return { + opcode: Data.from(Buffer.from([opCode])), + name: opcode[opCode], + from: Address.from(env.codeAddress.buf), + to: Address.from(to), + signature, + value: value === undefined ? undefined : Quantity.from(value), + data: Data.from(data), + args, + pc: programCounter + }; + case opcode.CREATE: + case opcode.CREATE2: + return { + opcode: Data.from(Buffer.from([opCode])), + name: opcode[opCode], + pc: programCounter + }; + case opcode.JUMP: + case opcode.JUMPI: + const destination = Quantity.from(stack._store[stack.length - 1]); + const condition = + opCode === opcode.JUMPI + ? Quantity.from(stack._store[stack.length - 1]) + : undefined; + return { + opcode: Data.from(Buffer.from([opCode])), + name: opcode[opCode], + destination, + condition, + pc: programCounter + }; + } + } + /** * Creates a new VM with it's internal state set to that of the given `block`, * up to, but _not_ including, the transaction at the given From 741027f6b7283baf6b34f05d5d66181954e393f4 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:51:27 -0400 Subject: [PATCH 08/13] update docs with new trace types --- src/chains/ethereum/ethereum/src/api.ts | 11 +++++++++++ src/packages/sim/docs.md | 24 ++++++++++++++++-------- src/packages/sim/types.ts | 22 +++++++++++++++------- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 266a3514d2..147720eb74 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -122,9 +122,20 @@ export type GasBreakdown = { }; export type TraceEntry = { + /** + * The opcode of the trace entry. + * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, JUMP, JUMPI + * + */ opcode: Data; + /** + * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, JUMP, JUMPI) + */ name: string; pc: number; + /** + * Decoded function signature (via 4byte directory) + */ signature?: string; } & (CALLTraceEntry | JUMPTraceEntry | {}); // {} because CREATE and CREATE2 materialize as just the base TraceEntry diff --git a/src/packages/sim/docs.md b/src/packages/sim/docs.md index 6ebd51573c..db7803b208 100644 --- a/src/packages/sim/docs.md +++ b/src/packages/sim/docs.md @@ -171,24 +171,32 @@ type StateChange = { export type TraceEntry = { /** * The opcode of the trace entry. - * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL + * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, JUMP, JUMPI * */ - opcode: DATA; + opcode: Data; /** - * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL) + * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, JUMP, JUMPI) */ name: string; - from: ADDRESS; - to: ADDRESS; - value: QUANTITY; pc: number; - data?: string; /** * Decoded function signature (via 4byte directory) */ signature?: string; - args?: { type: string; value: DATA | QUANTITY }[]; +} & (CALLTraceEntry | JUMPTraceEntry | {}); // {} because CREATE and CREATE2 materialize as just the base TraceEntry + +export type CALLTraceEntry = { + from?: Address; + to?: Address; + value?: Quantity; + data?: Data; + args?: { type: string; value: Quantity | Data }[]; +}; + +export type JUMPTraceEntry = { + destination: Quantity; + condition?: Quantity; }; ``` diff --git a/src/packages/sim/types.ts b/src/packages/sim/types.ts index 9802a016f4..2a1dafa945 100644 --- a/src/packages/sim/types.ts +++ b/src/packages/sim/types.ts @@ -158,22 +158,30 @@ type StateChange = { export type TraceEntry = { /** * The opcode of the trace entry. - * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL + * Currently limited to opcodes for CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, JUMP, JUMPI * */ opcode: DATA; /** - * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL) + * The name of the opcode (CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, JUMP, JUMPI) */ name: string; - from: ADDRESS; - to: ADDRESS; - value: QUANTITY; pc: number; - data?: string; /** * Decoded function signature (via 4byte directory) */ signature?: string; - args?: { type: string; value: DATA | QUANTITY }[]; +} & (CALLTraceEntry | JUMPTraceEntry | {}); // {} because CREATE and CREATE2 materialize as just the base TraceEntry + +export type CALLTraceEntry = { + from?: ADDRESS; + to?: ADDRESS; + value?: QUANTITY; + data?: DATA; + args?: { type: string; value: QUANTITY | DATA }[]; +}; + +export type JUMPTraceEntry = { + destination: QUANTITY; + condition?: QUANTITY; }; From 82a4c870d15959a84ce45080c5bb6b583e7c5dc6 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Sat, 17 Jun 2023 06:55:17 +1200 Subject: [PATCH 09/13] JUMPI conditional was incorrectly using the value for destination --- 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 491a2c20a8..56a9bf28cc 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1535,7 +1535,7 @@ export default class Blockchain extends Emittery { const destination = Quantity.from(stack._store[stack.length - 1]); const condition = opCode === opcode.JUMPI - ? Quantity.from(stack._store[stack.length - 1]) + ? Quantity.from(stack._store[stack.length - 2]) : undefined; return { opcode: Data.from(Buffer.from([opCode])), From 0d25bec1eb99d56a12aa385d9c6a95a15fa39e33 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:33:30 -0400 Subject: [PATCH 10/13] add continue on failure option to tx sim --- src/chains/ethereum/ethereum/src/api.ts | 13 +- .../ethereum/ethereum/src/blockchain.ts | 464 +++++++++--------- src/packages/sim/docs.md | 8 + src/packages/sim/index.html | 23 + src/packages/sim/index.ts | 2 +- src/packages/sim/types.ts | 8 + 6 files changed, 284 insertions(+), 234 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 147720eb74..6488b17212 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -64,6 +64,7 @@ type TransactionSimulationArgs = { overrides?: Ethereum.Call.Overrides; trace?: TraceType; estimateGas?: Estimate; + continueOnFailure?: boolean; }; type Log = [address: Address, topics: DATA[], data: DATA]; @@ -186,7 +187,8 @@ async function simulateTransactions( blockNumber: QUANTITY | Ethereum.Tag, overrides: Ethereum.Call.Overrides = {}, includeTrace: boolean, - includeGasEstimate: Estimate + includeGasEstimate: Estimate, + continueOnFailure: boolean ): Promise[]> { const blocks = blockchain.blocks; const parentBlock = await blocks.get(blockNumber); @@ -327,7 +329,8 @@ async function simulateTransactions( parentBlock, overrides, includeTrace, - includeGasEstimate + includeGasEstimate, + continueOnFailure ); return results; @@ -3066,7 +3069,8 @@ export default class EthereumApi implements Api { overrides, transactions, trace, - estimateGas + estimateGas, + continueOnFailure }: TransactionSimulationArgs, blockNumber: QUANTITY | Ethereum.Tag = Tag.latest ): Promise[]> { @@ -3080,7 +3084,8 @@ export default class EthereumApi implements Api { overrides, includeTrace, //@ts-ignore - estimateGas === true + estimateGas === true, + continueOnFailure === true ); return simulatedTransactionResults.map( diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index 56a9bf28cc..e36ab96947 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -1139,7 +1139,8 @@ export default class Blockchain extends Emittery { parentBlock: Block, overrides: CallOverrides, includeTrace: boolean, - includeGasEstimate: Estimate + includeGasEstimate: Estimate, + continueOnFailure: boolean ) { const stateTrie = this.trie.copy(false); stateTrie.setContext( @@ -1180,259 +1181,264 @@ export default class Blockchain extends Emittery { const runningEncodedAccounts = {}; const runningRawStorageSlots = {}; - const results: InternalTransactionSimulationResult[] = new Array( - transactions.length - ); - - // We only simulate transactions until a reversion occurs. We hang onto `i`, so that we know how many we simulated. - let i = 0; - for ( - ; - i < transactions.length && - // break out as soon as we receive an error - (i === 0 || results[i - 1].result.exceptionError === undefined); - i++ - ) { - const transaction = transactions[i]; - const trace: TraceEntry[] = []; - const storageChanges: { - address: Address; - key: Buffer; - before: Buffer; - after: Buffer; - }[] = []; - const stateChanges = new Map< - Buffer, - [[Buffer, Buffer, Buffer, Buffer], [Buffer, Buffer, Buffer, Buffer]] - >(); - - const touchedStorage = new Map(); - - const data = transaction.data; - // subtract out the transaction's base fee from the gas limit before - // simulating the tx, because `runCall` doesn't account for raw gas costs. - const hasToAddress = transaction.to != null; - const to = hasToAddress ? new Address(transaction.to.toBuffer()) : null; - - const intrinsicGas = calculateIntrinsicGas(data, hasToAddress, common); - let gasLeft = transaction.gas.toBigInt() - intrinsicGas; - - if (gasLeft >= 0n) { - const caller = transaction.from.toBuffer(); - const callerAddress = new Address(caller); - - if (common.isActivatedEIP(2929)) { - const eei = vm.eei; - // handle Berlin hardfork warm storage reads - warmPrecompiles(eei); - - eei.addWarmedAddress(caller); - if (to) { - eei.addWarmedAddress(to.buf); - } - - // shanghai hardfork requires that we warm the coinbase address - if (common.isActivatedEIP(3651)) { - eei.addWarmedAddress(runtimeBlock.header.coinbase.buf); - } - } - - // we need to update the balance and nonce of the sender _before_ - // we run this transaction so that things that rely on these values - // are correct (like contract creation!). - const fromAccount = await vm.eei.getAccount(callerAddress); - - // 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 = - intrinsicTxCost > startBalance ? 0n : startBalance - intrinsicTxCost; - await vm.eei.putAccount(callerAddress, fromAccount); - - const stepHandler = async (interpreter: Interpreter) => { - const runState = (interpreter as any)._runState as RunState; - const { opCode, stack, env } = runState; - const codeAddress = env.codeAddress; - - if (opCode === opcode.SSTORE) { - const stackLength = stack.length; - const keyBigInt = stack._store[stackLength - 1]; - const valueBigInt = stack._store[stackLength - 2]; - - const keyString = keyBigInt.toString(); - - const addressString = codeAddress.buf.toString("utf8"); - let touchedAddressStorage = touchedStorage[addressString]; - if (touchedAddressStorage === undefined) { - touchedAddressStorage = touchedStorage[addressString] = {}; - } - touchedAddressStorage[keyString] = [ - codeAddress, - keyBigInt, - valueBigInt - ]; - } else if (includeTrace) { - const traceElement = this.makeTraceElement(runState); - if (traceElement) { - trace.push(traceElement); + const results: InternalTransactionSimulationResult[] = []; + + for (let i = 0; i < transactions.length; i++) { + try { + const transaction = transactions[i]; + const trace: TraceEntry[] = []; + const storageChanges: { + address: Address; + key: Buffer; + before: Buffer; + after: Buffer; + }[] = []; + const stateChanges = new Map< + Buffer, + [[Buffer, Buffer, Buffer, Buffer], [Buffer, Buffer, Buffer, Buffer]] + >(); + + const touchedStorage = new Map(); + + const data = transaction.data; + // subtract out the transaction's base fee from the gas limit before + // simulating the tx, because `runCall` doesn't account for raw gas costs. + const hasToAddress = transaction.to != null; + const to = hasToAddress ? new Address(transaction.to.toBuffer()) : null; + + const intrinsicGas = calculateIntrinsicGas(data, hasToAddress, common); + const gasLeft = transaction.gas.toBigInt() - intrinsicGas; + + let result: InternalTransactionSimulationResult; + + if (gasLeft >= 0n) { + const caller = transaction.from.toBuffer(); + const callerAddress = new Address(caller); + + if (common.isActivatedEIP(2929)) { + const eei = vm.eei; + // handle Berlin hardfork warm storage reads + warmPrecompiles(eei); + + eei.addWarmedAddress(caller); + if (to) { + eei.addWarmedAddress(to.buf); } - } - }; - - // `onRunStep` is shared with the gas tracer, so we need to play nice - // hack: fix it sometime! - const evm: any = vm.evm; - const oldRunStep = evm.onRunStep; - evm.onRunStep = (...args: any) => { - oldRunStep && oldRunStep.apply(evm, args); - stepHandler.apply(evm, args); - }; - const runCallArgs = { - caller: callerAddress, - data: transaction.data && transaction.data.toBuffer(), - gasPrice: transaction.gasPrice.toBigInt(), - gasLimit: gasLeft, - to, - value: transaction.value == null ? 0n : transaction.value.toBigInt(), - block: runtimeBlock as any - }; - const result = await vm.evm.runCall(runCallArgs); - - // todo: this is always going to pull the "before" from before _all_ simulations - // in order for this to be correct, we need to check all previously simulated transactions - // (or store them in a running set of "current") - const afterCache = vm.stateManager["_cache"]["_cache"] as any; // OrderedMap - - const end = afterCache.end(); - for (const it = afterCache.begin(); !it.equals(end); it.next()) { - const i = it.pointer; - const addressStr = i[0]; - - let beforeEncoded = runningEncodedAccounts[addressStr]; - let addressBuf: Buffer; - if (beforeEncoded === undefined) { - // we haven't changed this account in a previous simulation, need to get the original account - addressBuf = Buffer.from(addressStr, "hex"); - beforeEncoded = await beforeStateManager._trie.get(addressBuf); + // shanghai hardfork requires that we warm the coinbase address + if (common.isActivatedEIP(3651)) { + eei.addWarmedAddress(runtimeBlock.header.coinbase.buf); + } } - const afterEncoded = i[1].val; - if (!beforeEncoded.equals(afterEncoded)) { - // the account has changed - runningEncodedAccounts[addressStr] = afterEncoded; - - const before = decode(beforeEncoded); - const after = decode(afterEncoded); + // we need to update the balance and nonce of the sender _before_ + // we run this transaction so that things that rely on these values + // are correct (like contract creation!). + const fromAccount = await vm.eei.getAccount(callerAddress); + + // 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 = + intrinsicTxCost > startBalance + ? 0n + : startBalance - intrinsicTxCost; + await vm.eei.putAccount(callerAddress, fromAccount); + + const stepHandler = async (interpreter: Interpreter) => { + const runState = (interpreter as any)._runState as RunState; + const { opCode, stack, env } = runState; + const codeAddress = env.codeAddress; + + if (opCode === opcode.SSTORE) { + const stackLength = stack.length; + const keyBigInt = stack._store[stackLength - 1]; + const valueBigInt = stack._store[stackLength - 2]; + + const keyString = keyBigInt.toString(); + + const addressString = codeAddress.buf.toString("utf8"); + let touchedAddressStorage = touchedStorage[addressString]; + if (touchedAddressStorage === undefined) { + touchedAddressStorage = touchedStorage[addressString] = {}; + } + touchedAddressStorage[keyString] = [ + codeAddress, + keyBigInt, + valueBigInt + ]; + } else if (includeTrace) { + const traceElement = this.makeTraceElement(runState); + if (traceElement) { + trace.push(traceElement); + } + } + }; - stateChanges.set(addressBuf || Buffer.from(addressStr, "hex"), [ - before, - after - ]); - } - } + // `onRunStep` is shared with the gas tracer, so we need to play nice + // hack: fix it sometime! + const evm: any = vm.evm; + const oldRunStep = evm.onRunStep; + evm.onRunStep = (...args: any) => { + oldRunStep && oldRunStep.apply(evm, args); + stepHandler.apply(evm, args); + }; - for (const addr in touchedStorage) { - let storageTrie: Trie; - - const storage = touchedStorage[addr] as TouchedStorage; - for (const keyStr in storage) { - const [address, key, valueAfter] = storage[keyStr] as [ - Address, - bigint, - bigint - ]; - if (storageTrie === undefined) { - // HACK: only the fork trie has a `getStorageTrie` method. - // i don't know why - storageTrie = beforeStateManager.getStorageTrie - ? await beforeStateManager.getStorageTrie(address.buf) - : await beforeStateManager._getStorageTrie(address as any); + const runCallArgs = { + caller: callerAddress, + data: transaction.data && transaction.data.toBuffer(), + gasPrice: transaction.gasPrice.toBigInt(), + gasLimit: gasLeft, + to, + value: + transaction.value == null ? 0n : transaction.value.toBigInt(), + block: runtimeBlock as any + }; + const evmResult = await vm.evm.runCall(runCallArgs); + + // todo: this is always going to pull the "before" from before _all_ simulations + // in order for this to be correct, we need to check all previously simulated transactions + // (or store them in a running set of "current") + const afterCache = vm.stateManager["_cache"]["_cache"] as any; // OrderedMap + + const end = afterCache.end(); + for (const it = afterCache.begin(); !it.equals(end); it.next()) { + const i = it.pointer; + const addressStr = i[0]; + + let beforeEncoded = runningEncodedAccounts[addressStr]; + let addressBuf: Buffer; + if (beforeEncoded === undefined) { + // we haven't changed this account in a previous simulation, need to get the original account + addressBuf = Buffer.from(addressStr, "hex"); + beforeEncoded = await beforeStateManager._trie.get(addressBuf); } - const keyBuf = key === 0n ? BUFFER_32_ZERO : bigIntToBuffer(key); + const afterEncoded = i[1].val; + if (!beforeEncoded.equals(afterEncoded)) { + // the account has changed - const addressSlotKey = addr + keyStr; - const before = - runningRawStorageSlots[addressSlotKey] || - decode(await storageTrie.get(keyBuf)); + runningEncodedAccounts[addressStr] = afterEncoded; - const after = bigIntToBuffer(valueAfter); + const before = decode(beforeEncoded); + const after = decode(afterEncoded); - runningRawStorageSlots[addressSlotKey] = after; - if (!before.equals(after)) { - storageChanges.push({ - address, - key: keyBuf, + stateChanges.set(addressBuf || Buffer.from(addressStr, "hex"), [ before, after - }); + ]); } } - } - - const totalGasSpent = intrinsicGas + result.execResult.executionGasUsed; - const maxRefund = totalGasSpent / 5n; - const actualRefund = - result.execResult.gasRefund > maxRefund - ? maxRefund - : result.execResult.gasRefund; - const gasBreakdown: GasBreakdown = { - total: Quantity.from(totalGasSpent), - - actual: Quantity.from(totalGasSpent - actualRefund), - refund: Quantity.from(actualRefund), + for (const addr in touchedStorage) { + let storageTrie: Trie; + + const storage = touchedStorage[addr] as TouchedStorage; + for (const keyStr in storage) { + const [address, key, valueAfter] = storage[keyStr] as [ + Address, + bigint, + bigint + ]; + if (storageTrie === undefined) { + // HACK: only the fork trie has a `getStorageTrie` method. + // i don't know why + storageTrie = beforeStateManager.getStorageTrie + ? await beforeStateManager.getStorageTrie(address.buf) + : await beforeStateManager._getStorageTrie(address as any); + } + const keyBuf = key === 0n ? BUFFER_32_ZERO : bigIntToBuffer(key); + + const addressSlotKey = addr + keyStr; + const before = + runningRawStorageSlots[addressSlotKey] || + decode(await storageTrie.get(keyBuf)); + + const after = bigIntToBuffer(valueAfter); + + runningRawStorageSlots[addressSlotKey] = after; + if (!before.equals(after)) { + storageChanges.push({ + address, + key: keyBuf, + before, + after + }); + } + } + } - intrinsic: Quantity.from(intrinsicGas), - execution: Quantity.from(result.execResult.executionGasUsed), + const totalGasSpent = + intrinsicGas + evmResult.execResult.executionGasUsed; + const maxRefund = totalGasSpent / 5n; + const actualRefund = + evmResult.execResult.gasRefund > maxRefund + ? maxRefund + : evmResult.execResult.gasRefund; - // @ts-ignore - estimate: gasTracer - ? Quantity.from(gasTracer.computeGasLimit() + intrinsicGas) - : undefined - }; + const gasBreakdown: GasBreakdown = { + total: Quantity.from(totalGasSpent), - results[i] = { - result: result.execResult, - gas: gasBreakdown, - storageChanges, - stateChanges, - trace - }; - } else { - results[i] = { - result: { - runState: { programCounter: 0 }, - exceptionError: new VmError(ERROR.OUT_OF_GAS), - returnValue: BUFFER_EMPTY - }, - gas: { - actual: Quantity.Zero, - refund: Quantity.Zero, + actual: Quantity.from(totalGasSpent - actualRefund), + refund: Quantity.from(actualRefund), intrinsic: Quantity.from(intrinsicGas), - execution: Quantity.Zero, + execution: Quantity.from(evmResult.execResult.executionGasUsed), // @ts-ignore - estimate: includeGasEstimate ? Quantity.Zero : undefined - }, - storageChanges, - stateChanges, - trace - }; - } + estimate: gasTracer + ? Quantity.from(gasTracer.computeGasLimit() + intrinsicGas) + : undefined + }; + + result = { + result: evmResult.execResult, + gas: gasBreakdown, + storageChanges, + stateChanges, + trace + }; + } else { + result = { + result: { + runState: { programCounter: 0 }, + exceptionError: new VmError(ERROR.OUT_OF_GAS), + returnValue: BUFFER_EMPTY + }, + gas: { + actual: Quantity.Zero, + refund: Quantity.Zero, + + intrinsic: Quantity.from(intrinsicGas), + execution: Quantity.Zero, + + // @ts-ignore + estimate: includeGasEstimate ? Quantity.Zero : undefined + }, + storageChanges, + stateChanges, + trace + }; + } - gasTracer && gasTracer.reset(); - vm.eei.clearOriginalStorageCache(); - vm.eei.clearWarmedAccounts(); - await vm.eei.cleanupTouchedAccounts(); + results.push(result); + + // if we should _not_ continue on failue, and this transaction failed, + // break out of the loop + if (!continueOnFailure && result.result.exceptionError) break; + } finally { + gasTracer && gasTracer.reset(); + vm.eei.clearOriginalStorageCache(); + vm.eei.clearWarmedAccounts(); + await vm.eei.cleanupTouchedAccounts(); + } } - return i < transactions.length ? results.slice(0, i) : results; + return results; } private makeTraceElement(runState: RunState): TraceEntry | undefined { diff --git a/src/packages/sim/docs.md b/src/packages/sim/docs.md index db7803b208..75d19bec1c 100644 --- a/src/packages/sim/docs.md +++ b/src/packages/sim/docs.md @@ -41,13 +41,21 @@ export type SimulationRequestParams = [ * This means that if the transaction runs out of gas at the user-specified * limit the gas estimate will reflect the amount of gas required to get * to the point at which it ran out of gas. + * default is `false` */ estimateGas?: boolean; /** * `true` to return a transaction CALL* trace, otherwise `false`. + * default is `false` */ trace?: boolean; + /** + * `true` to run all transactions even if preceding transactions fail. + * default is `false` + */ + continueOnFailure?: boolean; + overrides: { [address: ADDRESS]: StateOverride; }; diff --git a/src/packages/sim/index.html b/src/packages/sim/index.html index 6e42302662..1dc071f2d6 100644 --- a/src/packages/sim/index.html +++ b/src/packages/sim/index.html @@ -294,6 +294,29 @@

Transactions Simulator

+ +
+ + + + Should the simulation continue if a transaction + fails? + + +
+ +
+
diff --git a/src/packages/sim/index.ts b/src/packages/sim/index.ts index c5b61c7546..ca4a461320 100644 --- a/src/packages/sim/index.ts +++ b/src/packages/sim/index.ts @@ -1,6 +1,6 @@ import http from "http"; import fs from "fs"; -let remote = false; +let remote = true; const hostname = remote ? "3.140.186.190" : "localhost"; const port = remote ? 8080 : 8545; diff --git a/src/packages/sim/types.ts b/src/packages/sim/types.ts index 2a1dafa945..2d81c8a23a 100644 --- a/src/packages/sim/types.ts +++ b/src/packages/sim/types.ts @@ -28,13 +28,21 @@ export type SimulationRequestParams = [ * This means that if the transaction runs out of gas at the user-specified * limit the gas estimate will reflect the amount of gas required to get * to the point at which it ran out of gas. + * default is `false` */ estimateGas?: boolean; /** * `true` to return a transaction CALL* trace, otherwise `false`. + * default is `false` */ trace?: boolean; + /** + * `true` to run all transactions even if preceding transactions fail. + * default is `false` + */ + continueOnFailure?: boolean; + overrides: { [address: ADDRESS]: StateOverride; }; From 150bbf071efb4cc20faa51c3399f8bbeca45480a Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Fri, 16 Jun 2023 17:07:07 -0400 Subject: [PATCH 11/13] change sim port to 9009 --- src/packages/sim/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/sim/index.ts b/src/packages/sim/index.ts index ca4a461320..8599943411 100644 --- a/src/packages/sim/index.ts +++ b/src/packages/sim/index.ts @@ -81,6 +81,6 @@ const server = http.createServer((req, res) => { } }); -server.listen(3000, () => { - console.log(`Server is running on http://localhost:${3000}`); +server.listen(9009, () => { + console.log(`Server is running on http://localhost:${9009}`); }); From 0e7ed88ba808dd97c0082581442ad0cfe1c04bfc Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Fri, 16 Jun 2023 21:21:38 -0400 Subject: [PATCH 12/13] update sim to run with own tsconfig --- src/packages/ganache/package.json | 2 +- src/packages/sim/package.json | 8 +------- src/packages/sim/tsconfig.json | 23 ++++++++++++++++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/packages/ganache/package.json b/src/packages/ganache/package.json index 90bc9daf2b..0bad386805 100644 --- a/src/packages/ganache/package.json +++ b/src/packages/ganache/package.json @@ -1,6 +1,6 @@ { "name": "ganache", - "version": "7.8.0-tx-sim.0", + "version": "7.8.0-tx-sim.1", "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/sim/package.json b/src/packages/sim/package.json index 0919a3a029..ed10be8173 100644 --- a/src/packages/sim/package.json +++ b/src/packages/sim/package.json @@ -26,9 +26,7 @@ }, "scripts": { "start": "ts-node index.ts", - "tsc": "tsc --build", - "test": "nyc npm run mocha", - "mocha": "cross-env TS_NODE_FILES=true mocha --exit --check-leaks --throw-deprecation --trace-warnings --require ts-node/register 'tests/**/*.test.ts'" + "tsc": "tsc --build" }, "bugs": { "url": "https://github.com/trufflesuite/ganache/issues" @@ -49,10 +47,6 @@ "truffle" ], "devDependencies": { - "@types/mocha": "9.0.0", - "cross-env": "7.0.3", - "mocha": "9.1.3", - "nyc": "15.1.0", "ts-node": "10.9.1", "typescript": "4.7.4" } diff --git a/src/packages/sim/tsconfig.json b/src/packages/sim/tsconfig.json index 2f07ebea64..de99d04e0f 100644 --- a/src/packages/sim/tsconfig.json +++ b/src/packages/sim/tsconfig.json @@ -1,11 +1,24 @@ { - "extends": "../../tsconfig-base.json", "compilerOptions": { + "alwaysStrict": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true, + "module": "CommonJS", + "esModuleInterop": true, + "target": "ES2020", + "moduleResolution": "node", + "noErrorTruncation": true, + "sourceMap": true, + "strict": true, + /** `noImplicitAny: false` and `strictNullChecks: false` are temporary during initial rapid development. */ + "strictNullChecks": false, + "noImplicitAny": false, + "newLine": "lf", + "lib": ["ES2020"], + "experimentalDecorators": true, "outDir": "lib", "declarationDir": "typings" }, - "include": [ - "src", - "index.ts" - ] + "include": ["src", "index.ts"] } From 70334b58f00eb1db601059916df46e61991e8a46 Mon Sep 17 00:00:00 2001 From: David Murdoch <187813+davidmurdoch@users.noreply.github.com> Date: Sat, 17 Jun 2023 10:59:21 -0400 Subject: [PATCH 13/13] fixing some bits --- .../ethereum/ethereum/src/helpers/gas.ts | 180 ++++++++++++------ .../ethereum/ethereum/tests/gas/gas.test.ts | 2 +- src/packages/ganache/package.json | 2 +- src/packages/sim/index.html | 6 +- 4 files changed, 124 insertions(+), 66 deletions(-) diff --git a/src/chains/ethereum/ethereum/src/helpers/gas.ts b/src/chains/ethereum/ethereum/src/helpers/gas.ts index adc124d19f..78ca99f95d 100644 --- a/src/chains/ethereum/ethereum/src/helpers/gas.ts +++ b/src/chains/ethereum/ethereum/src/helpers/gas.ts @@ -1,7 +1,5 @@ import { Interpreter } from "@ethereumjs/evm/dist/interpreter"; -import { precompiles } from "@ethereumjs/evm/dist/precompiles"; import { VM } from "@ethereumjs/vm"; -import { BUFFER_ZERO, Quantity } from "@ganache/utils"; // gas exactimation: // There are opcodes, the CALLs and CREATE (CREATE2? i dunno) that must // "withhold" 1/64th of gasLeft from the gasLimit of these internal CALL/CREATE. @@ -80,7 +78,7 @@ const SSTORE = 0x55; const CALL = 0xf1; const CALLCODE = 0xf2; -type Node = { +type Op = { /** * `code` is required for distinguishing SSTORE opcodes for special rules */ @@ -91,8 +89,8 @@ type Node = { */ minimum: bigint; stipend: bigint; - children: Node[]; - parent: Node | null; + children: Op[]; + parent: Op | null; name: string; }; @@ -113,9 +111,9 @@ function createNode( cost: bigint, minimum: bigint, stipend: bigint, - parent: Node | null, + parent: Op | null, name: string -): Node { +): Op { return { children: [], cost, @@ -140,9 +138,9 @@ function appendNewCallNode( cost: bigint, minimum: bigint, stipend: bigint, - parent: Node, + parent: Op, name: string -): Node { +): Op { const newNode = createNode(code, cost, minimum, stipend, parent, name); parent.children.push(newNode); return newNode; @@ -153,7 +151,7 @@ function appendNewCallNode( * * @param node The node to get the last child of. */ -function getLastChild({ children }: Node): Node | undefined { +function getLastChild({ children }: Op): Op | undefined { return children[children.length - 1]; } @@ -172,7 +170,7 @@ function max(a: bigint, b: bigint) { * * @param node The node to compute the gas costs of. */ -function _computeGas(node: Node) { +function _computeGas(node: Op) { const { children, cost, minimum, stipend } = node; if (children.length === 0) { return { @@ -239,19 +237,19 @@ function _computeGas(node: Node) { // which is: // `currentGasLeft = (availableGasLeft * 64) / 63 // See: https://www.wolframalpha.com/input?i=x+-+%28x%2F64%29+%3D+y - const sixtyFloorths = computeAllButOneSixtyFourth(totalMinimum); + const sixtyFourSixtyFourths = reverseAllButOneSixtyFloorth(totalMinimum); // the minimum gas required for this node and it's children, is the // children's minimum gas, plus withheld (1/64th) gas, plus the minimum gas // to execute this node. return { cost: totalCost + cost, - minimum: sixtyFloorths + minimum + minimum: sixtyFourSixtyFourths + minimum }; } } -function computeAllButOneSixtyFourth(minimumGas: bigint) { +function reverseAllButOneSixtyFloorth(minimumGas: bigint) { // it's possible for the minimum gas to be zero, without this check we'll // end up returning -1n, which is, um, wrong. if (minimumGas === 0n) return 0n; @@ -266,9 +264,9 @@ function computeAllButOneSixtyFourth(minimumGas: bigint) { // Because of 1/64th flooring there is precision loss when we want to // reverse it. This means there are sometimes two numbers that resolve // to the same "all but 1/64th" number. We should always pick the smaller - // of the two, since they both will compute the same "all by 1/64th" + // of the two, since they both will compute the same "all but 1/64th" // anyway. - // Two example `gasLeft`s that will result in the same "all by 1/64th" + // Two example `gasLeft`s that will result in the same "all but 1/64th" // number are `1023` and `1024`. The "all but 1/64th" number is `1008`. //https://www.wolframalpha.com/input?i=x+-+%E2%8C%8A%28x%2F64%29%E2%8C%8B+%3D+1008 return allButOneSixtyFourths % 64n === 0n @@ -276,60 +274,117 @@ function computeAllButOneSixtyFourth(minimumGas: bigint) { : allButOneSixtyFourths; } +function processNode( + op: Op, + results: Map +) { + if (op.children.length === 0) { + const cost = max(op.cost - op.stipend, 0n); + results.set(op, { + cost, + minimum: op.minimum + }); + } else { + // At any point, `totalMinimum` represents the total minimum gas required to + // execute the ops in `this.children` up to that point. This op's cost + // will be considered at the end. + // + // As we iterate over a child, `totalMinimum` becomes the total minimum gas + // required to execute the ops in `this.children` up to and including that + // op. At this point, `totalMinimum` may be greater than (`totalCost` + + // `minimum`) if previous ops had a signficant `minimum > cost` overhead. + // e.g.: + // + // Given ops: + // { cost: 10, minimum: 10 }, + // { cost: 10, minimum: 50 }, + // { cost: 10, minimum: 30 }, + // + // Before executing the third op: + // + // `totalCost` = 20 + // `this.minimum` = 30 + // `totalMinimum` = 60 + // + // therefore, + // `totalMinimum` := max(totalCost + minimum, totalMinimum) + // := max(20 + 30, 60) = 60 + + let totalMinimum = 0n; + let totalCost = 0n; + + for (const child of op.children) { + const { cost: childCost, minimum } = results.get(child); + totalMinimum = max(totalCost + minimum, totalMinimum); + // we need to carry the _actual_ cost forward, as that is what we spend, + // and is needed to calculate subsequent ops + totalCost += childCost; + } + + // This op's `stipend` is available (in its entirety - 1/64th is not + // "withheld") to its children (in the op's call frame). Therefore, the + // `totalMinimum` is decremented by the `stipend` amount. The stipend is not + // allowed to reduce the `totalMinimum` to < 0, because the actual _final_ + // `totalMinimum` must be at least this op's `minimum` value. The + // `totalCost` is also decremented by the `stipend` amount, because the + // `stipend` is "free gas". No worries if the `totalCost` becomes < 0, + // because the the `totalMinimum` will ensure that `gasLeft` never + // decrements below 0. + if (op.stipend !== 0n) { + totalMinimum = max(0n, totalMinimum - op.stipend); + totalCost -= op.stipend; + } + + // sixtyFourSixtyFourths represents the amount of gas required before the + // CALL. It is the amount of gas before 63/64ths is calculated. It is not + // neccessarily the amount of gas that is passed to the CALL, i.e., + // `call(gas(), ...)`, as the developer could have specified a their own + // gas amount. If the gas amount the dev passed to the CALL is less than + // totalMinimum (63/64ths of `sixtyFourSixtyFourths`) then we need to use + // that number instead. + // This is number is not possible to compute in every case, but we can + // guess at it by looking at the difference between `totalMinimum` and the + // `gasLeft` at the time of the `CALL`, and the gas the CALL itself actually + // received. think: `delegatecall(sub(gas(), 10000), ...)` + // If the CALL received `n` gas less less than the actual gas left at the + // time, we can assume the contract did something like: + // `delegatecall(sub(preCallGasLimit, n), ...)` + const sixtyFourSixtyFourths = reverseAllButOneSixtyFloorth(totalMinimum); + + // the minimum gas required for this op and its children, is the + // children's minimum gas, plus withheld (1/64th) gas, plus the minimum gas + // to execute this op. + results.set(op, { + cost: totalCost + op.cost, + minimum: sixtyFourSixtyFourths + op.minimum + }); + } +} + /** * Computes the actual and required gas costs of the node. * * @param root The root node to compute the gas costs of. */ -function computeGasIt(root: Node) { +function computeGasIt(root: Op) { // Initialize stack with root node const stack: { - node: Node; - parentResult?: { cost: bigint; minimum: bigint }; - }[] = [{ node: root }]; - const results: Map = new Map(); + op: Op; + result?: { cost: bigint; minimum: bigint }; + }[] = [{ op: root }]; + const results: Map = new Map(); while (stack.length > 0) { - const { node, parentResult } = stack[stack.length - 1]; + const node = stack[stack.length - 1]; - if (node.children.length > 0 && !parentResult) { + if (node.op.children.length > 0 && !node.result) { // Push all children to stack, marking the parent as having been processed - stack[stack.length - 1].parentResult = { cost: 0n, minimum: 0n }; - for (const child of node.children) { - stack.push({ node: child }); - } + node.result = { cost: 0n, minimum: 0n }; + for (const child of node.op.children) stack.push({ op: child }); } else { // Process node stack.pop(); - if (node.children.length === 0) { - const cost = max(node.cost - node.stipend, 0n); - results.set(node, { - cost, - minimum: node.minimum - }); - } else { - let totalMinimum = 0n; - let totalCost = 0n; - - for (const child of node.children) { - const { cost: childCost, minimum } = results.get(child); - totalMinimum = max(totalCost + minimum, totalMinimum); - // we need to carry the _actual_ cost forward, as that is what we spend - totalCost += childCost; - } - - if (node.stipend !== 0n) { - totalMinimum = max(0n, totalMinimum - node.stipend); - totalCost -= node.stipend; - } - - const allButOneSixtyFourths = computeAllButOneSixtyFourth(totalMinimum); - - results.set(node, { - cost: totalCost + node.cost, - minimum: allButOneSixtyFourths + node.minimum - }); - } + processNode(node.op, results); } } @@ -337,8 +392,8 @@ function computeGasIt(root: Node) { } export class GasTracer { - root: Node; - node: Node; + root: Op; + node: Op; depth: number; constructor(private readonly vm: VM) { this.node = this.root = createNode(-1, 0n, 0n, undefined, null, "ROOT"); @@ -387,7 +442,10 @@ export class GasTracer { // 1. a depth increasing opcode that updates `this.node` to `this.node`'s last child. // 2. a depth decreasing opcode that updates `this.node` to `this.node`'s `parent`. // In otherwords: `depth` and `node` don't need to change. - appendNewCallNode(-1, gasUsed, gasUsed, 0n, callNode, `PRECOMPILE`); + // Note: `callNode` could be `undefined` in cases like when someone sends + // a transactions with data to an account that doesn't have code. + callNode && + appendNewCallNode(-1, gasUsed, gasUsed, 0n, callNode, `PRECOMPILE`); } /** @@ -453,7 +511,7 @@ export class GasTracer { // over-written (we need it to be a single node to ensure that it's // stipend is applied correctly). (actually if it has no children, we // can probably calculate it's minimum, cost, and stipend, but will - // need to handle it's stipend correctly) + // need to handle its stipend correctly) previousSibling.stipend === 0n ) { previousSibling.minimum = previousSibling.cost += fee; @@ -489,7 +547,7 @@ export class GasTracer { this.depth = depth; } - // add the new node `this.node`'s call tree + // add the new node to `this.node`'s call tree appendNewCallNode(opcode, fee, minimum, stipend, this.node, name); } diff --git a/src/chains/ethereum/ethereum/tests/gas/gas.test.ts b/src/chains/ethereum/ethereum/tests/gas/gas.test.ts index cba26396fc..5da14ae89f 100644 --- a/src/chains/ethereum/ethereum/tests/gas/gas.test.ts +++ b/src/chains/ethereum/ethereum/tests/gas/gas.test.ts @@ -62,7 +62,7 @@ contract Storage { params: [ { transactions: [tx], - gasEstimation: "call-depth" + estimateGas: true } ] }; diff --git a/src/packages/ganache/package.json b/src/packages/ganache/package.json index 0bad386805..100dacc7f8 100644 --- a/src/packages/ganache/package.json +++ b/src/packages/ganache/package.json @@ -1,6 +1,6 @@ { "name": "ganache", - "version": "7.8.0-tx-sim.1", + "version": "7.8.0-tx-sim.2", "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/sim/index.html b/src/packages/sim/index.html index 1dc071f2d6..b9c4449a30 100644 --- a/src/packages/sim/index.html +++ b/src/packages/sim/index.html @@ -248,7 +248,7 @@

Transactions Simulator

- + How precise should the returned gas estimation be? @@ -257,8 +257,8 @@

Transactions Simulator