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

Commit 3050147

Browse files
authored
Merge branch 'poc/transaction_simulation' into perf/fork-trie-not-secure
2 parents 9423c9f + 9b8a716 commit 3050147

File tree

11 files changed

+498
-178
lines changed

11 files changed

+498
-178
lines changed

src/chains/ethereum/ethereum/src/api.ts

Lines changed: 265 additions & 104 deletions
Large diffs are not rendered by default.

src/chains/ethereum/ethereum/src/blockchain.ts

Lines changed: 149 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import { GanacheStateManager } from "./state-manager";
7878
import { TrieDB } from "./trie-db";
7979
import { Trie } from "@ethereumjs/trie";
8080
import { removeEIP3860InitCodeSizeLimitCheck } from "./helpers/common-helpers";
81+
import { bigIntToBuffer } from "@ganache/utils";
8182

8283
const mclInitPromise = mcl.init(mcl.BLS12_381).then(() => {
8384
mcl.setMapToMode(mcl.IRTF); // set the right map mode; otherwise mapToG2 will return wrong values.
@@ -1106,24 +1107,37 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
11061107
parentBlock: Block,
11071108
overrides: CallOverrides
11081109
) {
1109-
let result: EVMResult;
1110+
const { header } = transaction.block;
1111+
1112+
const timings: { time: number; label: string }[] = [];
11101113

1114+
timings.push({ time: performance.now(), label: "start" });
1115+
1116+
let result: EVMResult;
1117+
const storageChanges = new Map<Buffer, [Buffer, Buffer, Buffer]>();
1118+
const stateChanges = new Map<
1119+
Buffer,
1120+
[[Buffer, Buffer, Buffer, Buffer], [Buffer, Buffer, Buffer, Buffer]]
1121+
>();
11111122
const data = transaction.data;
11121123
let gasLimit = transaction.gas.toBigInt();
11131124
// subtract out the transaction's base fee from the gas limit before
11141125
// simulating the tx, because `runCall` doesn't account for raw gas costs.
11151126
const hasToAddress = transaction.to != null;
11161127
const to = hasToAddress ? new Address(transaction.to.toBuffer()) : null;
11171128

1129+
//todo: getCommonForBlockNumber doesn't presently respect shanghai, so we just assume it's the same common as the fork
1130+
// this won't work as expected if simulating on blocks before shanghai.
11181131
const common = this.fallback
11191132
? this.fallback.getCommonForBlockNumber(
11201133
this.common,
11211134
BigInt(transaction.block.header.number.toString())
11221135
)
11231136
: this.common;
1137+
common.setHardfork("shanghai");
11241138

1125-
const gasLeft =
1126-
gasLimit - calculateIntrinsicGas(data, hasToAddress, common);
1139+
const intrinsicGas = calculateIntrinsicGas(data, hasToAddress, common);
1140+
const gasLeft = gasLimit - intrinsicGas;
11271141

11281142
const transactionContext = {};
11291143
this.emit("ganache:vm:tx:before", {
@@ -1145,24 +1159,54 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
11451159
false, // precompiles have already been initialized in the stateTrie
11461160
common
11471161
);
1148-
1162+
//console.log({ stateRoot: await vm.stateManager.getStateRoot() });
1163+
const stateManager = vm.stateManager as GanacheStateManager;
11491164
// take a checkpoint so the `runCall` never writes to the trie. We don't
11501165
// commit/revert later because this stateTrie is ephemeral anyway.
11511166
await vm.eei.checkpoint();
1152-
1153-
vm.evm.events.on("step", (event: InterpreterStep) => {
1154-
const logs = maybeGetLogs(event);
1155-
if (logs) {
1156-
options.logging.logger.log(...logs);
1157-
this.emit("ganache:vm:tx:console.log", {
1158-
context: transactionContext,
1159-
logs
1160-
});
1167+
vm.evm.events.on("step", async (event: InterpreterStep) => {
1168+
if (
1169+
event.opcode.name === "CALL" ||
1170+
event.opcode.name === "DELEGATECALL" ||
1171+
event.opcode.name === "STATICCALL" ||
1172+
event.opcode.name === "JUMP"
1173+
) {
1174+
//console.log(event.opcode.name);
11611175
}
11621176

1163-
if (!this.#emitStepEvent) return;
1164-
const ganacheStepEvent = makeStepEvent(transactionContext, event);
1165-
this.emit("ganache:vm:tx:step", ganacheStepEvent);
1177+
if (event.opcode.name === "SSTORE") {
1178+
const stackLength = event.stack.length;
1179+
const keyBigInt = event.stack[stackLength - 1];
1180+
const key =
1181+
keyBigInt === 0n
1182+
? BUFFER_32_ZERO
1183+
: // todo: this isn't super efficient, but :shrug: we probably don't do it often
1184+
Data.toBuffer(bigIntToBuffer(keyBigInt), 32);
1185+
const valueBigInt = event.stack[stackLength - 2];
1186+
1187+
const value = Data.toBuffer(bigIntToBuffer(valueBigInt), 32);
1188+
// todo: DELEGATE_CALL might impact the address context from which the `before` value should be fetched
1189+
1190+
const storageTrie = await stateManager.getStorageTrie(
1191+
event.codeAddress.toBuffer()
1192+
);
1193+
1194+
const from = decode<Buffer>(await storageTrie.get(key));
1195+
1196+
/*console.log({
1197+
SSTORE_refund: event.gasRefund,
1198+
address: Data.from(event.codeAddress.toBuffer()),
1199+
key: Data.from(key),
1200+
from: Data.from(from),
1201+
to: Data.from(value)
1202+
});*/
1203+
1204+
storageChanges.set(key, [
1205+
event.codeAddress.toBuffer(),
1206+
from.length === 0 ? Buffer.alloc(32) : Data.toBuffer(from, 32),
1207+
value
1208+
]);
1209+
}
11661210
});
11671211

11681212
const caller = transaction.from.toBuffer();
@@ -1189,14 +1233,20 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
11891233
// we run this transaction so that things that rely on these values
11901234
// are correct (like contract creation!).
11911235
const fromAccount = await vm.eei.getAccount(callerAddress);
1192-
fromAccount.nonce += 1n;
1193-
const txCost = gasLimit * transaction.gasPrice.toBigInt();
1236+
1237+
// todo: re previous comment, incrementing the nonce here results in a double
1238+
// incremented nonce in the result :/ Need to validate whether this is required.
1239+
//fromAccount.nonce += 1n;
1240+
const intrinsicTxCost = intrinsicGas * transaction.gasPrice.toBigInt();
1241+
//todo: does the execution gas get subtracted from the balance?
11941242
const startBalance = fromAccount.balance;
11951243
// TODO: should we throw if insufficient funds?
1196-
fromAccount.balance = txCost > startBalance ? 0n : startBalance - txCost;
1244+
fromAccount.balance =
1245+
intrinsicTxCost > startBalance ? 0n : startBalance - intrinsicTxCost;
11971246
await vm.eei.putAccount(callerAddress, fromAccount);
1198-
11991247
// finally, run the call
1248+
timings.push({ time: performance.now(), label: "running transaction" });
1249+
12001250
result = await vm.evm.runCall({
12011251
caller: callerAddress,
12021252
data: transaction.data && transaction.data.toBuffer(),
@@ -1206,6 +1256,48 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
12061256
value: transaction.value == null ? 0n : transaction.value.toBigInt(),
12071257
block: transaction.block as any
12081258
});
1259+
timings.push({
1260+
time: performance.now(),
1261+
label: "finished running transaction"
1262+
});
1263+
1264+
const afterCache = stateManager["_cache"]["_cache"] as any; // OrderedMap<any, any>
1265+
1266+
const asyncAccounts: Promise<void>[] = [];
1267+
1268+
afterCache.forEach(i => {
1269+
asyncAccounts.push(
1270+
new Promise<void>(async resolve => {
1271+
const addressBuf = Buffer.from(i[0], "hex");
1272+
const beforeAccount = await this.vm.stateManager.getAccount(
1273+
Address.from(addressBuf)
1274+
);
1275+
1276+
// todo: it's a shame to serialize here - should get the raw address directly.
1277+
const beforeRaw = beforeAccount.serialize();
1278+
if (!beforeRaw.equals(i[1].val)) {
1279+
// the account has changed
1280+
const address = Buffer.from(i[0], "hex");
1281+
const after = decode<EthereumRawAccount>(i[1].val);
1282+
const before = [
1283+
Quantity.toBuffer(beforeAccount.nonce),
1284+
Quantity.toBuffer(beforeAccount.balance),
1285+
beforeAccount.storageRoot,
1286+
beforeAccount.codeHash
1287+
] as EthereumRawAccount;
1288+
stateChanges.set(address, [before, after]);
1289+
}
1290+
resolve();
1291+
})
1292+
);
1293+
});
1294+
1295+
await Promise.all(asyncAccounts);
1296+
1297+
timings.push({
1298+
time: performance.now(),
1299+
label: "finished building state diff"
1300+
});
12091301
} else {
12101302
result = {
12111303
execResult: {
@@ -1215,13 +1307,49 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
12151307
}
12161308
} as EVMResult;
12171309
}
1310+
12181311
this.emit("ganache:vm:tx:after", {
12191312
context: transactionContext
12201313
});
12211314
if (result.execResult.exceptionError) {
12221315
throw new CallError(result);
12231316
} else {
1224-
return Data.from(result.execResult.returnValue || "0x");
1317+
const totalGasSpent = result.execResult.executionGasUsed + intrinsicGas;
1318+
const maxRefund = totalGasSpent / 5n;
1319+
const actualRefund =
1320+
result.execResult.gasRefund > maxRefund
1321+
? maxRefund
1322+
: result.execResult.gasRefund;
1323+
1324+
/*console.log({
1325+
totalGasSpent,
1326+
execGas: result.execResult.executionGasUsed,
1327+
maxRefund,
1328+
intrinsicGas,
1329+
refund: result.execResult.gasRefund,
1330+
actualRefund
1331+
});*/
1332+
1333+
//todo: we are treating the property "executionGasUsed" as the total gas
1334+
// cost, which it is not. Probably should derive a return object here,
1335+
// rather than just using the object returned from the EVM.
1336+
result.execResult.executionGasUsed =
1337+
(result.execResult.executionGasUsed || 0n) +
1338+
intrinsicGas -
1339+
actualRefund;
1340+
1341+
const startTime = timings[0].time;
1342+
const timingsSummary = timings.map(({ time, label }) => ({
1343+
label,
1344+
duration: time - startTime
1345+
}));
1346+
1347+
return {
1348+
result: result.execResult,
1349+
storageChanges,
1350+
stateChanges,
1351+
timings: timingsSummary
1352+
};
12251353
}
12261354
}
12271355

src/chains/ethereum/ethereum/src/connector.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,27 +106,29 @@ export class Connector<
106106

107107
format(
108108
result: any,
109-
payload: R
109+
payload: R,
110+
durationMs?: number
110111
): RecognizedString | Generator<RecognizedString>;
111-
format(result: any, payload: R): RecognizedString;
112-
format(results: any[], payloads: R[]): RecognizedString;
112+
format(result: any, payload: R, durationMs?: number): RecognizedString;
113+
format(results: any[], payloads: R[], durationMs?: number): RecognizedString;
113114
format(
114115
results: any | any[],
115-
payload: R | R[]
116+
payload: R | R[],
117+
durationMs?: number
116118
): RecognizedString | Generator<RecognizedString> {
117119
if (Array.isArray(payload)) {
118120
return JSON.stringify(
119121
payload.map((payload, i) => {
120122
const result = results[i];
121123
if (result instanceof Error) {
122-
return makeError(payload.id, result as any);
124+
return makeError(payload.id, result as any, durationMs);
123125
} else {
124-
return makeResponse(payload.id, result);
126+
return makeResponse(payload.id, result, durationMs);
125127
}
126128
})
127129
);
128130
} else {
129-
const json = makeResponse(payload.id, results);
131+
const json = makeResponse(payload.id, results, durationMs);
130132
if (
131133
payload.method === "debug_traceTransaction" &&
132134
typeof results === "object" &&
@@ -159,8 +161,18 @@ export class Connector<
159161
}
160162
}
161163

162-
formatError(error: Error & { code: number }, payload: R): RecognizedString {
163-
const json = makeError(payload && payload.id ? payload.id : null, error);
164+
formatError(
165+
error: Error & { code: number },
166+
payload: R,
167+
durationMs?: number
168+
): RecognizedString {
169+
console.log("Formatting error", durationMs);
170+
const json = makeError(
171+
payload && payload.id ? payload.id : null,
172+
error,
173+
undefined,
174+
durationMs
175+
);
164176
return JSON.stringify(json);
165177
}
166178

src/chains/ethereum/ethereum/src/forking/handlers/base-handler.ts

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,17 @@ export class BaseHandler {
164164
method: string,
165165
params: any[],
166166
key: string,
167-
send: (
168-
...args: unknown[]
169-
) => Promise<{
167+
send: (...args: unknown[]) => Promise<{
170168
response: { result: any } | { error: { message: string; code: number } };
171169
raw: string | Buffer;
172170
}>,
173171
options = { disableCache: false }
174172
): Promise<T> {
173+
const memCached = this.getFromMemCache<T>(key);
174+
if (memCached !== undefined) {
175+
return memCached;
176+
}
175177
if (!options.disableCache) {
176-
const memCached = this.getFromMemCache<T>(key);
177-
if (memCached !== undefined) return memCached;
178-
179178
const diskCached = await this.getFromSlowCache<T>(method, params, key);
180179
if (diskCached !== undefined) {
181180
this.valueCache.set(key, Buffer.from(diskCached.raw));
@@ -189,33 +188,30 @@ export class BaseHandler {
189188
if (this.abortSignal.aborted) return Promise.reject(new AbortError());
190189

191190
if (hasOwn(response, "result")) {
192-
if (!options.disableCache) {
193-
// cache non-error responses only
194-
this.valueCache.set(key, raw);
195-
191+
// cache non-error responses only
192+
this.valueCache.set(key, raw);
193+
if (!options.disableCache && this.persistentCache) {
196194
// swallow errors for the persistentCache, since it's not vital that
197195
// it always works
198-
if (this.persistentCache) {
199-
const prom = this.persistentCache
200-
.put(
201-
method,
202-
params,
203-
key,
204-
typeof raw === "string" ? Buffer.from(raw) : raw
205-
)
206-
.catch(_ => {
207-
// the cache.put may fail if the db is closed while a request
208-
// is in flight. This is a "fire and forget" method.
209-
});
210-
211-
// track these unawaited `puts`
212-
this.fireForget.add(prom);
213-
214-
// clean up once complete
215-
prom.finally(() => {
216-
this.fireForget.delete(prom);
196+
const prom = this.persistentCache
197+
.put(
198+
method,
199+
params,
200+
key,
201+
typeof raw === "string" ? Buffer.from(raw) : raw
202+
)
203+
.catch(_ => {
204+
// the cache.put may fail if the db is closed while a request
205+
// is in flight. This is a "fire and forget" method.
217206
});
218-
}
207+
208+
// track these unawaited `puts`
209+
this.fireForget.add(prom);
210+
211+
// clean up once complete
212+
prom.finally(() => {
213+
this.fireForget.delete(prom);
214+
});
219215
}
220216

221217
return response.result as T;

src/chains/ethereum/ethereum/src/forking/trie.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export class ForkTrie extends GanacheTrie {
162162
// the fork block because we can't possibly delete keys _before_ the fork
163163
// block, since those happened before ganache was even started
164164
// This little optimization can cut debug_traceTransaction time _in half_.
165-
if (!this.isPreForkBlock) {
165+
if (true || !this.isPreForkBlock) {
166166
const delKey = this.createDelKey(key);
167167
const metaDataPutPromise = this.checkpointedMetadata.put(
168168
delKey,
@@ -277,7 +277,7 @@ export class ForkTrie extends GanacheTrie {
277277
// the fork block because we can't possibly delete keys _before_ the fork
278278
// block, since those happened before ganache was even started
279279
// This little optimization can cut debug_traceTransaction time _in half_.
280-
if (!this.isPreForkBlock && (await this.keyWasDeleted(key))) return null;
280+
if (await this.keyWasDeleted(key)) return null;
281281

282282
if (this.address === null) {
283283
// if the trie context's address isn't set, our key represents an address:

0 commit comments

Comments
 (0)