@feelyourprotocol/vm 8141.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +583 -0
  3. package/dist/cjs/bloom/index.d.ts +29 -0
  4. package/dist/cjs/bloom/index.d.ts.map +1 -0
  5. package/dist/cjs/bloom/index.js +76 -0
  6. package/dist/cjs/bloom/index.js.map +1 -0
  7. package/dist/cjs/buildBlock.d.ts +118 -0
  8. package/dist/cjs/buildBlock.d.ts.map +1 -0
  9. package/dist/cjs/buildBlock.js +363 -0
  10. package/dist/cjs/buildBlock.js.map +1 -0
  11. package/dist/cjs/constructors.d.ts +9 -0
  12. package/dist/cjs/constructors.d.ts.map +1 -0
  13. package/dist/cjs/constructors.js +75 -0
  14. package/dist/cjs/constructors.js.map +1 -0
  15. package/dist/cjs/emitEVMProfile.d.ts +9 -0
  16. package/dist/cjs/emitEVMProfile.d.ts.map +1 -0
  17. package/dist/cjs/emitEVMProfile.js +130 -0
  18. package/dist/cjs/emitEVMProfile.js.map +1 -0
  19. package/dist/cjs/index.d.ts +11 -0
  20. package/dist/cjs/index.d.ts.map +1 -0
  21. package/dist/cjs/index.js +36 -0
  22. package/dist/cjs/index.js.map +1 -0
  23. package/dist/cjs/package.json +3 -0
  24. package/dist/cjs/params.d.ts +3 -0
  25. package/dist/cjs/params.d.ts.map +1 -0
  26. package/dist/cjs/params.js +105 -0
  27. package/dist/cjs/params.js.map +1 -0
  28. package/dist/cjs/requests.d.ts +11 -0
  29. package/dist/cjs/requests.d.ts.map +1 -0
  30. package/dist/cjs/requests.js +208 -0
  31. package/dist/cjs/requests.js.map +1 -0
  32. package/dist/cjs/runBlock.d.ts +35 -0
  33. package/dist/cjs/runBlock.d.ts.map +1 -0
  34. package/dist/cjs/runBlock.js +797 -0
  35. package/dist/cjs/runBlock.js.map +1 -0
  36. package/dist/cjs/runFrameTx.d.ts +18 -0
  37. package/dist/cjs/runFrameTx.d.ts.map +1 -0
  38. package/dist/cjs/runFrameTx.js +313 -0
  39. package/dist/cjs/runFrameTx.js.map +1 -0
  40. package/dist/cjs/runTx.d.ts +18 -0
  41. package/dist/cjs/runTx.d.ts.map +1 -0
  42. package/dist/cjs/runTx.js +900 -0
  43. package/dist/cjs/runTx.js.map +1 -0
  44. package/dist/cjs/types.d.ts +452 -0
  45. package/dist/cjs/types.d.ts.map +1 -0
  46. package/dist/cjs/types.js +3 -0
  47. package/dist/cjs/types.js.map +1 -0
  48. package/dist/cjs/vm.d.ts +75 -0
  49. package/dist/cjs/vm.d.ts.map +1 -0
  50. package/dist/cjs/vm.js +111 -0
  51. package/dist/cjs/vm.js.map +1 -0
  52. package/dist/esm/bloom/index.d.ts +29 -0
  53. package/dist/esm/bloom/index.d.ts.map +1 -0
  54. package/dist/esm/bloom/index.js +72 -0
  55. package/dist/esm/bloom/index.js.map +1 -0
  56. package/dist/esm/buildBlock.d.ts +118 -0
  57. package/dist/esm/buildBlock.d.ts.map +1 -0
  58. package/dist/esm/buildBlock.js +358 -0
  59. package/dist/esm/buildBlock.js.map +1 -0
  60. package/dist/esm/constructors.d.ts +9 -0
  61. package/dist/esm/constructors.d.ts.map +1 -0
  62. package/dist/esm/constructors.js +72 -0
  63. package/dist/esm/constructors.js.map +1 -0
  64. package/dist/esm/emitEVMProfile.d.ts +9 -0
  65. package/dist/esm/emitEVMProfile.d.ts.map +1 -0
  66. package/dist/esm/emitEVMProfile.js +127 -0
  67. package/dist/esm/emitEVMProfile.js.map +1 -0
  68. package/dist/esm/index.d.ts +11 -0
  69. package/dist/esm/index.d.ts.map +1 -0
  70. package/dist/esm/index.js +11 -0
  71. package/dist/esm/index.js.map +1 -0
  72. package/dist/esm/package.json +3 -0
  73. package/dist/esm/params.d.ts +3 -0
  74. package/dist/esm/params.d.ts.map +1 -0
  75. package/dist/esm/params.js +102 -0
  76. package/dist/esm/params.js.map +1 -0
  77. package/dist/esm/requests.d.ts +11 -0
  78. package/dist/esm/requests.d.ts.map +1 -0
  79. package/dist/esm/requests.js +204 -0
  80. package/dist/esm/requests.js.map +1 -0
  81. package/dist/esm/runBlock.d.ts +35 -0
  82. package/dist/esm/runBlock.d.ts.map +1 -0
  83. package/dist/esm/runBlock.js +790 -0
  84. package/dist/esm/runBlock.js.map +1 -0
  85. package/dist/esm/runFrameTx.d.ts +18 -0
  86. package/dist/esm/runFrameTx.d.ts.map +1 -0
  87. package/dist/esm/runFrameTx.js +310 -0
  88. package/dist/esm/runFrameTx.js.map +1 -0
  89. package/dist/esm/runTx.d.ts +18 -0
  90. package/dist/esm/runTx.d.ts.map +1 -0
  91. package/dist/esm/runTx.js +896 -0
  92. package/dist/esm/runTx.js.map +1 -0
  93. package/dist/esm/types.d.ts +452 -0
  94. package/dist/esm/types.d.ts.map +1 -0
  95. package/dist/esm/types.js +2 -0
  96. package/dist/esm/types.js.map +1 -0
  97. package/dist/esm/vm.d.ts +75 -0
  98. package/dist/esm/vm.d.ts.map +1 -0
  99. package/dist/esm/vm.js +107 -0
  100. package/dist/esm/vm.js.map +1 -0
  101. package/dist/tsconfig.prod.cjs.tsbuildinfo +1 -0
  102. package/dist/tsconfig.prod.esm.tsbuildinfo +1 -0
  103. package/package.json +117 -0
  104. package/src/bloom/index.ts +83 -0
  105. package/src/buildBlock.ts +470 -0
  106. package/src/constructors.ts +91 -0
  107. package/src/emitEVMProfile.ts +151 -0
  108. package/src/index.ts +10 -0
  109. package/src/params.ts +104 -0
  110. package/src/requests.ts +293 -0
  111. package/src/runBlock.ts +1022 -0
  112. package/src/runFrameTx.ts +411 -0
  113. package/src/runTx.ts +1203 -0
  114. package/src/types.ts +511 -0
  115. package/src/vm.ts +147 -0
