diff --git a/src/pages/ethereum/execution/gas-profiler/SimulatePage.tsx b/src/pages/ethereum/execution/gas-profiler/SimulatePage.tsx index b636530b1..45af657b2 100644 --- a/src/pages/ethereum/execution/gas-profiler/SimulatePage.tsx +++ b/src/pages/ethereum/execution/gas-profiler/SimulatePage.tsx @@ -35,8 +35,10 @@ import { useNetwork } from '@/hooks/useNetwork'; import { useThemeColors } from '@/hooks/useThemeColors'; import { GasScheduleDrawer } from './components/GasScheduleDrawer'; import { BlockSimulationResultsV2 } from './components/BlockSimulationResultsV2'; +import { GlamsterdamPresetModal } from './components/GlamsterdamPresetModal'; import { useGasSchedule } from './hooks/useGasSchedule'; import type { GasSchedule, BlockSimulationResult, CallError } from './SimulatePage.types'; +import { GLAMSTERDAM_PRESET } from './SimulatePage.types'; // Register ECharts components echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer]); @@ -167,6 +169,7 @@ interface ApiBlockSimulationResponse { simulatedReverts: number; originalErrors: CallError[] | null; simulatedErrors: CallError[] | null; + error?: string; }>; opcodeBreakdown: Record< string, @@ -195,6 +198,7 @@ function transformApiResponse(response: ApiBlockSimulationResponse): BlockSimula simulatedReverts: tx.simulatedReverts, originalErrors: tx.originalErrors ?? [], simulatedErrors: tx.simulatedErrors ?? [], + error: tx.error, })), opcodeBreakdown: response.opcodeBreakdown, }; @@ -218,6 +222,9 @@ export function SimulatePage(): JSX.Element { // Drawer state const [drawerOpen, setDrawerOpen] = useState(false); + // Preset modal state + const [presetModalOpen, setPresetModalOpen] = useState(false); + // Selection state const [selectedBlockIndex, setSelectedBlockIndex] = useState(null); @@ -297,6 +304,12 @@ export function SimulatePage(): JSX.Element { }).length; }, [gasSchedule, gasScheduleDefaults]); + // Check if Glamsterdam preset is currently applied + const isGlamsterdamApplied = useMemo( + () => Object.entries(GLAMSTERDAM_PRESET).every(([key, value]) => gasSchedule[key] === value), + [gasSchedule] + ); + // Aggregate stats from completed results const aggregateStats = useMemo(() => calculateAggregateStats(simState.results), [simState.results]); @@ -601,6 +614,30 @@ export function SimulatePage(): JSX.Element { const modifiedParamNames = useMemo(() => modifiedEntries.map(([key]) => key), [modifiedEntries]); + // Pending simulate flag - triggers simulation on next render after preset is applied + const [pendingSimulate, setPendingSimulate] = useState(false); + + // Auto-simulate after preset application + useEffect(() => { + if (pendingSimulate) { + setPendingSimulate(false); + handleSimulate(); + } + }, [pendingSimulate, handleSimulate]); + + // Handle preset modal "Apply & Simulate" + const handlePresetApplyAndSimulate = useCallback((presetValues: Record) => { + setGasSchedule(presetValues as GasSchedule); + setGasWarning(false); + setPendingSimulate(true); + }, []); + + // Handle preset modal "Cancel" - resets gas schedule to empty + const handlePresetCancel = useCallback(() => { + setGasSchedule({}); + setGasWarning(false); + }, []); + const isRunning = simState.status === 'running'; const hasResults = simState.results.length > 0; @@ -680,24 +717,42 @@ export function SimulatePage(): JSX.Element { {/* Gas Schedule */}
- +
+ + +
{/* Actions */} @@ -1169,6 +1224,15 @@ export function SimulatePage(): JSX.Element { onChange={handleScheduleChange} /> )} + + {/* Glamsterdam Preset Modal */} + setPresetModalOpen(false)} + onApplyAndSimulate={handlePresetApplyAndSimulate} + onCancel={handlePresetCancel} + defaults={gasScheduleDefaults ?? null} + /> ); } diff --git a/src/pages/ethereum/execution/gas-profiler/SimulatePage.types.ts b/src/pages/ethereum/execution/gas-profiler/SimulatePage.types.ts index 891bfe77c..5cedabc38 100644 --- a/src/pages/ethereum/execution/gas-profiler/SimulatePage.types.ts +++ b/src/pages/ethereum/execution/gas-profiler/SimulatePage.types.ts @@ -293,6 +293,7 @@ export const GAS_PARAMETER_GROUPS: GasParameterGroup[] = [ { key: 'PC_MODEXP_MIN_GAS', label: 'MODEXP Min Gas', min: 0, max: 2000, step: 10 }, { key: 'PC_BN254_PAIRING_BASE', label: 'BN254 Pairing Base', min: 0, max: 200000, step: 5000 }, { key: 'PC_BN254_PAIRING_PER_PAIR', label: 'BN254 Pairing /Pair', min: 0, max: 200000, step: 1000 }, + { key: 'PC_BLAKE2F_BASE', label: 'BLAKE2F Base', min: 0, max: 1000, step: 10 }, { key: 'PC_BLAKE2F_PER_ROUND', label: 'BLAKE2F /Round', min: 0, max: 20, step: 1 }, { key: 'PC_BLS12_PAIRING_CHECK_BASE', label: 'BLS12 Pairing Base', min: 0, max: 200000, step: 5000 }, { key: 'PC_BLS12_PAIRING_CHECK_PER_PAIR', label: 'BLS12 Pairing /Pair', min: 0, max: 200000, step: 1000 }, @@ -317,6 +318,45 @@ export const GAS_PARAMETER_GROUPS: GasParameterGroup[] = [ }, ]; +/** + * Glamsterdam (EIP-8007) gas schedule preset. + * Applies all gas parameter changes proposed for the Glamsterdam fork. + * + * EIP-7904: Compute repricing (opcodes + precompiles) + * EIP-7976: Calldata floor cost increase + * EIP-2780: Transaction base cost reduction + * EIP-8038: State access repricing (TBD - using current values as placeholders) + */ +export const GLAMSTERDAM_PRESET: Record = { + // EIP-7904: Compute repricing + DIV: 15, + SDIV: 20, + MOD: 12, + MULMOD: 11, + KECCAK256: 45, + PC_BLAKE2F_BASE: 170, + PC_BLAKE2F_PER_ROUND: 2, + PC_BLS12_G1ADD: 643, + PC_BLS12_G2ADD: 765, + PC_BN254_ADD: 314, + PC_BN254_PAIRING_PER_PAIR: 34103, + PC_KZG_POINT_EVALUATION: 89363, + + // EIP-7976: Calldata floor + TX_FLOOR_PER_TOKEN: 15, + + // EIP-2780: TX base cost + TX_BASE: 4500, + + // EIP-8038: State access repricing (TBD - current values) + SLOAD_COLD: 2100, + SLOAD_WARM: 100, + SSTORE_RESET: 2900, + CALL_COLD: 2600, + TX_ACCESS_LIST_ADDR: 2400, + TX_ACCESS_LIST_KEY: 1900, +}; + /** * Summary of gas usage (original or simulated) */ @@ -362,6 +402,8 @@ export interface TxSummary { originalErrors: CallError[]; /** Errors from nested calls in simulated execution */ simulatedErrors: CallError[]; + /** Pre-execution error (e.g. "intrinsic gas too low") when execution fails before the EVM runs */ + error?: string; } /** diff --git a/src/pages/ethereum/execution/gas-profiler/components/BlockSimulationResultsV2/BlockSimulationResultsV2.tsx b/src/pages/ethereum/execution/gas-profiler/components/BlockSimulationResultsV2/BlockSimulationResultsV2.tsx index 6ad89dc60..f316fd2a9 100644 --- a/src/pages/ethereum/execution/gas-profiler/components/BlockSimulationResultsV2/BlockSimulationResultsV2.tsx +++ b/src/pages/ethereum/execution/gas-profiler/components/BlockSimulationResultsV2/BlockSimulationResultsV2.tsx @@ -367,7 +367,7 @@ function ExecutionHealthSection({ subtitle: totalTransactions > 0 && divergenceSummary.divergedCount > 0 ? `${((divergenceSummary.divergedCount / totalTransactions) * 100).toFixed(1)}% of txs` - : undefined, + : 'all paths unchanged', accentColor: `${divergedColor}33`, }, { @@ -622,8 +622,9 @@ function TransactionImpactView({ all: transactions.length, diverged: transactions.filter(t => t.diverged).length, status: transactions.filter(t => t.originalStatus !== t.simulatedStatus).length, - errors: transactions.filter(t => (t.originalErrors?.length ?? 0) > 0 || (t.simulatedErrors?.length ?? 0) > 0) - .length, + errors: transactions.filter( + t => (t.originalErrors?.length ?? 0) > 0 || (t.simulatedErrors?.length ?? 0) > 0 || !!t.error + ).length, }), [transactions] ); @@ -639,7 +640,7 @@ function TransactionImpactView({ txs = txs.filter(t => t.originalStatus !== t.simulatedStatus); break; case 'errors': - txs = txs.filter(t => (t.originalErrors?.length ?? 0) > 0 || (t.simulatedErrors?.length ?? 0) > 0); + txs = txs.filter(t => (t.originalErrors?.length ?? 0) > 0 || (t.simulatedErrors?.length ?? 0) > 0 || !!t.error); break; } @@ -746,8 +747,9 @@ function TransactionImpactView({ {filteredAndSorted.slice(0, visibleCount).map(tx => { const statusChanged = tx.originalStatus !== tx.simulatedStatus; const hasErrors = (tx.originalErrors?.length ?? 0) > 0 || (tx.simulatedErrors?.length ?? 0) > 0; + const hasPreExecError = !!tx.error; const isExpanded = expandedTxs.has(tx.hash); - const isNotable = statusChanged || tx.diverged || hasErrors; + const isNotable = statusChanged || tx.diverged || hasErrors || hasPreExecError; const absDelta = Math.abs(tx.simulatedGas - tx.originalGas); return ( @@ -869,6 +871,11 @@ function TransactionImpactView({ + {/* Pre-execution error (e.g. intrinsic gas too low) */} + {hasPreExecError && ( + + )} + {/* Error cards (if any) */} {hasErrors && (
diff --git a/src/pages/ethereum/execution/gas-profiler/components/GlamsterdamPresetModal/GlamsterdamPresetModal.tsx b/src/pages/ethereum/execution/gas-profiler/components/GlamsterdamPresetModal/GlamsterdamPresetModal.tsx new file mode 100644 index 000000000..be4cbdd6a --- /dev/null +++ b/src/pages/ethereum/execution/gas-profiler/components/GlamsterdamPresetModal/GlamsterdamPresetModal.tsx @@ -0,0 +1,388 @@ +import { type JSX, useState, useCallback, useMemo } from 'react'; +import { ArrowTopRightOnSquareIcon, ExclamationTriangleIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; +import clsx from 'clsx'; +import { Dialog } from '@/components/Overlays/Dialog'; +import { Button } from '@/components/Elements/Button'; +import type { GasScheduleDefaults } from '../../SimulatePage.types'; +import { GLAMSTERDAM_PRESET } from '../../SimulatePage.types'; + +/** + * EIP section definition for the preset modal + */ +interface EipSection { + eip: string; + name: string; + description: string; + url: string; + parameters: { key: string; label: string }[]; + /** If true, values are placeholders (TBD) and shown with a muted indicator */ + placeholder?: boolean; + /** Additional note shown below parameters (e.g. to explain unsimulated parts of the EIP) */ + note?: string; +} + +/** + * EIP sections that cannot be simulated (structural changes) + */ +interface UnsupportedEip { + eip: string; + name: string; + reason: string; + url: string; +} + +/** EIP sections with overridable parameters */ +const EIP_SECTIONS: EipSection[] = [ + { + eip: 'EIP-7904', + name: 'Compute Repricing', + description: 'Adjusts gas costs for compute-heavy opcodes and precompiles to better reflect actual resource usage.', + url: 'https://eips.ethereum.org/EIPS/eip-7904', + parameters: [ + { key: 'DIV', label: 'DIV' }, + { key: 'SDIV', label: 'SDIV' }, + { key: 'MOD', label: 'MOD' }, + { key: 'MULMOD', label: 'MULMOD' }, + { key: 'KECCAK256', label: 'KECCAK256' }, + { key: 'PC_BLAKE2F_BASE', label: 'BLAKE2F Base' }, + { key: 'PC_BLAKE2F_PER_ROUND', label: 'BLAKE2F /Round' }, + { key: 'PC_BLS12_G1ADD', label: 'BLS12 G1Add' }, + { key: 'PC_BLS12_G2ADD', label: 'BLS12 G2Add' }, + { key: 'PC_BN254_ADD', label: 'BN254 Add' }, + { key: 'PC_BN254_PAIRING_PER_PAIR', label: 'BN254 Pairing /Pair' }, + { key: 'PC_KZG_POINT_EVALUATION', label: 'KZG Point Eval' }, + ], + }, + { + eip: 'EIP-7976', + name: 'Calldata Floor Cost', + description: 'Increases the calldata floor cost per token, discouraging using calldata for data availability.', + url: 'https://eips.ethereum.org/EIPS/eip-7976', + parameters: [{ key: 'TX_FLOOR_PER_TOKEN', label: 'Floor Per Token' }], + }, + { + eip: 'EIP-2780', + name: 'Transaction Repricing', + description: + 'Reprices transaction costs: reduces the base cost from 21,000 to 4,500, and restructures value transfer and cold account access pricing.', + url: 'https://eips.ethereum.org/EIPS/eip-2780', + parameters: [{ key: 'TX_BASE', label: 'TX Base' }], + note: 'This EIP also introduces structural changes (new account surcharge, cold account cost splitting by code presence, value transfer repricing) that cannot be simulated as parameter overrides.', + }, + { + eip: 'EIP-8038', + name: 'State Access Repricing', + description: + 'Reprices state access opcodes (SLOAD, SSTORE, CALL cold/warm). Values are still under discussion and may change.', + url: 'https://eips.ethereum.org/EIPS/eip-8038', + placeholder: true, + parameters: [ + { key: 'SLOAD_COLD', label: 'SLOAD Cold' }, + { key: 'SLOAD_WARM', label: 'SLOAD Warm' }, + { key: 'SSTORE_RESET', label: 'SSTORE Reset' }, + { key: 'CALL_COLD', label: 'CALL Cold' }, + { key: 'TX_ACCESS_LIST_ADDR', label: 'Access List Addr' }, + { key: 'TX_ACCESS_LIST_KEY', label: 'Access List Key' }, + ], + note: 'SSTORE clear refund and EXTCODESIZE/EXTCODECOPY formula changes in this EIP cannot be simulated as parameter overrides.', + }, +]; + +/** EIPs that involve structural changes and cannot be simulated via parameter overrides */ +const UNSUPPORTED_EIPS: UnsupportedEip[] = [ + { + eip: 'EIP-8037', + name: 'State Growth Costs', + reason: + 'Introduces new gas costs for account/storage creation that require protocol-level changes, not just parameter adjustments.', + url: 'https://eips.ethereum.org/EIPS/eip-8037', + }, + { + eip: 'EIP-7981', + name: 'Access List Data Gas', + reason: + 'Adds a new intrinsic gas mechanism for access list data that cannot be expressed as a simple price override.', + url: 'https://eips.ethereum.org/EIPS/eip-7981', + }, +]; + +/** + * Derive slider constraints from the default and preset values + */ +function getSliderRange(defaultValue: number, presetValue: number): { min: number; max: number; step: number } { + const maxVal = Math.max(defaultValue, presetValue); + if (maxVal === 0) return { min: 0, max: 100, step: 1 }; + if (maxVal <= 10) return { min: 0, max: Math.max(50, maxVal * 5), step: 1 }; + if (maxVal <= 100) return { min: 0, max: Math.max(500, maxVal * 5), step: 1 }; + if (maxVal <= 1000) return { min: 0, max: Math.max(5000, maxVal * 5), step: 10 }; + if (maxVal <= 10000) return { min: 0, max: Math.max(50000, maxVal * 5), step: 100 }; + return { min: 0, max: maxVal * 5, step: 1000 }; +} + +export interface GlamsterdamPresetModalProps { + open: boolean; + onClose: () => void; + onApplyAndSimulate: (values: Record) => void; + onCancel: () => void; + defaults: GasScheduleDefaults | null; +} + +/** + * Modal that explains the Glamsterdam (EIP-8007) gas schedule preset, + * shows all parameter changes with adjustable sliders, and allows + * the user to apply the preset and start simulation. + */ +export function GlamsterdamPresetModal({ + open, + onClose, + onApplyAndSimulate, + onCancel, + defaults, +}: GlamsterdamPresetModalProps): JSX.Element { + // Local copy of preset values that the user can tweak before applying + const [values, setValues] = useState>({ ...GLAMSTERDAM_PRESET }); + + // Reset local values when modal opens + const handleOpen = useCallback(() => { + setValues({ ...GLAMSTERDAM_PRESET }); + }, []); + + // Reset local values when modal becomes visible + // (useEffect-like behavior via checking open state) + useMemo(() => { + if (open) handleOpen(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + + const handleParamChange = useCallback((key: string, value: number) => { + setValues(prev => ({ ...prev, [key]: value })); + }, []); + + const handleResetParam = useCallback((key: string) => { + setValues(prev => ({ ...prev, [key]: GLAMSTERDAM_PRESET[key] })); + }, []); + + // Count how many values differ from GLAMSTERDAM_PRESET defaults + const tweakedCount = useMemo( + () => Object.entries(values).filter(([key, val]) => val !== GLAMSTERDAM_PRESET[key]).length, + [values] + ); + + return ( + + + {tweakedCount > 0 + ? `${tweakedCount} value${tweakedCount !== 1 ? 's' : ''} adjusted from preset defaults` + : `${Object.keys(GLAMSTERDAM_PRESET).length} parameters will be applied`} + +
+ + +
+
+ } + > +
+ {/* Intro */} +
+

+ This preset applies all gas parameter changes proposed in{' '} + + EIP-8007 (Glamsterdam) + + + , the upcoming Ethereum execution layer upgrade. It bundles compute repricing, calldata floor increases, and + transaction base cost reductions into a single preset you can simulate against real blocks. +

+

+ Values below are pre-filled with the proposed changes. You can adjust any parameter before simulating. +

+
+ + {/* EIP Sections */} + {EIP_SECTIONS.map(section => ( +
+ {/* Section Header */} +
+
+
+ + {section.eip} + +

{section.name}

+ {section.placeholder && ( + + Values TBD + + )} +
+

{section.description}

+
+ + + +
+ + {/* Parameter Rows */} +
+ {section.parameters.map(param => { + const defaultValue = defaults?.parameters[param.key]?.value ?? 0; + const presetValue = GLAMSTERDAM_PRESET[param.key] ?? defaultValue; + const currentValue = values[param.key] ?? presetValue; + const isModified = currentValue !== defaultValue; + const isTweaked = currentValue !== presetValue; + const { min, max, step } = getSliderRange(defaultValue, presetValue); + + return ( +
+ {/* Label */} +
+ + {param.label} + +
+ + {/* Default → New indicator */} + + {defaultValue.toLocaleString()} + + + + {/* Number Input */} + handleParamChange(param.key, Number(e.target.value))} + className={clsx( + 'w-20 shrink-0 rounded-xs border bg-surface px-2 py-1 text-right font-mono text-xs focus:ring-1 focus:ring-primary focus:outline-hidden', + isModified ? 'border-primary/30 text-primary' : 'border-border text-foreground' + )} + /> + + {/* Slider */} + handleParamChange(param.key, Number(e.target.value))} + className={clsx( + 'min-w-0 flex-1 cursor-pointer appearance-none bg-transparent', + '[&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-xs [&::-webkit-slider-runnable-track]:bg-border', + '[&::-moz-range-track]:h-1.5 [&::-moz-range-track]:rounded-xs [&::-moz-range-track]:bg-border', + '[&::-webkit-slider-thumb]:-mt-1 [&::-webkit-slider-thumb]:size-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full', + isModified ? '[&::-webkit-slider-thumb]:bg-primary' : '[&::-webkit-slider-thumb]:bg-muted', + '[&::-moz-range-thumb]:size-3 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0', + isModified ? '[&::-moz-range-thumb]:bg-primary' : '[&::-moz-range-thumb]:bg-muted' + )} + /> + + {/* Reset to preset value button (only if tweaked) */} + {isTweaked && ( + + )} +
+ ); + })} +
+ + {/* Optional note about unsimulated parts */} + {section.note && ( +
+ +

{section.note}

+
+ )} +
+ ))} + + {/* Not Covered EIPs */} +
+
+ +

Not Simulated

+
+

+ The following EIPs in Glamsterdam involve structural protocol changes that cannot be expressed as gas + parameter overrides. Their effects are not reflected in this simulation. +

+
+ {UNSUPPORTED_EIPS.map(eip => ( +
+ + {eip.eip} + +
+
+ {eip.name} + + + +
+

{eip.reason}

+
+
+ ))} +
+
+
+ + ); +} diff --git a/src/pages/ethereum/execution/gas-profiler/components/GlamsterdamPresetModal/index.ts b/src/pages/ethereum/execution/gas-profiler/components/GlamsterdamPresetModal/index.ts new file mode 100644 index 000000000..c563a1ac2 --- /dev/null +++ b/src/pages/ethereum/execution/gas-profiler/components/GlamsterdamPresetModal/index.ts @@ -0,0 +1 @@ +export { GlamsterdamPresetModal } from './GlamsterdamPresetModal';