mega-evm 1.7.0

The evm tailored for the MegaETH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
use alloy_primitives::{Address, U256};
use revm::{
    context::{transaction::AuthorizationTr, Transaction},
    handler::{EthFrame, FrameResult},
    interpreter::{
        interpreter::EthInterpreter, interpreter_action::FrameInit, FrameInput, InterpreterAction,
        SStoreResult,
    },
};

use super::frame_limit::CallFrameInfo;
use crate::{FrameLimitTracker, MegaSpecId, TxRuntimeLimit};

/// The number of bytes for the base transaction data.
pub const BASE_TX_SIZE: u64 = 110;
/// The number of bytes for the each EIP-7702 authorization.
pub const AUTHORIZATION_SIZE: u64 = 101;
/// The number of bytes for the each log topic.
pub const LOG_TOPIC_SIZE: u64 = 32;
/// The number of bytes for the salt key.
pub const SALT_KEY_SIZE: u64 = 8;
/// The number of bytes for the salt value delta of the account info. We assume the XOR delta
/// of address, nonce, and code hash is very small, so we can ignore them. The only significant
/// delta is the balance. We over-estimate it to 32 bytes.
pub const SALT_VALUE_DELTA_ACCOUNT_INFO_SIZE: u64 = 32;
/// The number of bytes for the salt value XOR delta of the storage slot. We over-estimate it to
/// 32 bytes.
pub const SALT_VALUE_DELTA_STORAGE_SLOT_SIZE: u64 = 32;
/// The originated data size for reading an account info.
pub const ACCOUNT_INFO_WRITE_SIZE: u64 = SALT_KEY_SIZE + SALT_VALUE_DELTA_ACCOUNT_INFO_SIZE;
/// The originated data size for writing a storage slot.
pub const STORAGE_SLOT_WRITE_SIZE: u64 = SALT_KEY_SIZE + SALT_VALUE_DELTA_STORAGE_SLOT_SIZE;
/// REX6+ per-log base overhead: one value unit (`LOG_TOPIC_SIZE`) for the address every receipt log
/// carries, so an empty `LOG0` is not free in the `data_size` lane.
pub const LOG_BASE_SIZE: u64 = 32;

/// A tracker for the total data size (in bytes) generated from transaction execution.
///
/// Uses `FrameLimitTracker` for frame-aware tracking with per-frame budgets (Rex4+).
///
/// In Rex4+, data size is enforced at the per-frame level: each inner call frame receives
/// `remaining * 98 / 100` of the parent's remaining data size budget.
/// When a frame exceeds its budget, it reverts (not halts) and its discardable data is dropped,
/// protecting the parent's budget.
/// In pre-Rex4, data size is enforced at the TX level only.
///
/// ## Tracked Data Types
///
/// **Non-discardable (permanent):**
/// - Transaction base data: 110 bytes
/// - Calldata: actual byte length
/// - Access lists: sum of access entry sizes
/// - EIP-7702 authorizations: 101 bytes per authorization
/// - Transaction caller account update: 40 bytes
/// - EIP-7702 authority account updates: 40 bytes each
///
/// **Discardable (reverted on frame revert):**
/// - Log data: 32 bytes per topic + data length
/// - Storage writes: 40 bytes (only when original ≠ new value, refunded when reset to original)
/// - Account updates from calls/creates: 40 bytes each
/// - Contract code: actual deployed bytecode size
#[derive(Debug, Clone)]
pub(crate) struct DataSizeTracker {
    rex4_enabled: bool,
    rex5_enabled: bool,
    rex6_enabled: bool,
    frame_tracker: FrameLimitTracker<CallFrameInfo>,
}

impl DataSizeTracker {
    pub(crate) fn new(spec: MegaSpecId, tx_limit: u64) -> Self {
        Self {
            rex4_enabled: spec.is_enabled(MegaSpecId::REX4),
            rex5_enabled: spec.is_enabled(MegaSpecId::REX5),
            rex6_enabled: spec.is_enabled(MegaSpecId::REX6),
            frame_tracker: FrameLimitTracker::new(spec, tx_limit),
        }
    }

    /// Returns whether there is at least one active frame on the stack.
    pub(crate) fn has_active_frame(&self) -> bool {
        self.frame_tracker.has_active_frame()
    }

    /// Records discardable data in the current frame.
    fn record_discardable(&mut self, size: u64) {
        self.frame_tracker.add_frame_discardable(size);
    }

