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

Commit

Permalink
feat: allow balance, code, nonce, and state overrides in eth_call (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: MicaiahReid <[email protected]>
Co-authored-by: David Murdoch <[email protected]>
  • Loading branch information
4 people authored Apr 20, 2022
1 parent 81219d7 commit 037e156
Show file tree
Hide file tree
Showing 5 changed files with 1,168 additions and 169 deletions.
55 changes: 45 additions & 10 deletions src/chains/ethereum/ethereum/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -2737,7 +2768,11 @@ export default class EthereumApi implements Api {
block
};

return blockchain.simulateTransaction(simulatedTransaction, parentBlock);
return blockchain.simulateTransaction(
simulatedTransaction,
parentBlock,
overrides
);
}
//#endregion

Expand Down
36 changes: 24 additions & 12 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -367,7 +371,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
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;
}
Expand Down Expand Up @@ -791,7 +795,10 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
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) {
Expand Down Expand Up @@ -980,7 +987,8 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {

public async simulateTransaction(
transaction: SimulationTransaction,
parentBlock: Block
parentBlock: Block,
overrides: CallOverrides
) {
let result: EVMResult;

Expand All @@ -1002,9 +1010,9 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {

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 =
Expand Down Expand Up @@ -1050,6 +1058,10 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
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!).
Expand Down Expand Up @@ -1120,9 +1132,9 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {

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({
Expand Down Expand Up @@ -1268,7 +1280,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
// 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.
Expand Down
137 changes: 136 additions & 1 deletion src/chains/ethereum/ethereum/src/helpers/run-call.ts
Original file line number Diff line number Diff line change
@@ -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 = {
/**
Expand Down Expand Up @@ -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 -
Expand Down Expand Up @@ -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<void> {
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);
}
}
}
}
}
Loading

0 comments on commit 037e156

Please sign in to comment.