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