-
Notifications
You must be signed in to change notification settings - Fork 289
fix(revm): 2d nonce gas schedule #1442
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
19a617e
9bb1a70
c49aa4f
f58ed28
21d6103
5ff37fc
13f8785
c3c60f2
0251974
a03ab64
e7eb5ca
fde8faf
8433963
d7dde29
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,10 @@ use tempo_contracts::{ | |
| use tempo_precompiles::{ | ||
| account_keychain::{AccountKeychain, TokenLimit, authorizeKeyCall}, | ||
| error::TempoPrecompileError, | ||
| nonce::{INonce::getNonceCall, NonceManager}, | ||
| nonce::{ | ||
| INonce::{getActiveNonceKeyCountCall, getNonceCall}, | ||
| NonceManager, | ||
| }, | ||
| storage::{evm::EvmPrecompileStorageProvider, slots::mapping_slot}, | ||
| tip_fee_manager::TipFeeManager, | ||
| tip20::{self, ITIP20::InsufficientBalance, TIP20Error}, | ||
|
|
@@ -69,6 +72,12 @@ const KEY_AUTH_BASE_GAS: u64 = 27_000; | |
| /// Gas per spending limit in KeyAuthorization | ||
| const KEY_AUTH_PER_LIMIT_GAS: u64 = 22_000; | ||
|
|
||
| /// Gas cost for using an existing 2D nonce key (cold SSTORE on non-zero slot: 2,900 base + 2,100 cold access) | ||
| const EXISTING_NONCE_KEY_GAS: u64 = 5_000; | ||
|
|
||
| /// Gas multiplier per active nonce key when creating a new key (compensates for state growth) | ||
| const NEW_NONCE_KEY_MULTIPLIER: u64 = 20_000; | ||
|
|
||
| /// Hashed account code of default 7702 delegate deployment | ||
| const DEFAULT_7702_DELEGATE_CODE_HASH: B256 = | ||
| b256!("e7b3e4597bdbdd0cc4eb42f9b799b580f23068f54e472bb802cb71efb1570482"); | ||
|
|
@@ -141,6 +150,43 @@ fn calculate_key_authorization_gas( | |
| KEY_AUTH_BASE_GAS + sig_gas + limits_gas | ||
| } | ||
|
|
||
| /// Calculates the gas cost for 2D nonce usage. | ||
| /// | ||
| /// Gas schedule (post-AllegroModerato): | ||
| /// - Protocol nonce (key 0): 0 gas (no additional cost) | ||
| /// - Existing user key (nonce > 0): 5,000 gas | ||
| /// - New user key (nonce == 0): (num_active_keys + 1) * 20,000 gas | ||
| #[inline] | ||
| fn calculate_2d_nonce_gas( | ||
| nonce_manager: &mut NonceManager< | ||
| '_, | ||
| impl tempo_precompiles::storage::PrecompileStorageProvider, | ||
| >, | ||
| caller: Address, | ||
| nonce_key: U256, | ||
| ) -> Result<u64, TempoPrecompileError> { | ||
| // Protocol nonce (key 0) - no additional cost | ||
| if nonce_key.is_zero() { | ||
| return Ok(0); | ||
| } | ||
|
|
||
| // Get current nonce for this key | ||
| let current_nonce = nonce_manager.get_nonce(getNonceCall { | ||
| account: caller, | ||
| nonceKey: nonce_key, | ||
| })?; | ||
|
|
||
| if current_nonce > 0 { | ||
| // Existing key - fixed cost | ||
| Ok(EXISTING_NONCE_KEY_GAS) | ||
| } else { | ||
| // New key - progressive cost based on number of active keys | ||
| let active_count = nonce_manager | ||
| .get_active_nonce_key_count(getActiveNonceKeyCountCall { account: caller })?; | ||
| Ok((active_count.saturating_to::<u64>() + 1) * NEW_NONCE_KEY_MULTIPLIER) | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm that seems super expensive and kinda breaks the usecase of using random nonce keys to not care about your nonces after all it's just a cold sstore to non-existent slot which costs
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initially when we designed this dan and georgios were concerned about state bloat. We were discussing adding garbage collection and state rent, but unlike other state 2d nonces can never be garbage collected because it would lead to tx replay attacks. So we wanted to limit the number of nonce keys a user could create.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think some of those concerns exist. But since we are relaxing our requirements for state bloat solutions, we can probably also relax them for 2d nonces.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have updated this to remove this linear gas cost for now, as I think the perf benefits of not having to keep track of the active nonce key count, alone makes it worth it. |
||
| } | ||
|
|
||
| /// Tempo EVM [`Handler`] implementation with Tempo specific modifications: | ||
| /// | ||
| /// Fees are paid in fee tokens instead of account balance. | ||
|
|
@@ -447,14 +493,20 @@ where | |
| evm: &mut Self::Evm, | ||
| init_and_floor_gas: &InitialAndFloorGas, | ||
| ) -> Result<FrameResult, Self::Error> { | ||
| // Add 2D nonce gas to the initial gas | ||
| let adjusted_gas = InitialAndFloorGas::new( | ||
| init_and_floor_gas.initial_gas + evm.nonce_2d_gas, | ||
| init_and_floor_gas.floor_gas, | ||
| ); | ||
|
Comment on lines
+488
to
+492
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @rakita |
||
|
|
||
| // Check if this is an AA transaction by checking for tempo_tx_env | ||
| if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() { | ||
| // AA transaction - use batch execution with calls field | ||
| let calls = tempo_tx_env.aa_calls.clone(); | ||
| self.execute_multi_call(evm, init_and_floor_gas, calls) | ||
| self.execute_multi_call(evm, &adjusted_gas, calls) | ||
| } else { | ||
| // Standard transaction - use single-call execution | ||
| self.execute_single_call(evm, init_and_floor_gas) | ||
| self.execute_single_call(evm, &adjusted_gas) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -599,11 +651,22 @@ where | |
| // modify account nonce and touch the account. | ||
| caller_account.touch(); | ||
|
|
||
| let mut nonce_2d_gas = 0u64; | ||
|
|
||
| if !nonce_key.is_zero() { | ||
| let internals = EvmInternals::new(journal, block); | ||
| let mut storage_provider = EvmPrecompileStorageProvider::new_max_gas(internals, cfg); | ||
| let mut nonce_manager = NonceManager::new(&mut storage_provider); | ||
|
|
||
| // Calculate 2D nonce gas | ||
| if cfg.spec.is_allegro_moderato() { | ||
| nonce_2d_gas = calculate_2d_nonce_gas(&mut nonce_manager, tx.caller(), nonce_key) | ||
| .map_err(|err| match err { | ||
| TempoPrecompileError::Fatal(err) => EVMError::Custom(err), | ||
| err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(), | ||
| })?; | ||
| } | ||
|
|
||
| if !cfg.is_nonce_check_disabled() { | ||
| let tx_nonce = tx.nonce(); | ||
| let state = nonce_manager | ||
|
|
@@ -924,6 +987,7 @@ where | |
| } else { | ||
| journal.checkpoint_commit(); | ||
| evm.collected_fee = gas_balance_spending; | ||
| evm.nonce_2d_gas = nonce_2d_gas; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
@@ -1340,14 +1404,20 @@ where | |
| evm: &mut Self::Evm, | ||
| init_and_floor_gas: &InitialAndFloorGas, | ||
| ) -> Result<FrameResult, Self::Error> { | ||
| // Add 2D nonce gas to the initial gas (calculated in validate_against_state_and_deduct_caller) | ||
| let adjusted_gas = InitialAndFloorGas::new( | ||
| init_and_floor_gas.initial_gas + evm.nonce_2d_gas, | ||
| init_and_floor_gas.floor_gas, | ||
| ); | ||
|
|
||
| // Check if this is an AA transaction by checking for tempo_tx_env | ||
| if let Some(tempo_tx_env) = evm.ctx().tx().tempo_tx_env.as_ref() { | ||
| // AA transaction - use batch execution with calls field | ||
| let calls = tempo_tx_env.aa_calls.clone(); | ||
| self.inspect_execute_multi_call(evm, init_and_floor_gas, calls) | ||
| self.inspect_execute_multi_call(evm, &adjusted_gas, calls) | ||
| } else { | ||
| // Standard transaction - use single-call execution | ||
| self.inspect_execute_single_call(evm, init_and_floor_gas) | ||
| self.inspect_execute_single_call(evm, &adjusted_gas) | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1394,6 +1464,7 @@ mod tests { | |
| use alloy_primitives::{Address, U256}; | ||
| use revm::{ | ||
| Context, Journal, MainContext, | ||
| context::CfgEnv, | ||
| database::{CacheDB, EmptyDB}, | ||
| interpreter::instructions::utility::IntoU256, | ||
| primitives::hardfork::SpecId, | ||
|
|
@@ -2122,4 +2193,53 @@ mod tests { | |
| "Gas with key auth should match expected" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_2d_nonce_gas_schedule() { | ||
| let mut journal = create_test_journal(); | ||
| let block = TempoBlockEnv::default(); | ||
| let cfg = CfgEnv::<TempoHardfork>::default(); | ||
| let caller = Address::random(); | ||
|
|
||
| let calc_gas = |journal: &mut Journal<CacheDB<EmptyDB>>, key: u64| { | ||
| let internals = reth_evm::EvmInternals::new(journal, &block); | ||
| let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &cfg); | ||
| let mut nm = NonceManager::new(&mut storage); | ||
| calculate_2d_nonce_gas(&mut nm, caller, U256::from(key)).unwrap() | ||
| }; | ||
|
|
||
| let incr = |journal: &mut Journal<CacheDB<EmptyDB>>, key: u64| { | ||
| let internals = reth_evm::EvmInternals::new(journal, &block); | ||
| let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &cfg); | ||
| NonceManager::new(&mut storage) | ||
| .increment_nonce(caller, U256::from(key)) | ||
| .unwrap(); | ||
| }; | ||
|
|
||
| // Protocol nonce (key 0): always 0 gas | ||
| assert_eq!(calc_gas(&mut journal, 0), 0); | ||
|
|
||
| // New key with 0 active keys: (0+1) * 20,000 = 20,000 gas | ||
| assert_eq!(calc_gas(&mut journal, 1), NEW_NONCE_KEY_MULTIPLIER); | ||
|
|
||
| // Activate key 1, now existing key: 5,000 gas | ||
| incr(&mut journal, 1); | ||
| assert_eq!(calc_gas(&mut journal, 1), EXISTING_NONCE_KEY_GAS); | ||
|
|
||
| // New key with 1 active: (1+1) * 20,000 = 40,000 gas | ||
| assert_eq!(calc_gas(&mut journal, 2), 2 * NEW_NONCE_KEY_MULTIPLIER); | ||
|
|
||
| // Activate key 2, new key with 2 active: (2+1) * 20,000 = 60,000 gas | ||
| incr(&mut journal, 2); | ||
| assert_eq!(calc_gas(&mut journal, 3), 3 * NEW_NONCE_KEY_MULTIPLIER); | ||
|
|
||
| // Activate keys 3-10, test scaling: 10 active -> (10+1) * 20,000 = 220,000 gas | ||
| for i in 3..=10 { | ||
| incr(&mut journal, i); | ||
| } | ||
| assert_eq!(calc_gas(&mut journal, 11), 11 * NEW_NONCE_KEY_MULTIPLIER); | ||
|
|
||
| // Existing key still 5,000 regardless of active count | ||
| assert_eq!(calc_gas(&mut journal, 5), EXISTING_NONCE_KEY_GAS); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.