    /// Records discardable usage into the PARENT frame (one below the top). Used for a
    /// child-CREATE's creator-side account-info write, whose on-chain effect (the creator
    /// nonce bump) is undone only by the parent's revert, not the child's.
    fn record_parent_discardable(&mut self, size: u64) {
        self.frame_tracker.add_parent_discardable(size);
    }

    /// Records a refund (negative data) in the current frame.
    fn record_refund(&mut self, size: u64) {
        self.frame_tracker.add_frame_refund(size);
    }

    /// REX5+: meter an oracle-hint payload against the TX intrinsic data-size lane.
    ///
    /// Hints are a TX-scoped side-channel into the off-chain oracle backend. They do not
    /// belong to any call frame (the inner Oracle frame may revert, but the hint has already
    /// flowed out), so we record into `tx_entry.persistent_usage` — the same lane as calldata
    /// — rather than the current frame's discardable usage.
    pub(crate) fn record_oracle_hint_bytes(&mut self, len: u64) {
        self.frame_tracker.add_tx_persistent(len);
    }

    /// Records an account info write (40 bytes) as discardable data in the current frame.
    ///
    /// Used by SELFDESTRUCT beneficiary metering (REX5+) to charge data size for
    /// creating a new beneficiary account.
    pub(crate) fn record_account_write(&mut self) {
        self.record_discardable(ACCOUNT_INFO_WRITE_SIZE);
    }

    /// Records an account info write (40 bytes) as TX-level persistent (non-discardable) data.
    ///
    /// Used by the REX6 EIP-7702 authorization scan, which runs in `validate` before any frame
    /// exists, so the charge cannot go through the frame-scoped `record_account_write`.
    pub(crate) fn record_persistent_account_write(&mut self) {
        self.frame_tracker.add_tx_persistent(ACCOUNT_INFO_WRITE_SIZE);
    }

    /// Merges external persistent usage into the TX-level entry.
    ///
    /// Used by `KeylessDeploy` (REX5+) to propagate sandbox data size consumption
    /// back to the parent transaction.
    pub(crate) fn merge_persistent_usage(&mut self, amount: u64) {
        self.frame_tracker.add_tx_persistent(amount);
    }

    /// Returns the remaining data size budget for the current call frame, capped by
    /// the TX-level remaining.
    pub(crate) fn current_call_remaining(&self) -> u64 {
        let tx_remaining =
            self.frame_tracker.tx_limit().saturating_sub(self.frame_tracker.net_usage());
        if self.rex4_enabled {
            self.frame_tracker.current_frame_remaining().min(tx_remaining)
        } else {
            tx_remaining
        }
    }
}

impl TxRuntimeLimit for DataSizeTracker {
    /// Returns the current effective data size limit for the entire transaction.
    #[inline]
    fn tx_limit(&self) -> u64 {
        self.frame_tracker.tx_limit()
    }

    /// Returns the current total data size across all frames, clamped to zero.
    #[inline]
    fn tx_usage(&self) -> u64 {
        self.frame_tracker.net_usage()
    }

    fn reset(&mut self) {
        self.frame_tracker.reset();
    }

    /// Returns whether the data size limit has been exceeded.
    ///
    /// In Rex4+, checks the per-frame budget first, then falls through to a TX-level check.
    /// The TX-level fallthrough catches intrinsic overflow when no frame exists yet
    /// (intrinsic usage is recorded in `tx_entry` before the first frame is pushed).
    /// In pre-Rex4, checks total data size across all frames against the TX limit.
    fn check_limit(&self) -> super::LimitCheck {
        if self.rex4_enabled {
            let frame_check =
                self.frame_tracker.exceeds_current_frame_limit(super::LimitKind::DataSize);
            if frame_check.exceeded_limit() {
                return frame_check;
            }
            // TX-level fallthrough: defense-in-depth safety net.
            // In Rex4+ during execution, per-frame budgets are derived from remaining TX
            // budget, so this should only exceed when no frame exists (intrinsic overflow).
        }
        let used = self.tx_usage();
        let limit = self.frame_tracker.tx_limit();
        if used > limit {
            // Defense-in-depth: pre-REX5, the only mid-execution writer to `tx_entry` is
            // `before_tx_start` (which runs before any frame is pushed), so a TX-level
            // exceed with an active frame indicates a budget-accounting bug. REX5+ adds
            // `record_oracle_hint_bytes` which legitimately writes to `tx_entry` mid-
            // execution to meter oracle-hint payloads as TX-scoped side-channel cost, so
            // the invariant is only asserted on pre-REX5 specs.
            debug_assert!(
                !self.rex4_enabled || self.rex5_enabled || !self.frame_tracker.has_active_frame(),
                "DataSize TX-level exceeded with active frame — budget invariant violated"
            );
            super::LimitCheck::ExceedsLimit {
                kind: super::LimitKind::DataSize,
                limit,
                used,
                frame_local: false,
            }
        } else {
            super::LimitCheck::WithinLimit
        }
    }

