From 037e156c680f617cd2191ed1c9c224b9ecb7eb36 Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Thu, 21 Apr 2022 01:16:33 +0200 Subject: [PATCH] feat: allow balance, code, nonce, and state overrides in `eth_call` (#2565) * feat: Allow contract code and balance overrides Support overriding the contract code, account balances and nonces for simulations with eth_call. This is an extension also supported by geth. * reorg eth_call override type. add state/stateDiff * support state/stateDiff in eth_call override * fix eth_call code override. reorg a bit * add override structure to eth_call jsdocs * add override to eth_call example in jsdoc * prevent use of state & stateDiff on override * remove unused import on test * add encodeValue/deployContract helpers * reorg previous test to make room for new * add tests for eth_call override * rename eth_call override type * change keccak import to be from @ganache/utils * await async function * change `Object.entries` to more perfomant `for ... in` * rename confusing key name * change `Object.entries` to more performang `for ... in` * move comment * data validation for state/Diff override * add junk data test for eth_call overrides * remove unused file * correctly throw on both state && stateDiff * fix broken test * remove unused throw * change return to continue like a smart person * make state/stateDiff looping more efficient * fix test name * abstract override generation in test * fix override balance test expected value * fix override code test * fix override state/stateDiff test data * fix override stateDiff expected values * fix override state expected values * add todo comment * add bad slot values for state/Diff test * add junk value test for nonce * add data validation method for state/Diff override * validate data for state/Diff override * fix some default data to match geth * Move eth_call overrides setting to helper file * Apply suggestions from code review Co-authored-by: David Murdoch <187813+davidmurdoch@users.noreply.github.com> * extract types to run-call helper * check length directly rather than removing prefix * use copied trie for eth_call * fix copy/paste error * fix copy/paste error * beef up test a bit * remove unused libraries * add tests to confirm eth_call changes are ephemeral * set multiple slots in test * make tests more scrict Co-authored-by: Jacob Evans Co-authored-by: MicaiahReid Co-authored-by: David Murdoch <187813+davidmurdoch@users.noreply.github.com> --- src/chains/ethereum/ethereum/src/api.ts | 55 +- .../ethereum/ethereum/src/blockchain.ts | 36 +- .../ethereum/ethereum/src/helpers/run-call.ts | 137 ++- .../ethereum/tests/api/eth/call.test.ts | 1060 ++++++++++++++--- .../tests/api/eth/contracts/Inspector.sol | 49 + 5 files changed, 1168 insertions(+), 169 deletions(-) create mode 100644 src/chains/ethereum/ethereum/tests/api/eth/contracts/Inspector.sol diff --git a/src/chains/ethereum/ethereum/src/api.ts b/src/chains/ethereum/ethereum/src/api.ts index 4d3ca936f9..80a0e7a961 100644 --- a/src/chains/ethereum/ethereum/src/api.ts +++ b/src/chains/ethereum/ethereum/src/api.ts @@ -27,7 +27,12 @@ import { TypedTransaction, TypedTransactionJSON } from "@ganache/ethereum-transaction"; -import { toRpcSig, ecsign, hashPersonalMessage, KECCAK256_NULL } from "ethereumjs-util"; +import { + toRpcSig, + ecsign, + hashPersonalMessage, + KECCAK256_NULL +} from "ethereumjs-util"; import { TypedData as NotTypedData, signTypedData_v4 } from "eth-sig-util"; import { Data, @@ -53,6 +58,7 @@ import { decode } from "@ganache/rlp"; import { Address } from "@ganache/ethereum-address"; import { GanacheRawBlock } from "@ganache/ethereum-block"; import { Capacity } from "./miner/miner"; +import { CallOverrides } from "./helpers/run-call"; async function autofillDefaultTransactionValues( tx: TypedTransaction, @@ -464,10 +470,15 @@ export default class EthereumApi implements Api { // The ethereumjs-vm StateManager does not allow to set empty code, // therefore we will manually set the code hash when "clearing" the contract code if (codeBuffer.length > 0) { - await stateManager.putContractCode({ buf: addressBuffer } as any, codeBuffer) + await stateManager.putContractCode( + { buf: addressBuffer } as any, + codeBuffer + ); } else { - const account = await stateManager.getAccount({ buf: addressBuffer } as any); - account.codeHash = KECCAK256_NULL + const account = await stateManager.getAccount({ + buf: addressBuffer + } as any); + account.codeHash = KECCAK256_NULL; await stateManager.putAccount({ buf: addressBuffer } as any, account); } @@ -505,7 +516,11 @@ export default class EthereumApi implements Api { const valueBuffer = Data.from(value).toBuffer(); const blockchain = this.#blockchain; const stateManager = blockchain.vm.stateManager; - await stateManager.putContractStorage({ buf: addressBuffer } as any, slotBuffer, valueBuffer) + await stateManager.putContractStorage( + { buf: addressBuffer } as any, + slotBuffer, + valueBuffer + ); // TODO: do we need to mine a block here? The changes we're making really don't make any sense at all // and produce an invalid trie going forward. @@ -2613,9 +2628,19 @@ export default class EthereumApi implements Api { * * `value`: `QUANTITY` (optional) - Integer of the value in wei. * * `data`: `DATA` (optional) - Hash of the method signature and the ABI encoded parameters. * + * State Override object - An address-to-state mapping, where each entry specifies some + * state to be ephemerally overridden prior to executing the call. Each address maps to an + * object containing: + * * `balance`: `QUANTITY` (optional) - The balance to set for the account before executing the call. + * * `nonce`: `QUANTITY` (optional) - The nonce to set for the account before executing the call. + * * `code`: `DATA` (optional) - The EVM bytecode to set for the account before executing the call. + * * `state`: `OBJECT` (optional*) - Key-value mapping to override *all* slots in the account storage before executing the call. + * * `stateDiff`: `OBJECT` (optional*) - Key-value mapping to override *individual* slots in the account storage before executing the call. + * * *Note - `state` and `stateDiff` fields are mutually exclusive. * @param transaction - The transaction call object as seen in source. * @param blockNumber - Integer block number, or the string "latest", "earliest" * or "pending". + * @param overrides - State overrides to apply during the simulation. * * @returns The return value of executed contract. * @example @@ -2633,14 +2658,20 @@ export default class EthereumApi implements Api { * const simpleSol = "0x6080604052600560008190555060858060196000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80633fa4f24514602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea26469706673582212200897f7766689bf7a145227297912838b19bcad29039258a293be78e3bf58e20264736f6c63430007040033"; * const [from] = await provider.request({ method: "eth_accounts", params: [] }); * const txObj = { from, gas: "0x5b8d80", gasPrice: "0x1dfd14000", value:"0x0", data: simpleSol }; - * const result = await provider.request({ method: "eth_call", params: [txObj, "latest"] }); + * const slot = "0x0000000000000000000000000000000000000000000000000000000000000005" + * const overrides = { [from]: { balance: "0x3e8", "nonce: "0x5", code: "0xbaddad42", stateDiff: { [slot]: "0xbaddad42"}}} + * const result = await provider.request({ method: "eth_call", params: [txObj, "latest", overrides] }); * console.log(result); * ``` */ - @assertArgLength(1, 2) - async eth_call(transaction: any, blockNumber: QUANTITY | Tag = Tag.latest) { + @assertArgLength(1, 3) + async eth_call( + transaction: any, + blockNumber: QUANTITY | Tag = Tag.latest, + overrides: CallOverrides = {} + ) { const blockchain = this.#blockchain; - const common = this.#blockchain.common; + const common = blockchain.common; const blocks = blockchain.blocks; const parentBlock = await blocks.get(blockNumber); const parentHeader = parentBlock.header; @@ -2737,7 +2768,11 @@ export default class EthereumApi implements Api { block }; - return blockchain.simulateTransaction(simulatedTransaction, parentBlock); + return blockchain.simulateTransaction( + simulatedTransaction, + parentBlock, + overrides + ); } //#endregion diff --git a/src/chains/ethereum/ethereum/src/blockchain.ts b/src/chains/ethereum/ethereum/src/blockchain.ts index d76e7dd8e1..6a4cd199bd 100644 --- a/src/chains/ethereum/ethereum/src/blockchain.ts +++ b/src/chains/ethereum/ethereum/src/blockchain.ts @@ -53,7 +53,11 @@ import { TypedTransaction } from "@ganache/ethereum-transaction"; import { Block, RuntimeBlock, Snapshots } from "@ganache/ethereum-block"; -import { SimulationTransaction } from "./helpers/run-call"; +import { + SimulationTransaction, + applySimulationOverrides, + CallOverrides +} from "./helpers/run-call"; import { ForkStateManager } from "./forking/state-manager"; import { DefaultStateManager, @@ -164,7 +168,7 @@ function createCommon(chainId: number, networkId: number, hardfork: Hardfork) { // a) we don't currently support changing hardforks // b) it can cause `MaxListenersExceededWarning`. // Since we don't need it we overwrite .on to make it be quiet. - (common as any).on = () => { }; + (common as any).on = () => {}; return common; } @@ -367,7 +371,7 @@ export default class Blockchain extends Emittery { this.#state = Status.stopping; // ignore errors while stopping here, since we are already in an // exceptional case - await this.stop().catch(_ => { }); + await this.stop().catch(_ => {}); throw e; } @@ -791,7 +795,10 @@ export default class Blockchain extends Emittery { return (this.#timeAdjustment = timestamp - Date.now()); } - #deleteBlockData = async (blocksToDelete: Block[], newLatestBlockNumber: Buffer) => { + #deleteBlockData = async ( + blocksToDelete: Block[], + newLatestBlockNumber: Buffer + ) => { // if we are forking we need to make sure we clean up the forking related // metadata that isn't stored in the trie if ("revertMetaData" in this.trie) { @@ -980,7 +987,8 @@ export default class Blockchain extends Emittery { public async simulateTransaction( transaction: SimulationTransaction, - parentBlock: Block + parentBlock: Block, + overrides: CallOverrides ) { let result: EVMResult; @@ -1002,9 +1010,9 @@ export default class Blockchain extends Emittery { const common = this.fallback ? this.fallback.getCommonForBlockNumber( - this.common, - BigInt(transaction.block.header.number.toString()) - ) + this.common, + BigInt(transaction.block.header.number.toString()) + ) : this.common; const gasLeft = @@ -1050,6 +1058,10 @@ export default class Blockchain extends Emittery { if (to) stateManager.addWarmedAddress(to.buf); } + // If there are any overrides requested for eth_call, apply + // them now before running the simulation. + await applySimulationOverrides(stateTrie, vm, overrides); + // 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!). @@ -1120,9 +1132,9 @@ export default class Blockchain extends Emittery { const common = this.fallback ? this.fallback.getCommonForBlockNumber( - this.common, - BigInt(newBlock.header.number.toString()) - ) + this.common, + BigInt(newBlock.header.number.toString()) + ) : this.common; const vm = await VM.create({ @@ -1268,7 +1280,7 @@ export default class Blockchain extends Emittery { // simplest method I could find) is fine. // Remove this and you may see the infamous // `Uncaught TypeError: Cannot read property 'pop' of undefined` error! - (vm.stateManager as any)._cache.flush = () => { }; + (vm.stateManager as any)._cache.flush = () => {}; // Process the block without committing the data. // The vmerr key on the result appears to be removed. diff --git a/src/chains/ethereum/ethereum/src/helpers/run-call.ts b/src/chains/ethereum/ethereum/src/helpers/run-call.ts index 8a3d07aa66..5906c90e46 100644 --- a/src/chains/ethereum/ethereum/src/helpers/run-call.ts +++ b/src/chains/ethereum/ethereum/src/helpers/run-call.ts @@ -1,10 +1,12 @@ import { RuntimeBlock } from "@ganache/ethereum-block"; -import { Quantity, Data } from "@ganache/utils"; +import { Quantity, Data, hasOwn, keccak, BUFFER_EMPTY } from "@ganache/utils"; import { Address } from "@ganache/ethereum-address"; import Message from "@ethereumjs/vm/dist/evm/message"; import VM from "@ethereumjs/vm"; import { BN } from "ethereumjs-util"; import EVM from "@ethereumjs/vm/dist/evm/evm"; +import { KECCAK256_NULL } from "ethereumjs-util"; +import { GanacheTrie } from "./trie"; export type SimulationTransaction = { /** @@ -34,6 +36,26 @@ export type SimulationTransaction = { block: RuntimeBlock; }; +type CallOverride = + | Partial<{ + code: string; + nonce: string; + balance: string; + state: { [slot: string]: string }; + stateDiff: never; + }> + | Partial<{ + code: string; + nonce: string; + balance: string; + state: never; + stateDiff: { [slot: string]: string }; + }>; + +export type CallOverrides = { + [address: string]: CallOverride; +}; + /** * Executes a message/transaction against the vm. * @param vm - @@ -69,3 +91,116 @@ export function runCall( const evm = new EVM(vm, txContext, transaction.block as any); return evm.executeMessage(message); } + +const validateStorageOverride = ( + slot: string, + value: string, + fieldName: string +) => { + // assume Quantity will handle other types, these are just special string cases + if (typeof slot === "string" && slot !== "" && slot.indexOf("0x") === 0) { + // assume we're starting with 0x cause Quantity will verify if not + if (slot.length != 66) { + throw new Error( + `${fieldName} override slot must be a 64 character hex string. Received ${ + slot.length - 2 + } character string.` + ); + } + } + if (value === null || value === undefined) { + throw new Error(`${fieldName} override data not valid. Received: ${value}`); + } + // assume Quantity will handle other types, these are just special string cases + if (typeof value === "string" && value !== "" && value.indexOf("0x") === 0) { + if (value.length != 66) { + throw new Error( + `${fieldName} override data must be a 64 character hex string. Received ${ + value.length - 2 + } character string.` + ); + } + } +}; + +export async function applySimulationOverrides( + stateTrie: GanacheTrie, + vm: VM, + overrides: CallOverrides +): Promise { + const stateManager = vm.stateManager; + for (const address in overrides) { + if (!hasOwn(overrides, address)) continue; + const { balance, nonce, code, state, stateDiff } = overrides[address]; + + const vmAddr = { buf: Address.from(address).toBuffer() } as any; + // group together overrides that update the account + if (nonce != null || balance != null || code != null) { + const account = await stateManager.getAccount(vmAddr); + + if (nonce != null) { + account.nonce = { + toArrayLike: () => + // geth treats empty strings as "0x0" nonce for overrides + nonce === "" ? BUFFER_EMPTY : Quantity.from(nonce).toBuffer() + } as any; + } + if (balance != null) { + account.balance = { + toArrayLike: () => + // geth treats empty strings as "0x0" balance for overrides + balance === "" ? BUFFER_EMPTY : Quantity.from(balance).toBuffer() + } as any; + } + if (code != null) { + // geth treats empty strings as "0x" code for overrides + const codeBuffer = Data.from(code === "" ? "0x" : code).toBuffer(); + // The ethereumjs-vm StateManager does not allow to set empty code, + // therefore we will manually set the code hash when "clearing" the contract code + const codeHash = + codeBuffer.length > 0 ? keccak(codeBuffer) : KECCAK256_NULL; + account.codeHash = codeHash; + await stateTrie.db.put(codeHash, codeBuffer); + } + await stateManager.putAccount(vmAddr, account); + } + // group together overrides that update storage + if (state || stateDiff) { + if (state) { + // state and stateDiff fields are mutually exclusive + if (stateDiff) { + throw new Error("both state and stateDiff overrides specified"); + } + // it's possible that the user fed an override with a valid address + // and slot, but not a value we can actually set in the storage. if + // so, we don't want to set the storage, and we also don't want to + // clear it out + let clearedState = false; + for (const slot in state) { + if (!hasOwn(state, slot)) continue; + const value = state[slot]; + validateStorageOverride(slot, value, "State"); + if (!clearedState) { + // override.state clears all storage and sets just the specified slots + await stateManager.clearContractStorage(vmAddr); + clearedState = true; + } + const slotBuf = Quantity.from(slot).toBuffer(); + const valueBuf = Quantity.from(value).toBuffer(); + await stateManager.putContractStorage(vmAddr, slotBuf, valueBuf); + } + } else { + for (const slot in stateDiff) { + // don't set storage for invalid values + if (!hasOwn(stateDiff, slot)) continue; + const value = stateDiff[slot]; + validateStorageOverride(slot, value, "StateDiff"); + + const slotBuf = Quantity.from(slot).toBuffer(); + const valueBuf = Quantity.from(value).toBuffer(); + await stateManager.putContractStorage(vmAddr, slotBuf, valueBuf); + } + } + } + } +} diff --git a/src/chains/ethereum/ethereum/tests/api/eth/call.test.ts b/src/chains/ethereum/ethereum/tests/api/eth/call.test.ts index e71cd75934..b414f0a16f 100644 --- a/src/chains/ethereum/ethereum/tests/api/eth/call.test.ts +++ b/src/chains/ethereum/ethereum/tests/api/eth/call.test.ts @@ -3,181 +3,949 @@ import EthereumProvider from "../../../src/provider"; import getProvider from "../../helpers/getProvider"; import compile, { CompileOutput } from "../../helpers/compile"; import { join } from "path"; -import { BUFFER_EMPTY, Quantity, RPCQUANTITY_EMPTY } from "@ganache/utils"; +import { BUFFER_EMPTY, Data, Quantity, RPCQUANTITY_ONE } from "@ganache/utils"; import { CallError } from "@ganache/ethereum-utils"; +import Blockchain from "../../../src/blockchain"; +import Wallet from "../../../src/wallet"; +import { Address } from "@ganache/ethereum-address"; +import { Address as EthereumJsAddress } from "ethereumjs-util"; +import { SimulationTransaction } from "../../../src/helpers/run-call"; +import { Block, RuntimeBlock } from "@ganache/ethereum-block"; +import { + LegacyRpcTransaction, + TransactionFactory +} from "@ganache/ethereum-transaction"; +import { EthereumOptionsConfig } from "@ganache/ethereum-options"; +import { GanacheTrie } from "../../../src/helpers/trie"; + +const encodeValue = (val: number | string) => { + return Quantity.from(val).toBuffer().toString("hex").padStart(64, "0"); +}; + +async function deployContract(provider, from, code) { + await provider.send("eth_subscribe", ["newHeads"]); + const transactionHash = await provider.send("eth_sendTransaction", [ + { + from, + data: code, + gas: "0xfffff" + } as any + ]); + await provider.once("message"); + + const receipt = await provider.send("eth_getTransactionReceipt", [ + transactionHash + ]); + + return receipt.contractAddress; +} describe("api", () => { describe("eth", () => { describe("call", () => { - let contract: CompileOutput; - let provider: EthereumProvider; - let from, to: string; - let contractAddress: string; - let tx: object; + describe("normal operation", () => { + let contract: CompileOutput; + let provider: EthereumProvider; + let from, to: string; + let contractAddress: string; + let tx: object; - before("compile", () => { - contract = compile(join(__dirname, "./contracts/EthCall.sol"), { - contractName: "EthCall" + before("compile", () => { + contract = compile(join(__dirname, "./contracts/EthCall.sol"), { + contractName: "EthCall" + }); }); - }); - before(async () => { - provider = await getProvider({ wallet: { deterministic: true } }); - [from, to] = await provider.send("eth_accounts"); + before(async () => { + provider = await getProvider({ wallet: { deterministic: true } }); + [from, to] = await provider.send("eth_accounts"); + + contractAddress = await deployContract(provider, from, contract.code); - await provider.send("eth_subscribe", ["newHeads"]); - const contractHash = await provider.send("eth_sendTransaction", [ - { + tx = { from, - data: contract.code, - gasLimit: "0xfffff" - } - ]); - await provider.once("message"); - const receipt = await provider.send("eth_getTransactionReceipt", [ - contractHash - ]); - contractAddress = receipt.contractAddress; - - tx = { - from, - to: contractAddress, - data: "0x3fa4f245" // code for the "value" of the contract - }; - }); + to: contractAddress, + data: "0x3fa4f245" // code for the "value" of the contract + }; + }); - after(async () => { - provider && (await provider.disconnect()); - }); + after(async () => { + provider && (await provider.disconnect()); + }); - it("executes a message call", async () => { - const result = await provider.send("eth_call", [tx, "latest"]); - // gets the contract's "value", which should be 5 - assert.strictEqual(Quantity.from(result).toNumber(), 5); - }); + it("executes a message call", async () => { + const result = await provider.send("eth_call", [tx, "latest"]); + // gets the contract's "value", which should be 5 + assert.strictEqual(Quantity.from(result).toNumber(), 5); + }); - it("does not create a transaction on the chain", async () => { - const beforeCall = await provider.send("eth_getBlockByNumber", [ - "latest" - ]); - await provider.send("eth_call", [tx, "latest"]); - const afterCall = await provider.send("eth_getBlockByNumber", [ - "latest" - ]); - assert.strictEqual(beforeCall.number, afterCall.number); - }); + it("does not create a transaction on the chain", async () => { + const beforeCall = await provider.send("eth_getBlockByNumber", [ + "latest" + ]); + await provider.send("eth_call", [tx, "latest"]); + const afterCall = await provider.send("eth_getBlockByNumber", [ + "latest" + ]); + assert.strictEqual(beforeCall.number, afterCall.number); + }); - it("allows legacy 'gasPrice' based transactions", async () => { - const tx = { - from, - to: contractAddress, - data: "0x3fa4f245", - gasPrice: "0x1" - }; - const result = await provider.send("eth_call", [tx, "latest"]); - // we can still get the result when the gasPrice is set - assert.strictEqual(Quantity.from(result).toNumber(), 5); - }); + it("allows legacy 'gasPrice' based transactions", async () => { + const tx = { + from, + to: contractAddress, + data: "0x3fa4f245", + gasPrice: "0x1" + }; + const result = await provider.send("eth_call", [tx, "latest"]); + // we can still get the result when the gasPrice is set + assert.strictEqual(Quantity.from(result).toNumber(), 5); + }); - it("allows eip-1559 fee market transactions", async () => { - const tx = { - from, - to: contractAddress, - data: "0x3fa4f245", - maxFeePerGas: "0xff", - maxPriorityFeePerGas: "0xff" - }; + it("allows eip-1559 fee market transactions", async () => { + const tx = { + from, + to: contractAddress, + data: "0x3fa4f245", + maxFeePerGas: "0xff", + maxPriorityFeePerGas: "0xff" + }; - const result = await provider.send("eth_call", [tx, "latest"]); - // we can still get the result when the maxFeePerGas/maxPriorityFeePerGas are set - assert.strictEqual(Quantity.from(result).toNumber(), 5); - }); + const result = await provider.send("eth_call", [tx, "latest"]); + // we can still get the result when the maxFeePerGas/maxPriorityFeePerGas are set + assert.strictEqual(Quantity.from(result).toNumber(), 5); + }); - it("allows gas price to be omitted", async () => { - const result = await provider.send("eth_call", [tx, "latest"]); - // we can get the value if no gas info is given at all - assert.strictEqual(Quantity.from(result).toNumber(), 5); - }); + it("allows gas price to be omitted", async () => { + const result = await provider.send("eth_call", [tx, "latest"]); + // we can get the value if no gas info is given at all + assert.strictEqual(Quantity.from(result).toNumber(), 5); + }); - it("rejects transactions that specify both legacy and eip-1559 transaction fields", async () => { - const tx = { - from, - to: contractAddress, - data: "0x3fa4f245", - maxFeePerGas: "0xff", - maxPriorityFeePerGas: "0xff", - gasPrice: "0x1" - }; - const ethCallProm = provider.send("eth_call", [tx, "latest"]); - await assert.rejects( - ethCallProm, - new Error( - "both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified" - ), - "didn't reject transaction with both legacy and eip-1559 gas fields" - ); - }); + it("rejects transactions that specify both legacy and eip-1559 transaction fields", async () => { + const tx = { + from, + to: contractAddress, + data: "0x3fa4f245", + maxFeePerGas: "0xff", + maxPriorityFeePerGas: "0xff", + gasPrice: "0x1" + }; + const ethCallProm = provider.send("eth_call", [tx, "latest"]); + await assert.rejects( + ethCallProm, + new Error( + "both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified" + ), + "didn't reject transaction with both legacy and eip-1559 gas fields" + ); + }); - it("does not return an empty result for transactions with insufficient gas", async () => { - const tx = { - from, - to: contractAddress, - data: "0x3fa4f245", - gasLimit: "0xf" - }; - await assert.rejects(provider.send("eth_call", [tx, "latest"]), { - message: "VM Exception while processing transaction: out of gas" + it("does not return an empty result for transactions with insufficient gas", async () => { + const tx = { + from, + to: contractAddress, + data: "0x3fa4f245", + gasLimit: "0xf" + }; + await assert.rejects(provider.send("eth_call", [tx, "latest"]), { + message: "VM Exception while processing transaction: out of gas" + }); + }); + + it("rejects transactions with insufficient gas", async () => { + const provider = await getProvider({ + wallet: { deterministic: true } + }); + const tx = { + from, + input: contract.code, + gas: "0xf" + }; + const ethCallProm = provider.send("eth_call", [tx, "latest"]); + const result = { + execResult: { + exceptionError: { error: "out of gas" }, + returnValue: BUFFER_EMPTY, + runState: { programCounter: 0 } + } + } as any; + // the vm error should propagate through to here + await assert.rejects( + ethCallProm, + new CallError(result), + "didn't reject transaction with insufficient gas" + ); + }); + + it("uses the correct baseFee", async () => { + const block = await provider.send("eth_getBlockByNumber", ["latest"]); + const tx = { + from, + to: contractAddress, + data: `0x${contract.contract.evm.methodIdentifiers["getBaseFee()"]}` + }; + const result = await provider.send("eth_call", [tx, "latest"]); + assert.strictEqual(BigInt(result), BigInt(block.baseFeePerGas)); + }); + + it("returns string data property on revert error", async () => { + const tx = { + from, + to: contractAddress, + data: `0x${contract.contract.evm.methodIdentifiers["doARevert()"]}` + }; + const revertString = + "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000011796f75206172652061206661696c757265000000000000000000000000000000"; + await assert.rejects(provider.send("eth_call", [tx, "latest"]), { + message: + "VM Exception while processing transaction: revert you are a failure", + data: revertString + }); }); }); - it("rejects transactions with insufficient gas", async () => { - const provider = await getProvider({ - wallet: { deterministic: true } + describe("vm state overrides", () => { + let provider: EthereumProvider; + let from, to, addr, encodedAddr: string; + let contract: CompileOutput; + let methods: { [methodName: string]: string }; + let contractAddress: string; + + before("compile", () => { + contract = compile(join(__dirname, "./contracts/Inspector.sol"), { + contractName: "Inspector" + }); + methods = contract.contract.evm.methodIdentifiers; }); - const tx = { - from, - input: contract.code, - gas: "0xf" - }; - const ethCallProm = provider.send("eth_call", [tx, "latest"]); - const result = { - execResult: { - exceptionError: { error: "out of gas" }, - returnValue: BUFFER_EMPTY, - runState: { programCounter: 0 } + + beforeEach(async () => { + provider = await getProvider({ + chain: { vmErrorsOnRPCResponse: true } + }); + [from, to, addr] = await provider.send("eth_accounts"); + encodedAddr = encodeValue(addr); + contractAddress = await deployContract(provider, from, contract.code); + }); + + async function callContract(data, overrides) { + return await provider.send("eth_call", [ + { + from, + to: contractAddress, + data + }, + "latest", + overrides + ]); + } + + it("allows override of account nonce", async () => { + // this is a kind of separate test case from the rest, since we can't easily + // access an account's nonce in solidity. instead, we'll use the override to + // set the account's nonce high and send a contract creating transaction. the + // contract address is generated using the account's nonce, so it should be + // different from the same eth_call made without overriding the nonce + const simpleSol = + "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000009e6080604052600560008190555060858060196000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80633fa4f24514602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea26469706673582212200897f7766689bf7a145227297912838b19bcad29039258a293be78e3bf58e20264736f6c63430007040033"; + const data = `0x${methods["createContract(bytes)"]}${simpleSol}`; + const override = { [contractAddress]: { nonce: "0xff" } }; + // call to contract factory function with sender account's nonce set to `0xff` + const overrideNonceAddress = await provider.send("eth_call", [ + { + from: addr, + to: contractAddress, + gas: "0xfffffff", + data + }, + "latest", + override + ]); + const overrideNonceAddress1 = await provider.send("eth_call", [ + { + from: addr, + to: contractAddress, + gas: "0xfffffff", + data + }, + "latest", + override + ]); + // call to contract factory function with sender account's nonce not over written + const defaultNonceAddress = await provider.send("eth_call", [ + { + from: addr, + to: contractAddress, + gas: "0xfffffff", + data + }, + "latest" + ]); + // sanity check: when using the same account nonce, we can repeatedly generate the same contract address + assert.strictEqual(overrideNonceAddress, overrideNonceAddress1); + // the address generated depends on the nonce, so the two are difference + assert.notEqual(overrideNonceAddress, defaultNonceAddress); + }); + + it("allows override of account code", async () => { + const data = `0x${methods["getCode(address)"]}${encodedAddr}`; + const override = { [addr]: { code: "0x123456" } }; + const code = await callContract(data, override); + assert.strictEqual( + code, + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000031234560000000000000000000000000000000000000000000000000000000000" + ); + }); + + it("allows override of account balance", async () => { + const data = `0x${methods["getBalance(address)"]}${encodedAddr}`; + const override = { + [addr]: { balance: "0x1e240" } + }; + const balance = await callContract(data, override); + assert.strictEqual( + balance, + "0x000000000000000000000000000000000000000000000000000000000001e240" + ); + }); + + it("allows override of storage at a slot", async () => { + const slot = `0000000000000000000000000000000000000000000000000000000000000001`; + const data = + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42"; + const contractData = `0x${methods["getStorageAt(uint256)"]}${slot}`; + + // the stateDiff override sets the value at the specified slot, leaving the rest of the storage + // in tact + const override = { + [contractAddress]: { stateDiff: { [`0x${slot}`]: data } } + }; + const storage = await callContract(contractData, override); + assert.strictEqual( + storage, + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42" + ); + }); + + it("allows clearing of storage and writing at a slot", async () => { + const slot = `0000000000000000000000000000000000000000000000000000000000000001`; + const data = + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42"; + const getStorageMethod = `0x${methods["getStorageAt(uint256)"]}${slot}`; + // the state override clears all storage for the contract and sets the value specified at the slot + const override = { + [contractAddress]: { state: { [`0x${slot}`]: data } } + }; + const storage = await callContract(getStorageMethod, override); + assert.strictEqual( + storage, + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42" + ); + // call the contract with the same overrides, but get a different storage slot. it should be cleared + // out. (note, in the contract this storage slot is originally set to 1) + const emptySlot = `0000000000000000000000000000000000000000000000000000000000000000`; + const emptyData = `0x0000000000000000000000000000000000000000000000000000000000000000`; + const getStorageMethod2 = `0x${methods["getStorageAt(uint256)"]}${emptySlot}`; + const storage2 = await callContract(getStorageMethod2, override); + assert.strictEqual(storage2, emptyData); + }); + + it("allows setting multiple overrides in one call", async () => { + const slot1 = `0000000000000000000000000000000000000000000000000000000000000001`; + const slot2 = `0000000000000000000000000000000000000000000000000000000000000002`; + const data = + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42"; + // create a combined set of overrides that we use for each of these calls + const override = { + [addr]: { + balance: "0x1e240", + code: "0x123456" + }, + [contractAddress]: { + stateDiff: { [`0x${slot1}`]: data, [`0x${slot2}`]: data } + } + }; + const getBalanceMethod = `0x${methods["getBalance(address)"]}${encodedAddr}`; + const balance = await callContract(getBalanceMethod, override); + assert.strictEqual( + balance, + "0x000000000000000000000000000000000000000000000000000000000001e240" + ); + + const getCodeMethod = `0x${methods["getCode(address)"]}${encodedAddr}`; + const code = await callContract(getCodeMethod, override); + assert.strictEqual( + code, + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000031234560000000000000000000000000000000000000000000000000000000000" + ); + + const getStorageMethod1 = `0x${methods["getStorageAt(uint256)"]}${slot1}`; + const getStorageMethod2 = `0x${methods["getStorageAt(uint256)"]}${slot2}`; + const storage1 = await callContract(getStorageMethod1, override); + const storage2 = await callContract(getStorageMethod2, override); + assert.strictEqual( + storage1, + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42" + ); + assert.strictEqual( + storage2, + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42" + ); + }); + + it("does not persist overrides", async () => { + const slot = `0000000000000000000000000000000000000000000000000000000000000001`; + const data = + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42"; + // Simulate an unrelated call with overrides. + const overrides = { + [addr]: { + balance: "0x1e240", + code: "0x123456" + }, + [contractAddress]: { nonce: "0xff", state: { [`0x${slot}`]: data } } + }; + const getCodeMethod = `0x${methods["getCode(address)"]}${encodedAddr}`; + const overrideCode = await callContract(getCodeMethod, overrides); + + const code = await callContract(getCodeMethod, {}); + // The overrides should not have persisted. + const rawEmptyBytesEncoded = + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000"; + assert.strictEqual(code, rawEmptyBytesEncoded); + assert.notEqual(code, overrideCode); + + const getBalanceMethod = `0x${methods["getBalance(address)"]}${encodedAddr}`; + const overrideBalance = await callContract( + getBalanceMethod, + overrides + ); + const balance = await callContract(getBalanceMethod, {}); + const startBalance = + "0x00000000000000000000000000000000000000000000003635c9adc5dea00000"; + assert.strictEqual(balance, startBalance); + assert.notEqual(balance, overrideBalance); + + // this is hardcoded in the contract + const getStorageMethod = `0x${methods["getStorageAt(uint256)"]}${slot}`; + const overrideStorage = await callContract( + getStorageMethod, + overrides + ); + const storage = await callContract(getStorageMethod, {}); + const dataAtSlot = `0x0000000000000000000000000000000000000000000000000000000000000002`; + assert.strictEqual(storage, dataAtSlot); + assert.notEqual(storage, overrideStorage); + + const simpleSol = + "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000009e6080604052600560008190555060858060196000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80633fa4f24514602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea26469706673582212200897f7766689bf7a145227297912838b19bcad29039258a293be78e3bf58e20264736f6c63430007040033"; + const testNonceMethod = `0x${methods["createContract(bytes)"]}${simpleSol}`; + const overrideTestContractAddress = await callContract( + testNonceMethod, + overrides + ); + const testContractAddress = await callContract(testNonceMethod, {}); + assert.notEqual(testContractAddress, overrideTestContractAddress); + }); + + it("does not allow both state && stateDiff", async () => { + const slot = `0000000000000000000000000000000000000000000000000000000000000001`; + const data = + "0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42"; + // the stateDiff override sets the value at the specified slot, leaving the rest of the storage + // in tact + const override = { + [contractAddress]: { + stateDiff: { [`0x${slot}`]: data }, + state: { [`0x${slot}`]: data } + } + }; + const getStorageMethod = `0x${methods["getStorageAt(uint256)"]}${slot}`; + await assert.rejects(callContract(getStorageMethod, override), { + message: "both state and stateDiff overrides specified" + }); + }); + + it("does not use invalid override, does use valid override data", async () => { + const slot = `0000000000000000000000000000000000000000000000000000000000000001`; + const data = `0xbaddad42baddad42baddad42baddad42baddad42baddad42baddad42baddad42`; + const simpleSol = + "0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000009e6080604052600560008190555060858060196000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80633fa4f24514602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea26469706673582212200897f7766689bf7a145227297912838b19bcad29039258a293be78e3bf58e20264736f6c63430007040033"; + // to test clearing an account's code, we need another contract that has its code set. + const contractAddress2 = await deployContract( + provider, + from, + contract.code + ); + const currentNonceGeneratedAddress = await callContract( + `0x${methods["createContract(bytes)"]}${simpleSol}`, + {} + ); + const zeroNonceGeneratedAddress = await callContract( + `0x${methods["createContract(bytes)"]}${simpleSol}`, + { [contractAddress]: { nonce: "0x0" } } + ); + const encodedContractAddress = encodeValue(contractAddress2); + const tests = { + balance: { + junks: [ + { + junk: null, + expectedValue: + "0x00000000000000000000000000000000000000000000003635c9adc5dea00000" + }, + { + junk: undefined, + expectedValue: + "0x00000000000000000000000000000000000000000000003635c9adc5dea00000" + }, + { + junk: "", + expectedValue: + "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + junk: "123", + error: `cannot convert string value "123" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: {}, + error: `Cannot wrap a "object" as a json-rpc type` + } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2725 is closed + // { junk: "0xa string", error: `` } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2728 is closed + // { junk: -9, error: `` }, + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2857 is closed + //{ junk: "0x", error: `` }, + ], + contractMethod: `0x${methods["getBalance(address)"]}${encodedAddr}` + }, + code: { + junks: [ + { + junk: null, + expectedValue: `0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000059d${contract.contract.evm.deployedBytecode.object}000000` + }, + { + junk: undefined, + expectedValue: `0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000059d${contract.contract.evm.deployedBytecode.object}000000` + }, + { + junk: "", + expectedValue: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + }, + { + junk: "0x", + expectedValue: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + }, + { + junk: "123", + error: `cannot convert string value "123" into type \`Data\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: {}, + error: `Cannot wrap a "object" as a json-rpc type` + } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2725 is closed + // { junk: "0xa string", error: `` } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2728 is closed + // { junk: -9, error: `` }, + ], + contractMethod: `0x${methods["getCode(address)"]}${encodedContractAddress}` + }, + stateDiff: { + junks: [ + { + junk: null, + error: `StateDiff override data not valid. Received: null`, + expectedValue: null + }, + { + junk: undefined, + error: `StateDiff override data not valid. Received: undefined` + }, + { + junk: "", + error: `cannot convert string value "" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: "0x", + error: `StateDiff override data must be a 64 character hex string. Received 0 character string.` + }, + { + junk: "0xbaddad42", + error: `StateDiff override data must be a 64 character hex string. Received 8 character string.` + }, + { + junk: "123", + error: `cannot convert string value "123" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: {}, + error: `Cannot wrap a "object" as a json-rpc type` + } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2725 is closed + // { junk: "0xa string", error: `` } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2728 is closed + // { junk: -9, error: `` }, + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2857 is closed + //{ junk: "0x", error: `` }, + ], + contractMethod: `0x${methods["getStorageAt(uint256)"]}${slot}` + }, + state: { + junks: [ + { + junk: null, + error: `State override data not valid. Received: null`, + expectedValue: null + }, + { + junk: undefined, + error: `State override data not valid. Received: undefined` + }, + { + junk: "", + error: `cannot convert string value "" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: "0x", + error: `State override data must be a 64 character hex string. Received 0 character string.` + }, + { + junk: "0xbaddad42", + error: `State override data must be a 64 character hex string. Received 8 character string.` + }, + { + junk: "123", + error: `cannot convert string value "123" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: {}, + error: `Cannot wrap a "object" as a json-rpc type` + } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2725 is closed + // { junk: "0xa string", error: `` } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2728 is closed + // { junk: -9, error: `` }, + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2857 is closed + //{ junk: "0x", error: `` }, + ], + contractMethod: `0x${methods["getStorageAt(uint256)"]}${slot}` + }, + stateDiffSlot: { + junks: [ + { + junk: null, + error: `cannot convert string value "null" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".`, + expectedValue: null + }, + { + junk: undefined, + error: `cannot convert string value "undefined" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: "", + error: `cannot convert string value "" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: "0x", + error: `StateDiff override slot must be a 64 character hex string. Received 0 character string.` + }, + { + junk: "0xbaddad42", + error: `StateDiff override slot must be a 64 character hex string. Received 8 character string.` + }, + { + junk: "123", + error: `cannot convert string value "123" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: {}, + error: `cannot convert string value "[object Object]" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2725 is closed + // { junk: "0xa string", error: `` } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2728 is closed + // { junk: -9, error: `` }, + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2857 is closed + //{ junk: "0x", error: `` }, + ], + contractMethod: `0x${methods["getStorageAt(uint256)"]}${slot}` + }, + stateSlot: { + junks: [ + { + junk: null, + error: `cannot convert string value "null" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".`, + expectedValue: null + }, + { + junk: undefined, + error: `cannot convert string value "undefined" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: "", + error: `cannot convert string value "" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: "0x", + error: `State override slot must be a 64 character hex string. Received 0 character string.` + }, + { + junk: "0xbaddad42", + error: `State override slot must be a 64 character hex string. Received 8 character string.` + }, + { + junk: "123", + error: `cannot convert string value "123" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: {}, + error: `cannot convert string value "[object Object]" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2725 is closed + // { junk: "0xa string", error: `` } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2728 is closed + // { junk: -9, error: `` }, + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2857 is closed + //{ junk: "0x", error: `` }, + ], + contractMethod: `0x${methods["getStorageAt(uint256)"]}${slot}` + }, + nonce: { + junks: [ + { + junk: null, + expectedValue: currentNonceGeneratedAddress + }, + { + junk: undefined, + expectedValue: currentNonceGeneratedAddress + }, + { + junk: "", + expectedValue: zeroNonceGeneratedAddress + }, + { + junk: "123", + error: `cannot convert string value "123" into type \`Quantity\`; strings must be hex-encoded and prefixed with "0x".` + }, + { + junk: {}, + error: `Cannot wrap a "object" as a json-rpc type` + } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2725 is closed + // { junk: "0xa string", error: `` } + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2728 is closed + // { junk: -9, error: `` }, + // TODO: add this back once https://github.com/trufflesuite/ganache/issues/2857 is closed + //{ junk: "0x", error: `` }, + ], + contractMethod: `0x${methods["createContract(bytes)"]}${simpleSol}` + } + }; + + const getOverrideForType = (type: string, junk: any) => { + switch (type) { + case "balance": + return { [addr]: { [type]: junk } }; + case "code": + return { [contractAddress2]: { [type]: junk } }; + case "nonce": + return { [contractAddress]: { [type]: junk } }; + case "state": + case "stateDiff": + return { + [contractAddress]: { [type]: { [`0x${slot}`]: junk } } + }; + case "stateSlot": + return { + [contractAddress]: { state: { [junk]: data } } + }; + case "stateDiffSlot": + return { + [contractAddress]: { stateDiff: { [junk]: data } } + }; + default: + return {}; + } + }; + + for (const [type, { contractMethod, junks }] of Object.entries( + tests + )) { + for (const { junk, error, expectedValue } of junks) { + const override = getOverrideForType(type, junk); + const prom = callContract(contractMethod, override); + if (error) { + await assert.rejects( + prom, + new Error(error), + `Failed junk data validation for "${type}" override type with value "${junk}".` + ); + } else { + assert.strictEqual( + await prom, + expectedValue, + `Failed junk data validation for "${type}" override type with value "${junk}".` + ); + } + } } - } as any; - // the vm error should propagate through to here - await assert.rejects( - ethCallProm, - new CallError(result), - "didn't reject transaction with insufficient gas" - ); + }); }); - it("uses the correct baseFee", async () => { - const block = await provider.send("eth_getBlockByNumber", ["latest"]); - const tx = { - from, - to: contractAddress, - data: `0x${contract.contract.evm.methodIdentifiers["getBaseFee()"]}` + describe("changes are ephemeral", () => { + let wallet: Wallet; + let blockchain: Blockchain; + let from: string, to: string; + let simTx: SimulationTransaction; + let parentBlock: Block; + let gas: Quantity; + let ethereumJsFromAddress: EthereumJsAddress, + ethereumJsToAddress: EthereumJsAddress; + let transaction: LegacyRpcTransaction; + let privateKey: Data; + + before(async () => { + const options = EthereumOptionsConfig.normalize({ + logging: { quiet: true } + }); + wallet = new Wallet(options.wallet); + [from, to] = wallet.addresses; + blockchain = new Blockchain( + options, + new Address(wallet.addresses[0]) + ); + await blockchain.initialize(wallet.initialAccounts); + + // set up a simulation transaction + parentBlock = blockchain.blocks.latest; + const parentHeader = parentBlock.header; + gas = Quantity.from("0xfffff"); + const block = new RuntimeBlock( + parentHeader.number, + parentHeader.parentHash, + blockchain.coinbase, + gas.toBuffer(), + parentHeader.gasUsed.toBuffer(), + parentHeader.timestamp, + RPCQUANTITY_ONE, // difficulty + parentHeader.totalDifficulty, + parentHeader.baseFeePerGas.toBigInt() + ); + simTx = { + from: new Address(from), + to: new Address(to), + gas, + gasPrice: Quantity.from("0xfffffffffff"), + value: Quantity.from("0xffff"), + data: Data.from("0xabcdef1234"), + block: block + }; + ethereumJsFromAddress = new EthereumJsAddress( + Quantity.from(from).toBuffer() + ); + ethereumJsToAddress = new EthereumJsAddress( + Quantity.from(to).toBuffer() + ); + // set up a real transaction + transaction = { + from, + to, + gas: gas.toString(), + gasPrice: "0xfffffffffff", + value: "0xffff", + data: "0xabcdef1234" + }; + privateKey = wallet.unlockedAccounts.get(from); + }); + + const getDbData = async (trie: GanacheTrie) => { + let dbData: (string | Buffer)[] = []; + for await (const data of trie.db._leveldb.createReadStream()) { + dbData.push(data); + } + return dbData; }; - const result = await provider.send("eth_call", [tx, "latest"]); - assert.strictEqual(BigInt(result), BigInt(block.baseFeePerGas)); - }); - it("returns string data property on revert error", async () => { - const tx = { - from, - to: contractAddress, - data: `0x${contract.contract.evm.methodIdentifiers["doARevert()"]}` + const getBlockchainState = async () => { + const trie = blockchain.trie.copy(true); + const trieDbData = await getDbData(trie); + const vm = await blockchain.createVmFromStateTrie( + trie, + false, + false, + blockchain.common + ); + const fromState = await vm.stateManager.getAccount( + ethereumJsFromAddress + ); + const toState = await vm.stateManager.getAccount(ethereumJsToAddress); + return { root: trie.root, db: trieDbData, fromState, toState }; }; - const revertString = - "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000011796f75206172652061206661696c757265000000000000000000000000000000"; - await assert.rejects(provider.send("eth_call", [tx, "latest"]), { - message: - "VM Exception while processing transaction: revert you are a failure", - data: revertString + + it("does not persist changes to vm or state trie", async () => { + // copy the trie, its database, the vm, and the accounts + const before = await getBlockchainState(); + + // simulate the transaction + await blockchain.simulateTransaction(simTx, parentBlock, {}); + + // copy the trie, its database, the vm, and the accounts again for comparison + const after = await getBlockchainState(); + + // simulating a transaction does not change any of the data + assert.deepStrictEqual(before, after); + + // as a sanity check, confirm sending a real transaction does alter state + await blockchain.queueTransaction( + TransactionFactory.fromRpc(transaction, blockchain.common), + privateKey + ); + // wait for that new block to be mined + await blockchain.once("block"); + + // copy the trie, its database, the vm, and the accounts again for comparison + const afterTx = await getBlockchainState(); + + // simulating a transaction does change the trie root, db and VM accounts + assert.notDeepStrictEqual(before, afterTx); + }); + + it("does not persist changes to vm or state trie when overrides are set", async () => { + // copy the trie, its database, the vm, and the accounts + const before = await getBlockchainState(); + + // simulate the transaction, also setting overrides + const overrides = { + [from]: { balance: "0x0", nonce: "0xfff", code: "0x12345678" } + }; + await blockchain.simulateTransaction(simTx, parentBlock, overrides); + + // copy the trie, its database, the vm, and the accounts again for comparison + const after = await getBlockchainState(); + + // simulating a transaction with overrides does not change the trie or VM accounts + assert.deepStrictEqual(before, after); + + // as a sanity check, confirm sending a real transaction does alter state + await blockchain.queueTransaction( + TransactionFactory.fromRpc(transaction, blockchain.common), + privateKey + ); + // wait for that new block to be mined + await blockchain.once("block"); + + // copy the trie, its database, the vm, and the accounts again for comparison + const afterTx = await getBlockchainState(); + + // simulating a transaction does change the trie root, db and VM accounts + assert.notDeepStrictEqual(before, afterTx); }); }); }); diff --git a/src/chains/ethereum/ethereum/tests/api/eth/contracts/Inspector.sol b/src/chains/ethereum/ethereum/tests/api/eth/contracts/Inspector.sol new file mode 100644 index 0000000000..b814e9adb4 --- /dev/null +++ b/src/chains/ethereum/ethereum/tests/api/eth/contracts/Inspector.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.11; +pragma experimental ABIEncoderV2; + + +contract Inspector { + uint256 public val1 = 1; + uint256 public val2 = 2; + + function getBalance(address addr) + public + view + returns (uint256) + { + return addr.balance; + } + + function getCode(address addr) + public + view + returns (bytes memory code) + { + assembly { + // retrieve the size of the code, this needs assembly + let size := extcodesize(addr) + // allocate output byte array - this could also be done without assembly + // by using o_code = new bytes(size) + code := mload(0x40) + // new "memory end" including padding + mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) + // store length in memory + mstore(code, size) + // actually retrieve the code, this needs assembly + extcodecopy(addr, add(code, 0x20), 0, size) + } + } + + function getStorageAt(uint256 slot) public view returns (uint256 result) { + assembly { + result := sload(slot) + } + } + + function createContract(bytes memory bytecode) public returns (address contractAddr) { + assembly { + contractAddr := create(0, add(bytecode, 0x20), mload(bytecode)) + } + } +}