package/src/runTx.ts ADDED
@@ -0,0 +1,1203 @@
1
+ import { cliqueSigner, createBlockHeader } from '@feelyourprotocol/block'
2
+ import { ConsensusType, Hardfork } from '@feelyourprotocol/common'
3
+ import {
4
+ BinaryTreeAccessWitness,
5
+ type EVM,
6
+ type Log,
7
+ createEIP7708SelfdestructLog,
8
+ } from '@feelyourprotocol/evm'
9
+ import { Capability, isBlob4844Tx, isFrameEIP8141Tx } from '@feelyourprotocol/tx'
10
+ import type {
11
+ AccessList,
12
+ AccessList2930Tx,
13
+ AccessListItem,
14
+ EIP7702CompatibleTx,
15
+ FeeMarket1559Tx,
16
+ FrameEIP8141Tx,
17
+ LegacyTx,
18
+ TypedTransaction,
19
+ } from '@feelyourprotocol/tx'
20
+ import {
21
+ Account,
22
+ Address,
23
+ BIGINT_0,
24
+ BIGINT_1,
25
+ EthereumJSErrorWithoutCode,
26
+ KECCAK256_NULL,
27
+ MAX_UINT64,
28
+ type PrefixedHexString,
29
+ SECP256K1_ORDER_DIV_2,
30
+ bigIntMax,
31
+ bytesToBigInt,
32
+ bytesToHex,
33
+ bytesToUnprefixedHex,
34
+ concatBytes,
35
+ eoaCode7702RecoverAuthority,
36
+ equalsBytes,
37
+ hexToBytes,
38
+ short,
39
+ } from '@feelyourprotocol/util'
40
+ import debugDefault from 'debug'
41
+
42
+ import { Bloom } from './bloom/index.ts'
43
+ import { emitEVMProfile } from './emitEVMProfile.ts'
44
+ import { runFrameTransaction } from './runFrameTx.ts'
45
+
46
+ import type { Block } from '@feelyourprotocol/block'
47
+ import type { Common } from '@feelyourprotocol/common'
48
+ import type {
49
+ AfterTxEvent,
50
+ BaseTxReceipt,
51
+ EIP4844BlobTxReceipt,
52
+ PostByzantiumTxReceipt,
53
+ PreByzantiumTxReceipt,
54
+ RunTxOpts,
55
+ RunTxResult,
56
+ TxReceipt,
57
+ } from './types.ts'
58
+ import type { VM } from './vm.ts'
59
+
60
+ const debug = debugDefault('vm:tx')
61
+ const debugGas = debugDefault('vm:tx:gas')
62
+
63
+ const DEFAULT_HEADER = createBlockHeader()
64
+
65
+ let enableProfiler = false
66
+ const initLabel = 'EVM journal init, address/slot warming, fee validation'
67
+ const balanceNonceLabel = 'Balance/Nonce checks and update'
68
+ const executionLabel = 'Execution'
69
+ const logsGasBalanceLabel = 'Logs, gas usage, account/miner balances'
70
+ const accountsCleanUpLabel = 'Accounts clean up'
71
+ const accessListLabel = 'Access list label'
72
+ const journalCacheCleanUpLabel = 'Journal/cache cleanup'
73
+ const receiptsLabel = 'Receipts'
74
+ const entireTxLabel = 'Entire tx'
75
+
76
+ // EIP-7702 flag: if contract code starts with these 3 bytes, it is a 7702-delegated EOA
77
+ const DELEGATION_7702_FLAG = new Uint8Array([0xef, 0x01, 0x00])
78
+
79
+ /**
80
+ * Process EIP-7702 authorization list tuples.
81
+ * Sets delegation code for authorized accounts and calculates gas refunds.
82
+ *
83
+ * @param vm - The VM instance
84
+ * @param tx - The transaction (must support EIP7702EOACode capability)
85
+ * @param caller - The transaction sender address
86
+ * @param initialGasRefund - The current gas refund amount
87
+ * @returns The updated gas refund amount
88
+ */
89
+ async function processAuthorizationList(
90
+ vm: VM,
91
+ tx: EIP7702CompatibleTx,
92
+ caller: Address,
93
+ initialGasRefund: bigint,
94
+ ): Promise<bigint> {
95
+ let gasRefund = initialGasRefund
96
+ const authorizationList = tx.authorizationList
97
+
98
+ for (let i = 0; i < authorizationList.length; i++) {
99
+ const data = authorizationList[i]
100
+
101
+ // Validate chain ID
102
+ const chainId = data[0]
103
+ const chainIdBN = bytesToBigInt(chainId)
104
+ if (chainIdBN !== BIGINT_0 && chainIdBN !== vm.common.chainId()) {
105
+ continue
106
+ }
107
+
108
+ // Validate nonce bounds
109
+ const authorityNonce = data[2]
110
+ if (bytesToBigInt(authorityNonce) >= MAX_UINT64) {
111
+ // Authority nonce >= 2^64 - 1. Bumping this nonce by one will not make this fit in an uint64.
112
+ // EIPs PR: https://github.com/ethereum/EIPs/pull/8938
113
+ continue
114
+ }
115
+
116
+ // Validate signature malleability (s value)
117
+ const s = data[5]
118
+ if (bytesToBigInt(s) > SECP256K1_ORDER_DIV_2) {
119
+ // Malleability protection to avoid "flipping" a valid signature
120
+ continue
121
+ }
122
+
123
+ // Validate yParity
124
+ const yParity = bytesToBigInt(data[3])
125
+ if (yParity > BIGINT_1) {
126
+ continue
127
+ }
128
+
129
+ // Recover authority address from signature
130
+ let authority: Address
131
+ try {
132
+ authority = eoaCode7702RecoverAuthority(data)
133
+ } catch {
134
+ // Invalid signature
135
+ continue
136
+ }
137
+
138
+ const accountMaybeUndefined = await vm.stateManager.getAccount(authority)
139
+ const accountExists = accountMaybeUndefined !== undefined
140
+ const account = accountMaybeUndefined ?? new Account()
141
+
142
+ // Add authority address to warm addresses
143
+ vm.evm.journal.addAlwaysWarmAddress(authority.toString())
144
+
145
+ // EIP-7928: Add authority address to BAL (even if authorization fails later,
146
+ // the account was accessed to check nonce/code)
147
+ if (vm.common.isActivatedEIP(7928)) {
148
+ vm.evm.blockLevelAccessList!.addAddress(authority.toString())
149
+ }
150
+
151
+ // Skip if account is a "normal" contract (not 7702-delegated)
152
+ if (account.isContract()) {
153
+ const code = await vm.stateManager.getCode(authority)
154
+ if (!equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) {
155
+ continue
156
+ }
157
+ }
158
+
159
+ // Nonce validation
160
+ if (caller.toString() === authority.toString()) {
161
+ // Edge case: caller is the authority (self-signing delegation)
162
+ // Virtually bump the account nonce by one for comparison
163
+ if (account.nonce + BIGINT_1 !== bytesToBigInt(authorityNonce)) {
164
+ continue
165
+ }
166
+ } else if (account.nonce !== bytesToBigInt(authorityNonce)) {
167
+ continue
168
+ }
169
+
170
+ // Calculate gas refund for existing accounts
171
+ if (accountExists) {
172
+ const refund = tx.common.param('perEmptyAccountCost') - tx.common.param('perAuthBaseGas')
173
+ gasRefund += refund
174
+ }
175
+
176
+ // Update account nonce and store
177
+ account.nonce++
178
+ await vm.evm.journal.putAccount(authority, account)
179
+ if (vm.common.isActivatedEIP(7928)) {
180
+ vm.evm.blockLevelAccessList!.addNonceChange(
181
+ authority.toString(),
182
+ account.nonce,
183
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
184
+ )
185
+ }
186
+
187
+ // Set delegation code
188
+ const address = data[1]
189
+ // Get current code before modifying (needed for BAL tracking)
190
+ const currentCode = vm.common.isActivatedEIP(7928)
191
+ ? await vm.stateManager.getCode(authority)
192
+ : undefined
193
+ if (equalsBytes(address, new Uint8Array(20))) {
194
+ // Special case: clear delegation when delegating to zero address
195
+ // See EIP PR: https://github.com/ethereum/EIPs/pull/8929
196
+ await vm.stateManager.putCode(authority, new Uint8Array())
197
+ if (vm.common.isActivatedEIP(7928)) {
198
+ vm.evm.blockLevelAccessList!.addCodeChange(
199
+ authority.toString(),
200
+ new Uint8Array(),
201
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
202
+ currentCode,
203
+ )
204
+ }
205
+ } else {
206
+ const addressCode = concatBytes(DELEGATION_7702_FLAG, address)
207
+ await vm.stateManager.putCode(authority, addressCode)
208
+ if (vm.common.isActivatedEIP(7928)) {
209
+ vm.evm.blockLevelAccessList!.addCodeChange(
210
+ authority.toString(),
211
+ addressCode,
212
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
213
+ currentCode,
214
+ )
215
+ }
216
+ }
217
+ }
218
+
219
+ return gasRefund
220
+ }
221
+
222
+ /**
223
+ * Process selfdestruct cleanup for accounts marked for destruction.
224
+ * Handles EIP-6780 restrictions (only delete contracts created in same tx).
225
+ *
226
+ * @param vm - The VM instance
227
+ * @param results - The execution results containing selfdestruct list
228
+ */
229
+ async function processSelfdestructs(vm: VM, results: RunTxResult): Promise<void> {
230
+ if (results.execResult.selfdestruct === undefined) {
231
+ return
232
+ }
233
+
234
+ const destroyedForBAL: Set<PrefixedHexString> = new Set()
235
+ const finalizationLogs: Log[] = []
236
+ const sortedSelfdestructs = [...results.execResult.selfdestruct.entries()].sort(([a], [b]) =>
237
+ a.localeCompare(b),
238
+ )
239
+
240
+ for (const [addressToSelfdestructHex] of sortedSelfdestructs) {
241
+ const address = new Address(hexToBytes(addressToSelfdestructHex))
242
+
243
+ // EIP-6780: Only delete contracts created in the same transaction
244
+ if (vm.common.isActivatedEIP(6780)) {
245
+ if (!results.execResult.createdAddresses!.has(address.toString())) {
246
+ continue
247
+ }
248
+ }
249
+
250
+ if (vm.common.isActivatedEIP(7708)) {
251
+ const account = await vm.stateManager.getAccount(address)
252
+ const finalizationBalance = account?.balance ?? BIGINT_0
253
+ if (finalizationBalance > BIGINT_0) {
254
+ finalizationLogs.push(createEIP7708SelfdestructLog(address, finalizationBalance))
255
+ }
256
+ }
257
+
258
+ await vm.evm.journal.deleteAccount(address)
259
+ destroyedForBAL.add(address.toString())
260
+ if (vm.DEBUG) {
261
+ debug(`tx selfdestruct on address=${address}`)
262
+ }
263
+ }
264
+
265
+ if (finalizationLogs.length > 0) {
266
+ results.execResult.logs = [...(results.execResult.logs ?? []), ...finalizationLogs]
267
+ }
268
+
269
+ if (destroyedForBAL.size > 0 && vm.common.isActivatedEIP(7928)) {
270
+ vm.evm.blockLevelAccessList!.cleanupSelfdestructed([...destroyedForBAL])
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Build the access list result from the journal's tracked accesses.
276
+ * Converts the internal Map format to the standard AccessList format.
277
+ *
278
+ * @param vm - The VM instance
279
+ * @returns The formatted access list
280
+ */
281
+ function buildAccessListResult(vm: VM): AccessList {
282
+ const accessList: AccessList = []
283
+
284
+ for (const [address, storageSet] of vm.evm.journal.accessList!) {
285
+ const item: AccessListItem = {
286
+ address: `0x${address}`,
287
+ storageKeys: [],
288
+ }
289
+ for (const slot of storageSet) {
290
+ item.storageKeys.push(`0x${slot}`)
291
+ }
292
+ accessList.push(item)
293
+ }
294
+
295
+ return accessList
296
+ }
297
+
298
+ /**
299
+ * Update the miner's account balance with the transaction fee.
300
+ * Handles both EIP-1559 (priority fee only) and legacy (full gas price) fee models.
301
+ *
302
+ * @param vm - The VM instance
303
+ * @param state - The state manager
304
+ * @param block - The block (optional)
305
+ * @param results - The transaction results to update with minerValue
306
+ * @param inclusionFeePerGas - The priority fee per gas (for EIP-1559)
307
+ */
308
+ async function updateMinerBalance(
309
+ vm: VM,
310
+ state: VM['stateManager'],
311
+ block: Block | undefined,
312
+ results: RunTxResult,
313
+ inclusionFeePerGas: bigint,
314
+ ): Promise<void> {
315
+ // Determine miner address based on consensus type
316
+ let miner: Address
317
+ if (vm.common.consensusType() === ConsensusType.ProofOfAuthority) {
318
+ miner = cliqueSigner(block?.header ?? DEFAULT_HEADER)
319
+ } else {
320
+ miner = block?.header.coinbase ?? DEFAULT_HEADER.coinbase
321
+ }
322
+
323
+ // Get or create miner account
324
+ let minerAccount = await state.getAccount(miner)
325
+ if (minerAccount === undefined) {
326
+ minerAccount = new Account()
327
+ }
328
+
329
+ // Calculate miner value: priority fee for EIP-1559, full amount for legacy
330
+ results.minerValue = vm.common.isActivatedEIP(1559)
331
+ ? results.totalGasSpent * inclusionFeePerGas
332
+ : results.amountSpent
333
+ const minerOriginalBalance = minerAccount.balance
334
+ minerAccount.balance += results.minerValue
335
+ if (vm.common.isActivatedEIP(7928)) {
336
+ if (results.minerValue !== BIGINT_0) {
337
+ vm.evm.blockLevelAccessList!.addBalanceChange(
338
+ miner.toString(),
339
+ minerAccount.balance,
340
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
341
+ minerOriginalBalance,
342
+ )
343
+ } else {
344
+ // EIP-7928: If the COINBASE reward is zero, the COINBASE address
345
+ // MUST be included as a read (address only, no balance change)
346
+ vm.evm.blockLevelAccessList!.addAddress(miner.toString())
347
+ }
348
+ }
349
+
350
+ // Store updated miner account
351
+ // Note: If balance remains zero, account is marked as "touched" and may be
352
+ // removed during cleanup for forks >= SpuriousDragon
353
+ await vm.evm.journal.putAccount(miner, minerAccount)
354
+
355
+ if (vm.DEBUG) {
356
+ debug(`tx update miner account (${miner}) balance (-> ${minerAccount.balance})`)
357
+ }
358
+ }
359
+
360
+ /**
361
+ * @ignore
362
+ */
363
+ export async function runTx(vm: VM, opts: RunTxOpts): Promise<RunTxResult> {
364
+ if (vm['_opts'].profilerOpts?.reportAfterTx === true) {
365
+ enableProfiler = true
366
+ }
367
+
368
+ if (enableProfiler) {
369
+ const title = `Profiler run - Tx ${bytesToHex(opts.tx.hash())}`
370
+ // eslint-disable-next-line no-console
371
+ console.log(title)
372
+ // eslint-disable-next-line no-console
373
+ console.time(initLabel)
374
+ // eslint-disable-next-line no-console
375
+ console.time(entireTxLabel)
376
+ }
377
+
378
+ if (opts.skipHardForkValidation !== true && opts.block !== undefined) {
379
+ // If block and tx don't have a same hardfork, set tx hardfork to block
380
+ if (opts.tx.common.hardfork() !== opts.block.common.hardfork()) {
381
+ opts.tx.common.setHardfork(opts.block.common.hardfork())
382
+ }
383
+ if (opts.block.common.hardfork() !== vm.common.hardfork()) {
384
+ // Block and VM's hardfork should match as well
385
+ const msg = _errorMsg('block has a different hardfork than the vm', vm, opts.block, opts.tx)
386
+ throw EthereumJSErrorWithoutCode(msg)
387
+ }
388
+ }
389
+
390
+ const gasLimit = opts.block?.header.gasLimit ?? DEFAULT_HEADER.gasLimit
391
+ if (opts.skipBlockGasLimitValidation !== true && gasLimit < opts.tx.gasLimit) {
392
+ const msg = _errorMsg('tx has a higher gas limit than the block', vm, opts.block, opts.tx)
393
+ throw EthereumJSErrorWithoutCode(msg)
394
+ }
395
+
396
+ // Ensure we start with a clear warmed accounts Map
397
+ await vm.evm.journal.cleanup()
398
+
399
+ if (opts.reportAccessList === true) {
400
+ vm.evm.journal.startReportingAccessList()
401
+ }
402
+
403
+ if (opts.reportPreimages === true) {
404
+ vm.evm.journal.startReportingPreimages!()
405
+ }
406
+
407
+ await vm.evm.journal.checkpoint()
408
+ if (vm.DEBUG) {
409
+ debug('-'.repeat(100))
410
+ debug(`tx checkpoint`)
411
+ }
412
+
413
+ // Typed transaction specific setup tasks
414
+ if (opts.tx.supports(Capability.EIP2718TypedTransaction) && vm.common.isActivatedEIP(2718)) {
415
+ // Is it an Access List transaction?
416
+ if (!vm.common.isActivatedEIP(2930)) {
417
+ await vm.evm.journal.revert()
418
+ const msg = _errorMsg(
419
+ 'Cannot run transaction: EIP 2930 is not activated.',
420
+ vm,
421
+ opts.block,
422
+ opts.tx,
423
+ )
424
+ throw EthereumJSErrorWithoutCode(msg)
425
+ }
426
+ if (opts.tx.supports(Capability.EIP1559FeeMarket) && !vm.common.isActivatedEIP(1559)) {
427
+ await vm.evm.journal.revert()
428
+ const msg = _errorMsg(
429
+ 'Cannot run transaction: EIP 1559 is not activated.',
430
+ vm,
431
+ opts.block,
432
+ opts.tx,
433
+ )
434
+ throw EthereumJSErrorWithoutCode(msg)
435
+ }
436
+
437
+ if (!isFrameEIP8141Tx(opts.tx)) {
438
+ const castedTx = opts.tx as AccessList2930Tx
439
+
440
+ for (const accessListItem of castedTx.accessList) {
441
+ const [addressBytes, slotBytesList] = accessListItem
442
+ // Using deprecated bytesToUnprefixedHex for performance: journal methods expect unprefixed hex strings for Map/Set lookups.
443
+ const address = bytesToUnprefixedHex(addressBytes)
444
+ vm.evm.journal.addAlwaysWarmAddress(address, true)
445
+ for (const storageKey of slotBytesList) {
446
+ vm.evm.journal.addAlwaysWarmSlot(address, bytesToUnprefixedHex(storageKey), true)
447
+ }
448
+ }
449
+ }
450
+ }
451
+
452
+ try {
453
+ const result = await _runTx(vm, opts)
454
+ await vm.evm.journal.commit()
455
+ if (vm.DEBUG) {
456
+ debug(`tx checkpoint committed`)
457
+ }
458
+ return result
459
+ } catch (e: any) {
460
+ await vm.evm.journal.revert()
461
+ if (vm.DEBUG) {
462
+ debug(`tx checkpoint reverted`)
463
+ }
464
+ throw e
465
+ } finally {
466
+ if (vm.common.isActivatedEIP(2929)) {
467
+ vm.evm.journal.cleanJournal()
468
+ }
469
+ vm.evm.stateManager.originalStorageCache.clear()
470
+ if (enableProfiler) {
471
+ // eslint-disable-next-line no-console
472
+ console.timeEnd(entireTxLabel)
473
+ const logs = (vm.evm as EVM).getPerformanceLogs()
474
+ if (logs.precompiles.length === 0 && logs.opcodes.length === 0) {
475
+ // eslint-disable-next-line no-console
476
+ console.log('No precompile or opcode execution.')
477
+ }
478
+ emitEVMProfile(logs.precompiles, 'Precompile performance')
479
+ emitEVMProfile(logs.opcodes, 'Opcodes performance')
480
+ ;(vm.evm as EVM).clearPerformanceLogs()
481
+ }
482
+ }
483
+ }
484
+
485
+ async function _runTx(vm: VM, opts: RunTxOpts): Promise<RunTxResult> {
486
+ const state = vm.stateManager
487
+
488
+ // ===========================
489
+ // SETUP: Binary Tree Witness
490
+ // ===========================
491
+ let stateAccesses: BinaryTreeAccessWitness | undefined
492
+ let txAccesses: BinaryTreeAccessWitness | undefined
493
+
494
+ if (vm.common.isActivatedEIP(7864)) {
495
+ if (vm.evm.binaryTreeAccessWitness === undefined) {
496
+ throw Error(`Binary tree access witness needed for execution of binary tree blocks`)
497
+ }
498
+
499
+ // Check if statemanager is a BinaryTreeStateManager by checking for a method only on BinaryTreeStateManager API
500
+ if (!('verifyBinaryPostState' in vm.stateManager)) {
501
+ throw EthereumJSErrorWithoutCode(
502
+ `Binary tree State Manager needed for execution of binary tree blocks`,
503
+ )
504
+ }
505
+ stateAccesses = vm.evm.binaryTreeAccessWitness
506
+ txAccesses = new BinaryTreeAccessWitness({
507
+ hashFunction: vm.evm.binaryTreeAccessWitness.hashFunction,
508
+ })
509
+ }
510
+
511
+ // ===========================
512
+ // SETUP: Transaction and Events
513
+ // ===========================
514
+ const { tx, block } = opts
515
+
516
+ /** The `beforeTx` event - emits the Transaction that is about to be processed */
517
+ await vm._emit('beforeTx', tx)
518
+
519
+ const caller = tx.getSenderAddress()
520
+ if (vm.DEBUG) {
521
+ debug(
522
+ `New tx run hash=${
523
+ opts.tx.isSigned() ? bytesToHex(opts.tx.hash()) : 'unsigned'
524
+ } sender=${caller}`,
525
+ )
526
+ }
527
+
528
+ // ===========================
529
+ // SETUP: Address Warming (EIP-2929)
530
+ // ===========================
531
+ if (vm.common.isActivatedEIP(2929)) {
532
+ // Add origin, precompiles, and relevant addresses to warm set
533
+ const activePrecompiles = vm.evm.precompiles
534
+ for (const [addressStr] of activePrecompiles.entries()) {
535
+ vm.evm.journal.addAlwaysWarmAddress(addressStr)
536
+ }
537
+ vm.evm.journal.addAlwaysWarmAddress(caller.toString())
538
+ if (tx.to !== undefined) {
539
+ // Note: in case we create a contract, we do vm in EVMs `_executeCreate` (vm is also correct in inner calls, per the EIP)
540
+ // Using deprecated bytesToUnprefixedHex for performance: journal methods expect unprefixed hex strings.
541
+ vm.evm.journal.addAlwaysWarmAddress(bytesToUnprefixedHex(tx.to.bytes))
542
+ }
543
+ if (vm.common.isActivatedEIP(3651)) {
544
+ const coinbase = block?.header.coinbase.bytes ?? DEFAULT_HEADER.coinbase.bytes
545
+ // Using deprecated bytesToUnprefixedHex for performance: journal methods expect unprefixed hex strings.
546
+ vm.evm.journal.addAlwaysWarmAddress(bytesToUnprefixedHex(coinbase))
547
+ }
548
+ }
549
+
550
+ // ===========================
551
+ // VALIDATION: Gas Limit and Fees
552
+ // ===========================
553
+ // Validate gas limit against tx base fee (DataFee + TxFee + Creation Fee)
554
+ const intrinsicGas = tx.getIntrinsicGas()
555
+ let floorCost = BIGINT_0
556
+
557
+ // EIP-7623: Calculate floor cost for calldata
558
+ if (vm.common.isActivatedEIP(7623)) {
559
+ // Tx should at least cover the floor price for tx data
560
+ let tokens = 0
561
+ for (let i = 0; i < tx.data.length; i++) {
562
+ tokens += tx.data[i] === 0 ? 1 : 4
563
+ }
564
+ floorCost =
565
+ tx.common.param('txGas') + tx.common.param('totalCostFloorPerToken') * BigInt(tokens)
566
+ }
567
+
568
+ let gasLimit = tx.gasLimit
569
+ const minGasLimit = bigIntMax(intrinsicGas, floorCost)
570
+ if (gasLimit < minGasLimit) {
571
+ const msg = _errorMsg(
572
+ `tx gas limit ${Number(gasLimit)} is lower than the minimum gas limit of ${Number(
573
+ minGasLimit,
574
+ )}`,
575
+ vm,
576
+ block,
577
+ tx,
578
+ )
579
+ throw EthereumJSErrorWithoutCode(msg)
580
+ }
581
+ gasLimit -= intrinsicGas
582
+ if (vm.DEBUG) {
583
+ debugGas(`Subtracting base fee (${intrinsicGas}) from gasLimit (-> ${gasLimit})`)
584
+ }
585
+
586
+ if (vm.common.isActivatedEIP(1559)) {
587
+ // EIP-1559 spec:
588
+ // Ensure that the user was willing to at least pay the base fee
589
+ // assert transaction.max_fee_per_gas >= block.base_fee_per_gas
590
+ const maxFeePerGas = 'maxFeePerGas' in tx ? tx.maxFeePerGas : tx.gasPrice
591
+ const baseFeePerGas = block?.header.baseFeePerGas ?? DEFAULT_HEADER.baseFeePerGas!
592
+ if (maxFeePerGas < baseFeePerGas) {
593
+ const msg = _errorMsg(
594
+ `Transaction's ${
595
+ 'maxFeePerGas' in tx ? 'maxFeePerGas' : 'gasPrice'
596
+ } (${maxFeePerGas}) is less than the block's baseFeePerGas (${baseFeePerGas})`,
597
+ vm,
598
+ block,
599
+ tx,
600
+ )
601
+ throw EthereumJSErrorWithoutCode(msg)
602
+ }
603
+ }
604
+ if (enableProfiler) {
605
+ // eslint-disable-next-line no-console
606
+ console.timeEnd(initLabel)
607
+ // eslint-disable-next-line no-console
608
+ console.time(balanceNonceLabel)
609
+ }
610
+
611
+ // ===========================
612
+ // VALIDATION: Sender Account
613
+ // ===========================
614
+ let fromAccount = await state.getAccount(caller)
615
+ if (fromAccount === undefined) {
616
+ fromAccount = new Account()
617
+ }
618
+ const { nonce, balance } = fromAccount
619
+ if (vm.DEBUG) {
620
+ debug(`Sender's pre-tx balance is ${balance}`)
621
+ }
622
+
623
+ // EIP-3607: Reject transactions from senders with deployed code
624
+ // EIP-8141 frame transactions allow smart accounts (sender may have code)
625
+ if (!equalsBytes(fromAccount.codeHash, KECCAK256_NULL) && !isFrameEIP8141Tx(tx)) {
626
+ const isActive7702 = vm.common.isActivatedEIP(7702)
627
+ switch (isActive7702) {
628
+ case true: {
629
+ const code = await state.getCode(caller)
630
+ // If the EOA is 7702-delegated, sending txs from this EOA is fine
631
+ if (equalsBytes(code.slice(0, 3), DELEGATION_7702_FLAG)) break
632
+ // Trying to send TX from account with code (which is not 7702-delegated), falls through and throws
633
+ }
634
+ default: {
635
+ const msg = _errorMsg(
636
+ 'invalid sender address, address is not EOA (EIP-3607)',
637
+ vm,
638
+ block,
639
+ tx,
640
+ )
641
+ throw EthereumJSErrorWithoutCode(msg)
642
+ }
643
+ }
644
+ }
645
+
646
+ // Check balance against upfront tx cost
647
+ const baseFeePerGas = block?.header.baseFeePerGas ?? DEFAULT_HEADER.baseFeePerGas
648
+ const upFrontCost = tx.getUpfrontCost(baseFeePerGas)
649
+ if (balance < upFrontCost) {
650
+ if (opts.skipBalance === true && fromAccount.balance < upFrontCost) {
651
+ if (tx.supports(Capability.EIP1559FeeMarket) === false) {
652
+ // if skipBalance and not EIP1559 transaction, ensure caller balance is enough to run transaction
653
+ const originalBalance = fromAccount.balance
654
+ fromAccount.balance = upFrontCost
655
+ await vm.evm.journal.putAccount(caller, fromAccount)
656
+ if (vm.common.isActivatedEIP(7928)) {
657
+ vm.evm.blockLevelAccessList!.addBalanceChange(
658
+ caller.toString(),
659
+ fromAccount.balance,
660
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
661
+ originalBalance,
662
+ )
663
+ }
664
+ }
665
+ } else {
666
+ const msg = _errorMsg(
667
+ `sender doesn't have enough funds to send tx. The upfront cost is: ${upFrontCost} and the sender's account (${caller}) only has: ${balance}`,
668
+ vm,
669
+ block,
670
+ tx,
671
+ )
672
+ throw EthereumJSErrorWithoutCode(msg)
673
+ }
674
+ }
675
+
676
+ // Check balance against max potential cost (for EIP 1559 and 4844)
677
+ let maxCost = tx.value
678
+ let blobGasPrice = BIGINT_0
679
+ let totalblobGas = BIGINT_0
680
+ if (tx.supports(Capability.EIP1559FeeMarket)) {
681
+ // EIP-1559 spec:
682
+ // The signer must be able to afford the transaction
683
+ // `assert balance >= gas_limit * max_fee_per_gas`
684
+ maxCost += tx.gasLimit * (tx as FeeMarket1559Tx).maxFeePerGas
685
+ }
686
+
687
+ if (isBlob4844Tx(tx)) {
688
+ if (!vm.common.isActivatedEIP(4844)) {
689
+ const msg = _errorMsg('blob transactions are only valid with EIP4844 active', vm, block, tx)
690
+ throw EthereumJSErrorWithoutCode(msg)
691
+ }
692
+ // EIP-4844 spec
693
+ // the signer must be able to afford the transaction
694
+ // assert signer(tx).balance >= tx.message.gas * tx.message.max_fee_per_gas + get_total_data_gas(tx) * tx.message.max_fee_per_data_gas
695
+ totalblobGas = vm.common.param('blobGasPerBlob') * BigInt(tx.numBlobs())
696
+ maxCost += totalblobGas * tx.maxFeePerBlobGas
697
+
698
+ // 4844 minimum blobGas price check
699
+ blobGasPrice = opts.block?.header.getBlobGasPrice() ?? DEFAULT_HEADER.getBlobGasPrice()
700
+ if (tx.maxFeePerBlobGas < blobGasPrice) {
701
+ const msg = _errorMsg(
702
+ `Transaction's maxFeePerBlobGas ${tx.maxFeePerBlobGas}) is less than block blobGasPrice (${blobGasPrice}).`,
703
+ vm,
704
+ block,
705
+ tx,
706
+ )
707
+ throw EthereumJSErrorWithoutCode(msg)
708
+ }
709
+ }
710
+
711
+ if (fromAccount.balance < maxCost) {
712
+ if (opts.skipBalance === true && fromAccount.balance < maxCost) {
713
+ // if skipBalance, ensure caller balance is enough to run transaction
714
+ const originalBalance = fromAccount.balance
715
+ fromAccount.balance = maxCost
716
+ await vm.evm.journal.putAccount(caller, fromAccount)
717
+ if (vm.common.isActivatedEIP(7928)) {
718
+ vm.evm.blockLevelAccessList!.addBalanceChange(
719
+ caller.toString(),
720
+ fromAccount.balance,
721
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
722
+ originalBalance,
723
+ )
724
+ }
725
+ } else {
726
+ const msg = _errorMsg(
727
+ `sender doesn't have enough funds to send tx. The max cost is: ${maxCost} and the sender's account (${caller}) only has: ${balance}`,
728
+ vm,
729
+ block,
730
+ tx,
731
+ )
732
+ throw EthereumJSErrorWithoutCode(msg)
733
+ }
734
+ }
735
+
736
+ if (opts.skipNonce !== true) {
737
+ if (nonce !== tx.nonce) {
738
+ const msg = _errorMsg(
739
+ `the tx doesn't have the correct nonce. account has nonce of: ${nonce} tx has nonce of: ${tx.nonce}`,
740
+ vm,
741
+ block,
742
+ tx,
743
+ )
744
+ throw EthereumJSErrorWithoutCode(msg)
745
+ }
746
+ }
747
+
748
+ // ===========================
749
+ // CALCULATION: Gas Price
750
+ // ===========================
751
+ let gasPrice: bigint
752
+ let inclusionFeePerGas: bigint
753
+ if (tx.supports(Capability.EIP1559FeeMarket)) {
754
+ // TODO make txs use the new getEffectivePriorityFee
755
+ const baseFee = block?.header.baseFeePerGas ?? DEFAULT_HEADER.baseFeePerGas!
756
+ inclusionFeePerGas = tx.getEffectivePriorityFee(baseFee)
757
+
758
+ gasPrice = inclusionFeePerGas + baseFee
759
+ } else {
760
+ // Have to cast as legacy tx since EIP1559 tx does not have gas price
761
+ gasPrice = (tx as LegacyTx).gasPrice
762
+ if (vm.common.isActivatedEIP(1559)) {
763
+ const baseFee = block?.header.baseFeePerGas ?? DEFAULT_HEADER.baseFeePerGas!
764
+ inclusionFeePerGas = (tx as LegacyTx).gasPrice - baseFee
765
+ }
766
+ }
767
+
768
+ // EIP-4844 tx
769
+ let blobVersionedHashes
770
+ if (isBlob4844Tx(tx)) {
771
+ blobVersionedHashes = tx.blobVersionedHashes
772
+ }
773
+
774
+ // ===========================
775
+ // EIP-8141: Frame Transaction Execution (separate path)
776
+ // ===========================
777
+ let results: RunTxResult
778
+ let txCost: bigint
779
+ let blobGasCost: bigint
780
+ let gasRefund = BIGINT_0
781
+
782
+ let executionTimerPrecise: number | undefined
783
+ if (isFrameEIP8141Tx(tx)) {
784
+ // Frame transactions: gas deduction and nonce increment happen inside APPROVE
785
+ txCost = BIGINT_0
786
+ blobGasCost = BIGINT_0
787
+
788
+ if (enableProfiler) {
789
+ // eslint-disable-next-line no-console
790
+ console.timeEnd(balanceNonceLabel)
791
+ executionTimerPrecise = performance.now()
792
+ }
793
+
794
+ results = await runFrameTransaction(
795
+ vm,
796
+ tx as unknown as FrameEIP8141Tx,
797
+ gasPrice,
798
+ blobGasPrice,
799
+ intrinsicGas,
800
+ block,
801
+ opts,
802
+ )
803
+ } else {
804
+ // ===========================
805
+ // STATE UPDATE: Deduct Costs (standard transactions)
806
+ // ===========================
807
+ txCost = tx.gasLimit * gasPrice
808
+ blobGasCost = totalblobGas * blobGasPrice
809
+ const senderOriginalBalance = fromAccount.balance
810
+ fromAccount.balance -= txCost
811
+ fromAccount.balance -= blobGasCost
812
+ if (opts.skipBalance === true && fromAccount.balance < BIGINT_0) {
813
+ fromAccount.balance = BIGINT_0
814
+ }
815
+ await vm.evm.journal.putAccount(caller, fromAccount)
816
+
817
+ if (vm.common.isActivatedEIP(7928)) {
818
+ vm.evm.blockLevelAccessList!.addBalanceChange(
819
+ caller.toString(),
820
+ fromAccount.balance,
821
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
822
+ senderOriginalBalance,
823
+ )
824
+ }
825
+
826
+ // Process EIP-7702 authorization list (if applicable)
827
+ if (tx.supports(Capability.EIP7702EOACode)) {
828
+ gasRefund = await processAuthorizationList(vm, tx as EIP7702CompatibleTx, caller, gasRefund)
829
+ }
830
+
831
+ if (vm.DEBUG) {
832
+ debug(`Update fromAccount (caller) balance (-> ${fromAccount.balance}))`)
833
+ }
834
+ if (enableProfiler) {
835
+ // eslint-disable-next-line no-console
836
+ console.timeEnd(balanceNonceLabel)
837
+ executionTimerPrecise = performance.now()
838
+ }
839
+
840
+ // ===========================
841
+ // EXECUTION: Run EVM Call
842
+ // ===========================
843
+ const { value, data, to } = tx
844
+
845
+ if (vm.DEBUG) {
846
+ debug(
847
+ `Running tx=${
848
+ tx.isSigned() ? bytesToHex(tx.hash()) : 'unsigned'
849
+ } with caller=${caller} gasLimit=${gasLimit} to=${
850
+ to?.toString() ?? 'none'
851
+ } value=${value} data=${short(data)}`,
852
+ )
853
+ }
854
+
855
+ results = (await vm.evm.runCall({
856
+ block,
857
+ gasPrice,
858
+ caller,
859
+ gasLimit,
860
+ to,
861
+ value,
862
+ data,
863
+ blobVersionedHashes,
864
+ accessWitness: txAccesses,
865
+ })) as RunTxResult
866
+
867
+ if (vm.common.isActivatedEIP(7864)) {
868
+ ;(stateAccesses as BinaryTreeAccessWitness)?.merge(txAccesses! as BinaryTreeAccessWitness)
869
+ }
870
+ }
871
+
872
+ if (enableProfiler) {
873
+ // eslint-disable-next-line no-console
874
+ console.log(`${executionLabel}: ${performance.now() - executionTimerPrecise!}ms`)
875
+ // eslint-disable-next-line no-console
876
+ console.log('[ For execution details see table output ]')
877
+ // eslint-disable-next-line no-console
878
+ console.time(logsGasBalanceLabel)
879
+ }
880
+
881
+ if (vm.DEBUG) {
882
+ debug(`Update fromAccount (caller) nonce (-> ${fromAccount.nonce})`)
883
+ }
884
+
885
+ if (vm.DEBUG) {
886
+ const { executionGasUsed, exceptionError, returnValue } = results.execResult
887
+ debug('-'.repeat(100))
888
+ debug(
889
+ `Received tx execResult: [ executionGasUsed=${executionGasUsed} exceptionError=${
890
+ exceptionError !== undefined ? `'${exceptionError.error}'` : 'none'
891
+ } returnValue=${short(returnValue)} gasRefund=${results.gasRefund ?? 0} ]`,
892
+ )
893
+ }
894
+
895
+ // ===========================
896
+ // RESULTS: Gas and Balances
897
+ // ===========================
898
+ // Calculate tx gas used before refund processing
899
+ const totalGasSpentBeforeRefund = results.execResult.executionGasUsed + intrinsicGas
900
+ results.totalGasSpent = totalGasSpentBeforeRefund
901
+ if (vm.DEBUG) {
902
+ debugGas(`tx add baseFee ${intrinsicGas} to totalGasSpent (-> ${results.totalGasSpent})`)
903
+ }
904
+
905
+ // Add blob gas used to result
906
+ if (isBlob4844Tx(tx)) {
907
+ results.blobGasUsed = totalblobGas
908
+ }
909
+
910
+ // Process any gas refund
911
+ gasRefund += results.execResult.gasRefund ?? BIGINT_0
912
+ results.gasRefund = gasRefund // TODO: this field could now be incorrect with the introduction of 7623
913
+ const maxRefundQuotient = vm.common.param('maxRefundQuotient')
914
+ if (gasRefund !== BIGINT_0) {
915
+ const maxRefund = results.totalGasSpent / maxRefundQuotient
916
+ gasRefund = gasRefund < maxRefund ? gasRefund : maxRefund
917
+ results.totalGasSpent -= gasRefund
918
+ if (vm.DEBUG) {
919
+ debug(`Subtract tx gasRefund (${gasRefund}) from totalGasSpent (-> ${results.totalGasSpent})`)
920
+ }
921
+ } else {
922
+ if (vm.DEBUG) {
923
+ debug(`No tx gasRefund`)
924
+ }
925
+ }
926
+
927
+ if (vm.common.isActivatedEIP(7623)) {
928
+ if (results.totalGasSpent < floorCost) {
929
+ if (vm.DEBUG) {
930
+ debugGas(
931
+ `tx floorCost ${floorCost} is higher than to total execution gas spent (-> ${results.totalGasSpent}), setting floor as gas paid`,
932
+ )
933
+ }
934
+ results.gasRefund = BIGINT_0
935
+ results.totalGasSpent = floorCost
936
+ }
937
+ }
938
+
939
+ // EIP-7778: block-level gas accounting does not subtract tx refunds.
940
+ // For pre-7778 forks this equals the amount paid by the sender.
941
+ results.blockGasSpent = vm.common.isActivatedEIP(7778)
942
+ ? bigIntMax(totalGasSpentBeforeRefund, floorCost)
943
+ : results.totalGasSpent
944
+
945
+ results.amountSpent = results.totalGasSpent * gasPrice
946
+
947
+ // Update sender/payer balance with gas refund
948
+ if (isFrameEIP8141Tx(tx)) {
949
+ // EIP-8141: refund unused gas to the payer (set by APPROVE)
950
+ const frameTx = tx as unknown as FrameEIP8141Tx
951
+ const payer = frameTx.getSenderAddress() // For Example 1, payer == sender
952
+ let payerAccount = await state.getAccount(payer)
953
+ if (payerAccount === undefined) payerAccount = new Account()
954
+ const gasCostCharged = frameTx.gasLimit * gasPrice
955
+ const actualGasCost = results.totalGasSpent * gasPrice
956
+ const payerRefund = gasCostCharged - actualGasCost
957
+ if (payerRefund > BIGINT_0) {
958
+ payerAccount.balance += payerRefund
959
+ await vm.evm.journal.putAccount(payer, payerAccount)
960
+ }
961
+ if (vm.DEBUG) {
962
+ debug(
963
+ `EIP-8141: Refunded ${payerRefund} to payer ${payer} (charged=${gasCostCharged} actual=${actualGasCost})`,
964
+ )
965
+ }
966
+ } else {
967
+ fromAccount = await state.getAccount(caller)
968
+ if (fromAccount === undefined) {
969
+ fromAccount = new Account()
970
+ }
971
+ const actualTxCost = results.totalGasSpent * gasPrice
972
+ const txCostDiff = txCost - actualTxCost
973
+ const originalBalance = fromAccount.balance
974
+ fromAccount.balance += txCostDiff
975
+
976
+ if (vm.common.isActivatedEIP(7928)) {
977
+ vm.evm.blockLevelAccessList!.addBalanceChange(
978
+ caller.toString(),
979
+ fromAccount.balance,
980
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
981
+ originalBalance,
982
+ )
983
+ vm.evm.blockLevelAccessList!.addNonceChange(
984
+ caller.toString(),
985
+ fromAccount.nonce,
986
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
987
+ )
988
+ }
989
+
990
+ await vm.evm.journal.putAccount(caller, fromAccount)
991
+ if (vm.common.isActivatedEIP(7928) && txCostDiff > BIGINT_0) {
992
+ vm.evm.blockLevelAccessList!.addBalanceChange(
993
+ caller.toString(),
994
+ fromAccount.balance,
995
+ vm.evm.blockLevelAccessList!.blockAccessIndex,
996
+ )
997
+ }
998
+ if (vm.DEBUG) {
999
+ debug(
1000
+ `Refunded txCostDiff (${txCostDiff}) to fromAccount (caller) balance (-> ${fromAccount.balance})`,
1001
+ )
1002
+ }
1003
+ }
1004
+
1005
+ // Update miner's balance
1006
+ await updateMinerBalance(vm, state, block, results, inclusionFeePerGas!)
1007
+
1008
+ if (enableProfiler) {
1009
+ // eslint-disable-next-line no-console
1010
+ console.timeEnd(logsGasBalanceLabel)
1011
+ // eslint-disable-next-line no-console
1012
+ console.time(accountsCleanUpLabel)
1013
+ }
1014
+
1015
+ // ===========================
1016
+ // CLEANUP: Accounts and State
1017
+ // ===========================
1018
+ await processSelfdestructs(vm, results)
1019
+
1020
+ // Generate the bloom after selfdestruct finalization logs have been appended.
1021
+ results.bloom = txLogsBloom(results.execResult.logs, vm.common)
1022
+ if (vm.DEBUG) {
1023
+ debug(`Generated tx bloom with logs=${results.execResult.logs?.length}`)
1024
+ }
1025
+
1026
+ if (enableProfiler) {
1027
+ // eslint-disable-next-line no-console
1028
+ console.timeEnd(accountsCleanUpLabel)
1029
+ // eslint-disable-next-line no-console
1030
+ console.time(accessListLabel)
1031
+ }
1032
+
1033
+ // Build access list result if requested
1034
+ if (opts.reportAccessList === true && vm.common.isActivatedEIP(2930)) {
1035
+ results.accessList = buildAccessListResult(vm)
1036
+ }
1037
+
1038
+ if (enableProfiler) {
1039
+ // eslint-disable-next-line no-console
1040
+ console.timeEnd(accessListLabel)
1041
+ // eslint-disable-next-line no-console
1042
+ console.time(journalCacheCleanUpLabel)
1043
+ }
1044
+
1045
+ // Collect preimages if requested
1046
+ if (opts.reportPreimages === true && vm.evm.journal.preimages !== undefined) {
1047
+ results.preimages = vm.evm.journal.preimages
1048
+ }
1049
+
1050
+ // Clear journal and caches
1051
+ await vm.evm.journal.cleanup()
1052
+ state.originalStorageCache.clear()
1053
+
1054
+ if (enableProfiler) {
1055
+ // eslint-disable-next-line no-console
1056
+ console.timeEnd(journalCacheCleanUpLabel)
1057
+ // eslint-disable-next-line no-console
1058
+ console.time(receiptsLabel)
1059
+ }
1060
+
1061
+ // ===========================
1062
+ // FINALIZE: Receipt and Events
1063
+ // ===========================
1064
+ const gasUsed = opts.blockGasUsed ?? block?.header.gasUsed ?? DEFAULT_HEADER.gasUsed
1065
+ const cumulativeGasUsed = gasUsed + results.totalGasSpent
1066
+ results.receipt = await generateTxReceipt(
1067
+ vm,
1068
+ tx,
1069
+ results,
1070
+ cumulativeGasUsed,
1071
+ totalblobGas,
1072
+ blobGasPrice,
1073
+ )
1074
+
1075
+ if (enableProfiler) {
1076
+ // eslint-disable-next-line no-console
1077
+ console.timeEnd(receiptsLabel)
1078
+ }
1079
+
1080
+ // EIP-7928: Clean up net-zero balance changes
1081
+ // Per spec, if an account's balance changed during tx but final == pre-tx, don't record
1082
+ if (vm.common.isActivatedEIP(7928)) {
1083
+ vm.evm.blockLevelAccessList!.cleanupNetZeroBalanceChanges()
1084
+ }
1085
+
1086
+ /** The `afterTx` event - emits transaction results */
1087
+ const event: AfterTxEvent = { transaction: tx, ...results }
1088
+ await vm._emit('afterTx', event)
1089
+ if (vm.DEBUG) {
1090
+ debug(
1091
+ `tx run finished hash=${
1092
+ opts.tx.isSigned() ? bytesToHex(opts.tx.hash()) : 'unsigned'
1093
+ } sender=${caller}`,
1094
+ )
1095
+ }
1096
+
1097
+ return results
1098
+ }
1099
+
1100
+ /**
1101
+ * @method txLogsBloom
1102
+ * @private
1103
+ */
1104
+ function txLogsBloom(logs?: any[], common?: Common): Bloom {
1105
+ const bloom = new Bloom(undefined, common)
1106
+ if (logs) {
1107
+ for (let i = 0; i < logs.length; i++) {
1108
+ const log = logs[i]
1109
+ // add the address
1110
+ bloom.add(log[0])
1111
+ // add the topics
1112
+ const topics = log[1]
1113
+ for (let q = 0; q < topics.length; q++) {
1114
+ bloom.add(topics[q])
1115
+ }
1116
+ }
1117
+ }
1118
+ return bloom
1119
+ }
1120
+
1121
+ /**
1122
+ * Returns the tx receipt.
1123
+ * @param vm The vm instance
1124
+ * @param tx The transaction
1125
+ * @param txResult The tx result
1126
+ * @param cumulativeGasUsed The gas used in the block including vm tx
1127
+ * @param blobGasUsed The blob gas used in the tx
1128
+ * @param blobGasPrice The blob gas price for the block including vm tx
1129
+ */
1130
+ export async function generateTxReceipt(
1131
+ vm: VM,
1132
+ tx: TypedTransaction,
1133
+ txResult: RunTxResult,
1134
+ cumulativeGasUsed: bigint,
1135
+ blobGasUsed?: bigint,
1136
+ blobGasPrice?: bigint,
1137
+ ): Promise<TxReceipt> {
1138
+ const baseReceipt: BaseTxReceipt = {
1139
+ cumulativeBlockGasUsed: cumulativeGasUsed,
1140
+ bitvector: txResult.bloom.bitvector,
1141
+ logs: txResult.execResult.logs ?? [],
1142
+ }
1143
+
1144
+ let receipt
1145
+ if (vm.DEBUG) {
1146
+ debug(
1147
+ `Generate tx receipt transactionType=${
1148
+ tx.type
1149
+ } cumulativeBlockGasUsed=${cumulativeGasUsed} bitvector=${short(baseReceipt.bitvector)} (${
1150
+ baseReceipt.bitvector.length
1151
+ } bytes) logs=${baseReceipt.logs.length}`,
1152
+ )
1153
+ }
1154
+
1155
+ if (!tx.supports(Capability.EIP2718TypedTransaction)) {
1156
+ // Legacy transaction
1157
+ if (vm.common.gteHardfork(Hardfork.Byzantium)) {
1158
+ // Post-Byzantium
1159
+ receipt = {
1160
+ status: txResult.execResult.exceptionError !== undefined ? 0 : 1, // Receipts have a 0 as status on error
1161
+ ...baseReceipt,
1162
+ } as PostByzantiumTxReceipt
1163
+ } else {
1164
+ // Pre-Byzantium
1165
+ const stateRoot = await vm.stateManager.getStateRoot()
1166
+ receipt = {
1167
+ stateRoot,
1168
+ ...baseReceipt,
1169
+ } as PreByzantiumTxReceipt
1170
+ }
1171
+ } else {
1172
+ // Typed EIP-2718 Transaction
1173
+ if (isBlob4844Tx(tx)) {
1174
+ receipt = {
1175
+ blobGasUsed,
1176
+ blobGasPrice,
1177
+ status: txResult.execResult.exceptionError ? 0 : 1,
1178
+ ...baseReceipt,
1179
+ } as EIP4844BlobTxReceipt
1180
+ } else {
1181
+ receipt = {
1182
+ status: txResult.execResult.exceptionError ? 0 : 1,
1183
+ ...baseReceipt,
1184
+ } as PostByzantiumTxReceipt
1185
+ }
1186
+ }
1187
+ return receipt
1188
+ }
1189
+
1190
+ /**
1191
+ * Internal helper function to create an annotated error message
1192
+ *
1193
+ * @param msg Base error message
1194
+ * @hidden
1195
+ */
1196
+ function _errorMsg(msg: string, vm: VM, block: Block | undefined, tx: TypedTransaction) {
1197
+ const blockOrHeader = block ?? DEFAULT_HEADER
1198
+ const blockErrorStr = 'errorStr' in blockOrHeader ? blockOrHeader.errorStr() : 'block'
1199
+ const txErrorStr = 'errorStr' in tx ? tx.errorStr() : 'tx'
1200
+
1201
+ const errorMsg = `${msg} (${vm.errorStr()} -> ${blockErrorStr} -> ${txErrorStr})`
1202
+ return errorMsg
1203
+ }