    /// Records the data size of a transaction at the start of execution.
    ///
    /// This includes:
    /// - 110 bytes base transaction data
    /// - Calldata byte length
    /// - Access list sizes
    /// - EIP-7702 authorizations (101 bytes each) + authority account updates (40 bytes each)
    /// - Caller account update (40 bytes)
    ///
    /// All recorded as pre-frame (non-discardable) since no frame exists yet.
    fn before_tx_start(&mut self, tx: &crate::MegaTransaction) {
        // TX intrinsic data (non-discardable, recorded before any frame is pushed)
        let mut size = BASE_TX_SIZE;
        size += tx.input().len() as u64;
        size += tx
            .access_list()
            .map(|item| item.map(|access| access.size() as u64).sum::<u64>())
            .unwrap_or_default();
        size += tx.authorization_list_len() as u64 * AUTHORIZATION_SIZE;
        self.frame_tracker.add_tx_persistent(size);

        // EIP-7702 authority account updates (non-discardable).
        //
        // Pre-REX6: charged here for every authorization with a recoverable authority, even ones
        // that fail the chain-id / nonce / code application gates and never write the account.
        // REX6+ moves this charge into the journal-aware authorization scan in `validate` so only
        // *applied* authorities are charged; skip it here.
        if !self.rex6_enabled {
            for authorization in tx.authorization_list() {
                if authorization.authority().is_some() {
                    self.frame_tracker.add_tx_persistent(ACCOUNT_INFO_WRITE_SIZE);
                }
            }
        }

        // Caller account update (non-discardable)
        self.frame_tracker.add_tx_persistent(ACCOUNT_INFO_WRITE_SIZE);
    }

    /// Called when inspector intercepts and skips a call/create.
    ///
    /// Pushes an empty frame so `before_frame_return_result` can pop it to keep
    /// the frame stack aligned with the EVM's call stack.
    #[inline]
    fn push_empty_frame(&mut self) {
        self.frame_tracker.push_dummy_frame();
    }

    /// Hook called before a new execution frame is initialized.
    ///
    /// Pushes a new frame and records account info updates:
    /// - **Call with value transfer**: Updates parent's account info if not yet marked, then
    ///   records target account info update (40 bytes each).
    /// - **Create**: Updates parent's account info if not yet marked (caller nonce increment).
    ///   Created address is set later in `after_frame_init_on_frame`.
    /// - **Call without transfer**: No account info updates.
    fn before_frame_init<JOURNAL: crate::JournalInspectTr<DBError: core::fmt::Debug>>(
        &mut self,
        frame_init: &FrameInit,
        _journal: &mut JOURNAL,
    ) -> Result<(), JOURNAL::DBError> {
        match &frame_init.frame_input {
            FrameInput::Call(call_inputs) => {
                let has_transfer = call_inputs.transfers_value();
                let parent_needs_update =
                    self.frame_tracker.push_call_frame(call_inputs.target_address, has_transfer);
                if has_transfer {
                    if parent_needs_update {
                        // Parent's account info update goes to child's discardable.
                        self.record_discardable(ACCOUNT_INFO_WRITE_SIZE);
                    }
                    // A value transfer to the caller itself touches a single account, already
                    // accounted by the caller-side write above (or, at the top level, by the
                    // `before_tx_start` caller record). Recording the target side again would
                    // double-count that one account, so skip it under REX6.
                    if !(self.rex6_enabled && call_inputs.target_address == call_inputs.caller) {
                        // Record target account info update in child's discardable.
                        self.record_discardable(ACCOUNT_INFO_WRITE_SIZE);
                    }
                }
            }
            FrameInput::Create(_) => {
                let parent_needs_update = self.frame_tracker.push_create_frame();
                if parent_needs_update {
                    if self.rex6_enabled {
                        // The creator's nonce bump survives the child's revert (revm bumps it
                        // before the create checkpoint), so charge it to the parent frame —
                        // see `FrameLimitTracker::add_parent_discardable`.
                        self.record_parent_discardable(ACCOUNT_INFO_WRITE_SIZE);
                    } else {
                        // Pre-REX6: the creator nonce-bump charge is bundled into the child frame's
                        // discardable lane (frozen behavior).
                        self.record_discardable(ACCOUNT_INFO_WRITE_SIZE);
                    }
                }
            }
            FrameInput::Empty => unreachable!(),
        }
        Ok(())
    }

    /// Hook called when a new execution frame is successfully initialized.
    ///
    /// For CREATE frames, records the created address and its account info update (40 bytes).
    fn after_frame_init_on_frame(&mut self, frame: &EthFrame<EthInterpreter>) {
        if frame.data.is_create() {
            let created_address =
                frame.data.created_address().expect("created address is none for create frame");
            self.frame_tracker.set_created_address(created_address);
            // Record account info update for created address
            self.record_discardable(ACCOUNT_INFO_WRITE_SIZE);
        }
    }

    /// Hook called after a frame finishes running.
    ///
    /// For CREATE frames, records the deployed contract bytecode size as discardable data.
    fn after_frame_run<'a>(
        &mut self,
        frame: &'a EthFrame<EthInterpreter>,
        action: &'a mut InterpreterAction,
    ) {
        if let InterpreterAction::Return(interpreter_result) = action {
            if frame.data.is_create() {
                let code_size = interpreter_result.output.len() as u64;
                self.record_discardable(code_size);
            }
        }
    }

    /// Hook called when a frame returns its result to the parent frame.
    ///
    /// Pops the current frame from the tracker:
    /// - **On success**: merges the frame's data into the parent frame.
    /// - **On revert/failure**: discards the frame's discardable data.
    ///
    /// Rex5+: if the reverting child had set the parent's account-update flag, the flag
    /// is reset so the next successful call from the same parent still charges the parent
    /// account (avoiding undercounting after a revert-then-retry pattern). The unwind is
    /// owned by `FrameLimitTracker::pop_frame_unwind_parent`.
    fn before_frame_return_result<const LAST_FRAME: bool>(&mut self, result: &FrameResult) {
        assert!(LAST_FRAME || self.frame_tracker.has_active_frame(), "frame stack is empty");
        let is_success = result.instruction_result().is_ok();
        self.frame_tracker.pop_frame_unwind_parent(is_success);
    }

    /// Hook called when a storage slot is written via `SSTORE`.
    ///
    /// Records SSTORE data based on the storage slot's state transition:
    ///
    /// | Original == Present | Original == New | Effect                | Reason                  |
    /// |---------------------|-----------------|------------------------|-------------------------|
    /// | yes                 | yes             | —                      | No change               |
    /// | yes                 | no              | +40 bytes (discardable)| First write to slot     |
    /// | no                  | yes             | -40 bytes (refund)     | Reset to original value |
    /// | no                  | no              | —                      | Rewrite, no new data    |
    fn after_sstore(&mut self, _target_address: Address, _slot: U256, store_result: &SStoreResult) {
        if store_result.is_original_eq_present() {
            if !store_result.is_original_eq_new() {
                // First write to slot: original == present, but new differs
                self.record_discardable(STORAGE_SLOT_WRITE_SIZE);
            }
        } else if store_result.is_original_eq_new() {
            // Reset to original: refund
            self.record_refund(STORAGE_SLOT_WRITE_SIZE);
        }
    }

    /// Hook called when a log is emitted.
    ///
    /// Records `LOG_BASE_SIZE` (REX6+ only) + `num_topics * 32` + `data_size` as discardable.
    fn after_log(&mut self, num_topics: u64, data_size: u64) {
        let base = if self.rex6_enabled { LOG_BASE_SIZE } else { 0 };
        let size = base + num_topics * LOG_TOPIC_SIZE + data_size;
        self.record_discardable(size);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The originated data-size constants are *sums* of their salt key/value components,
    /// not products. Pin the values against independent literals so an arithmetic slip
    /// (e.g. `+` -> `*`) is caught (8 + 32 = 40, not 8 * 32 = 256).
    #[test]
    fn test_originated_data_size_constants() {
        assert_eq!(STORAGE_SLOT_WRITE_SIZE, 40);
        assert_eq!(ACCOUNT_INFO_WRITE_SIZE, 40);
    }

    /// `has_active_frame` must reflect the underlying frame stack, not a constant.
    #[test]
    fn test_has_active_frame_reflects_frame_stack() {
        let mut tracker = DataSizeTracker::new(MegaSpecId::MINI_REX, u64::MAX);
        assert!(!tracker.has_active_frame(), "no frame pushed yet");
        tracker.push_empty_frame();
        assert!(tracker.has_active_frame(), "a frame is on the stack");
    }
}