diff --git a/crates/contracts/src/precompiles/nonce.rs b/crates/contracts/src/precompiles/nonce.rs index 412e939260..a222e373d7 100644 --- a/crates/contracts/src/precompiles/nonce.rs +++ b/crates/contracts/src/precompiles/nonce.rs @@ -18,10 +18,12 @@ crate::sol! { /// Get the number of active nonce keys for an account /// @param account The account address /// @return count The number of nonce keys that have been used (nonce > 0) + /// @dev Deprecated: This function is kept for backwards compatibility pre-AllegroModerato function getActiveNonceKeyCount(address account) external view returns (uint256 count); // Events event NonceIncremented(address indexed account, uint256 indexed nonceKey, uint64 newNonce); + /// @dev Deprecated: This event is only emitted pre-AllegroModerato for backwards compatibility event ActiveKeyCountChanged(address indexed account, uint256 newCount); // Errors diff --git a/crates/node/tests/it/tempo_transaction.rs b/crates/node/tests/it/tempo_transaction.rs index 3adc725026..cd90564a67 100644 --- a/crates/node/tests/it/tempo_transaction.rs +++ b/crates/node/tests/it/tempo_transaction.rs @@ -2650,7 +2650,7 @@ async fn test_aa_estimate_gas_with_key_types() -> eyre::Result<()> { let baseline_gas: String = provider .raw_request( "eth_estimateGas".into(), - [serde_json::to_value(&base_tx_request())?], + [serde_json::to_value(base_tx_request())?], ) .await?; let baseline_gas_u64 = u64::from_str_radix(baseline_gas.trim_start_matches("0x"), 16)?; @@ -2748,7 +2748,7 @@ async fn test_aa_estimate_gas_with_keychain_and_key_auth() -> eyre::Result<()> { let baseline_gas: String = provider .raw_request( "eth_estimateGas".into(), - [serde_json::to_value(&base_tx_request())?], + [serde_json::to_value(base_tx_request())?], ) .await?; let baseline_gas_u64 = u64::from_str_radix(baseline_gas.trim_start_matches("0x"), 16)?; diff --git a/crates/precompiles/src/nonce/dispatch.rs b/crates/precompiles/src/nonce/dispatch.rs index d5bfbda61b..c50da66f22 100644 --- a/crates/precompiles/src/nonce/dispatch.rs +++ b/crates/precompiles/src/nonce/dispatch.rs @@ -24,10 +24,15 @@ impl Precompile for NonceManager { INonce::getNonceCall::SELECTOR => { view::(calldata, |call| self.get_nonce(call)) } + // Deprecated post-AllegroModerato: active key count is no longer tracked INonce::getActiveNonceKeyCountCall::SELECTOR => { - view::(calldata, |call| { - self.get_active_nonce_key_count(call) - }) + if self.storage.spec().is_allegro_moderato() { + unknown_selector(selector, self.storage.gas_used(), self.storage.spec()) + } else { + view::(calldata, |call| { + self.get_active_nonce_key_count(call) + }) + } } _ => unknown_selector(selector, self.storage.gas_used(), self.storage.spec()), }; diff --git a/crates/precompiles/src/nonce/mod.rs b/crates/precompiles/src/nonce/mod.rs index 84fc692a04..e2aecbd237 100644 --- a/crates/precompiles/src/nonce/mod.rs +++ b/crates/precompiles/src/nonce/mod.rs @@ -17,18 +17,19 @@ use alloy::primitives::{Address, U256}; /// ```solidity /// contract Nonce { /// mapping(address => mapping(uint256 => uint64)) public nonces; // slot 0 -/// mapping(address => uint256) public activeKeyCount; // slot 1 +/// mapping(address => uint256) public activeKeyCount; // slot 1 (deprecated post-AllegroModerato) /// } /// ``` /// /// - Slot 0: 2D nonce mapping - keccak256(abi.encode(nonce_key, keccak256(abi.encode(account, 0)))) -/// - Slot 1: Active key count - keccak256(abi.encode(account, 1)) +/// - Slot 1: Active key count - keccak256(abi.encode(account, 1)) (deprecated post-AllegroModerato) /// /// Note: Protocol nonce (key 0) is stored directly in account state, not here. /// Only user nonce keys (1-N) are managed by this precompile. #[contract(addr = NONCE_PRECOMPILE_ADDRESS)] pub struct NonceManager { nonces: Mapping>, + /// Deprecated post-AllegroModerato: tracks number of active nonce keys per account active_key_count: Mapping, } @@ -51,6 +52,9 @@ impl NonceManager { } /// Get the number of active user nonce keys for an account + /// + /// Deprecated: This function is only available pre-AllegroModerato for backwards compatibility. + /// Post-AllegroModerato, the dispatch layer returns unknown_selector error. pub fn get_active_nonce_key_count( &self, call: INonce::getActiveNonceKeyCountCall, @@ -66,8 +70,9 @@ impl NonceManager { let current = self.nonces.at(account).at(nonce_key).read()?; - // If transitioning from 0 to 1, increment active key count - if current == 0 { + // Pre-AllegroModerato: If transitioning from 0 to 1, increment active key count + // This is deprecated post-AllegroModerato where we use fixed gas pricing instead + if current == 0 && !self.storage.spec().is_allegro_moderato() { self.increment_active_key_count(account)?; } @@ -88,7 +93,11 @@ impl NonceManager { Ok(new_nonce) } - /// Increment the active key count for an account + /// Increment the active key count for an account (deprecated post-AllegroModerato) + /// + /// This function is only called pre-AllegroModerato to maintain backwards compatibility. + /// Post-AllegroModerato, we use fixed gas pricing based on whether the nonce slot is + /// zero or non-zero, rather than tracking the total count of active keys. fn increment_active_key_count(&mut self, account: Address) -> Result<()> { let current = self.active_key_count.at(account).read()?; @@ -98,7 +107,7 @@ impl NonceManager { self.active_key_count.at(account).write(new_count)?; - // Emit ActiveKeyCountChanged event (only after Moderato hardfork) + // Emit ActiveKeyCountChanged event (only between Moderato and AllegroModerato) if self.storage.spec().is_moderato() { self.emit_event(NonceEvent::ActiveKeyCountChanged( INonce::ActiveKeyCountChanged { diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index 44cddbf650..b935e3bd30 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -35,6 +35,8 @@ pub struct TempoEvm { pub logs: Vec, /// The fee collected in `collectFeePreTx` call. pub(crate) collected_fee: U256, + /// 2D nonce gas cost calculated during validation. + pub(crate) nonce_2d_gas: u64, } impl TempoEvm { @@ -68,6 +70,7 @@ impl TempoEvm { inner, logs: Vec::new(), collected_fee: U256::ZERO, + nonce_2d_gas: 0, } } } diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 24aa225128..0b8a7a4c5d 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -21,8 +21,8 @@ use revm::{ Gas, InitialAndFloorGas, gas::{ ACCESS_LIST_ADDRESS, ACCESS_LIST_STORAGE_KEY, CALLVALUE, COLD_ACCOUNT_ACCESS_COST, - COLD_SLOAD_COST, CREATE, STANDARD_TOKEN_COST, calc_tx_floor_cost, - get_tokens_in_calldata, initcode_cost, + COLD_SLOAD_COST, CREATE, SSTORE_SET, STANDARD_TOKEN_COST, WARM_SSTORE_RESET, + calc_tx_floor_cost, get_tokens_in_calldata, initcode_cost, }, interpreter::EthInterpreter, }, @@ -69,6 +69,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 SLOAD + warm SSTORE reset) +const EXISTING_NONCE_KEY_GAS: u64 = COLD_SLOAD_COST + WARM_SSTORE_RESET; + +/// Gas cost for using a new 2D nonce key (cold SLOAD + SSTORE set for 0 -> non-zero) +const NEW_NONCE_KEY_GAS: u64 = COLD_SLOAD_COST + SSTORE_SET; + /// Hashed account code of default 7702 delegate deployment const DEFAULT_7702_DELEGATE_CODE_HASH: B256 = b256!("e7b3e4597bdbdd0cc4eb42f9b799b580f23068f54e472bb802cb71efb1570482"); @@ -141,6 +147,38 @@ 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 (cold SLOAD + warm SSTORE reset) +/// - New user key (nonce == 0): 22,100 gas (cold SLOAD + SSTORE set) +#[inline] +fn calculate_2d_nonce_gas( + nonce_manager: &NonceManager, + caller: Address, + nonce_key: U256, +) -> Result { + // 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 - cold SLOAD + warm SSTORE reset + Ok(EXISTING_NONCE_KEY_GAS) + } else { + // New key - cold SLOAD + SSTORE set (0 -> non-zero) + Ok(NEW_NONCE_KEY_GAS) + } +} + /// Tempo EVM [`Handler`] implementation with Tempo specific modifications: /// /// Fees are paid in fee tokens instead of account balance. @@ -447,14 +485,20 @@ where evm: &mut Self::Evm, init_and_floor_gas: &InitialAndFloorGas, ) -> Result { + // 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, + ); + // 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,10 +643,26 @@ where // modify account nonce and touch the account. caller_account.touch(); + let nonce_2d_gas; + if !nonce_key.is_zero() { - StorageCtx::enter_evm(journal, block, cfg, || { + nonce_2d_gas = StorageCtx::enter_evm(journal, block, cfg, || { let mut nonce_manager = NonceManager::new(); + // Calculate 2D nonce gas (only post-AllegroModerato) + let gas = if cfg.spec.is_allegro_moderato() { + calculate_2d_nonce_gas(&nonce_manager, tx.caller(), nonce_key).map_err( + |err| match err { + TempoPrecompileError::Fatal(err) => EVMError::Custom(err), + err => { + TempoInvalidTransaction::NonceManagerError(err.to_string()).into() + } + }, + )? + } else { + 0 + }; + if !cfg.is_nonce_check_disabled() { let tx_nonce = tx.nonce(); let state = nonce_manager @@ -648,9 +708,10 @@ where err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(), })?; - Result::<(), EVMError>::Ok(()) + Ok::<_, EVMError>(gas) })?; } else { + nonce_2d_gas = 0; // Bump the nonce for calls. Nonce for CREATE will be bumped in `make_create_frame`. // // Always bump nonce for AA transactions. @@ -919,6 +980,7 @@ where } else { journal.checkpoint_commit(); evm.collected_fee = gas_balance_spending; + evm.nonce_2d_gas = nonce_2d_gas; Ok(()) } @@ -1339,14 +1401,20 @@ where evm: &mut Self::Evm, init_and_floor_gas: &InitialAndFloorGas, ) -> Result { + // 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) } } } @@ -1393,11 +1461,13 @@ mod tests { use alloy_primitives::{Address, U256}; use revm::{ Context, Journal, MainContext, + context::CfgEnv, database::{CacheDB, EmptyDB}, interpreter::instructions::utility::IntoU256, primitives::hardfork::SpecId, state::Account, }; + use std::convert::Infallible; use tempo_chainspec::hardfork::TempoHardfork; use tempo_precompiles::{DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, TIP_FEE_MANAGER_ADDRESS}; @@ -2120,4 +2190,51 @@ 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::::default(); + let caller = Address::random(); + + // Protocol nonce (key 0): always 0 gas + let gas = StorageCtx::enter_evm(&mut journal, &block, &cfg, || { + let nm = NonceManager::new(); + Ok::<_, EVMError>( + calculate_2d_nonce_gas(&nm, caller, U256::from(0)).unwrap(), + ) + }) + .unwrap(); + assert_eq!(gas, 0); + + // New key (nonce == 0): 22,100 gas (cold SLOAD + SSTORE set) + let gas = StorageCtx::enter_evm(&mut journal, &block, &cfg, || { + let nm = NonceManager::new(); + Ok::<_, EVMError>( + calculate_2d_nonce_gas(&nm, caller, U256::from(1)).unwrap(), + ) + }) + .unwrap(); + assert_eq!(gas, NEW_NONCE_KEY_GAS); + + // Increment the nonce to make it an existing key + StorageCtx::enter_evm(&mut journal, &block, &cfg, || { + NonceManager::new() + .increment_nonce(caller, U256::from(1)) + .unwrap(); + Ok::<_, EVMError>(()) + }) + .unwrap(); + + // Existing key (nonce > 0): 5,000 gas (cold SLOAD + warm SSTORE reset) + let gas = StorageCtx::enter_evm(&mut journal, &block, &cfg, || { + let nm = NonceManager::new(); + Ok::<_, EVMError>( + calculate_2d_nonce_gas(&nm, caller, U256::from(1)).unwrap(), + ) + }) + .unwrap(); + assert_eq!(gas, EXISTING_NONCE_KEY_GAS); + } } diff --git a/docs/pages/protocol/transactions/spec-tempo-transaction.mdx b/docs/pages/protocol/transactions/spec-tempo-transaction.mdx index 5f54928600..c676e5ed36 100644 --- a/docs/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/docs/pages/protocol/transactions/spec-tempo-transaction.mdx @@ -179,14 +179,12 @@ Each authorization in the list: #### Account State Changes - `nonces: mapping(uint256 => uint64)` - 2D nonce tracking -- `num_active_user_keys: uint` - tracks number of user keys for gas calculation **Implementation Note:** Nonces are stored in the storage of a designated precompile at address `0x4E4F4E4345000000000000000000000000000000` (ASCII hex for "NONCE"), as there is currently no clean way to extend account state in Reth. **Storage Layout at 0x4E4F4E4345:** - Storage key: `keccak256(abi.encode(account_address, nonce_key))` - Storage value: `nonce` (uint64) -- Active key count for account: stored at `keccak256(abi.encode(account_address, uint256(0)))` Note: Protocol Nonce key (0), is directly stored in the account state, just like normal transaction types. @@ -206,10 +204,6 @@ interface INonce { /// @param nonceKey The nonce key that was incremented /// @param newNonce The new nonce value after incrementing event NonceIncremented(address indexed account, uint256 indexed nonceKey, uint64 newNonce); - /// @notice Emitted when the active key count changes for an account - /// @param account The account whose active key count changed - /// @param newCount The new active key count - event ActiveKeyCountChanged(address indexed account, uint256 newCount); /// @notice Thrown when trying to access protocol nonce (key 0) through the precompile /// @dev Protocol nonce should be accessed through account state, not this precompile error ProtocolNonceNotSupported(); @@ -222,23 +216,17 @@ interface INonce { /// @param nonceKey The nonce key (must be > 0, protocol nonce key 0 not supported) /// @return nonce The current nonce value function getNonce(address account, uint256 nonceKey) external view returns (uint64 nonce); - /// @notice Get the number of active nonce keys for an account - /// @param account The account address - /// @return count The number of nonce keys that have been used (nonce > 0) - function getActiveNonceKeyCount(address account) external view returns (uint256 count); } ``` #### Precompile Implementation -The precompile contract maintains two primary storage mappings: +The precompile contract maintains a single storage mapping: ```solidity contract Nonce is INonce { /// @dev Mapping from account -> nonce key -> nonce value mapping(address => mapping(uint256 => uint64)) private nonces; - /// @dev Mapping from account -> count of active nonce keys - mapping(address => uint256) private activeKeyCount; } ``` @@ -246,16 +234,15 @@ contract Nonce is INonce { For transactions using nonce keys: -1. If `sequence > 0`: Add 5,000 gas to base cost (21,000) - - Rationale: Equivalent to a cold SSTORE on a non-zero slot (2,900 base + 2,100 cold access) +1. **Protocol nonce (key 0)**: No additional gas cost + - Uses the standard account nonce stored in account state -2. If `sequence == 0`: Add progressive cost - ```rust - let num_active_nonce_keys = count(non_zero_nonce_keys with sequence > 0) - base_gas_cost = 21_000 + num_active_nonce_keys * 20_000 - ``` +2. **Existing user key (nonce > 0)**: Add 5,000 gas to base cost + - Rationale: Cold SLOAD (2,100) + warm SSTORE reset (2,900) + +3. **New user key (nonce == 0)**: Add 22,100 gas to base cost + - Rationale: Cold SLOAD (2,100) + SSTORE set for 0 → non-zero (20,000) -This linearly increasing fee compensates for state growth and mitigates DOS vectors from unbounded sequence key creation. We specify the complete gas schedule in more detail in the [gas costs section](#gas-costs) ### Transaction Validation @@ -884,28 +871,20 @@ Transactions using parallelizable nonces incur additional costs based on the non - **Total:** 21,000 gas (base transaction cost) - **Rationale:** Maintains backward compatibility with existing transaction flow -#### Case 2: Existing User Nonce Key (sequence > 0) +#### Case 2: Existing User Nonce Key (nonce > 0) - **Additional Cost:** 5,000 gas - **Total:** 26,000 gas -- **Rationale:** Equivalent to a cold SSTORE on a non-zero slot (2,900 base + 2,100 cold access) - -#### Case 3: New User Nonce Key (sequence == 0) -- **Additional Cost:** Progressive based on active keys -- **Formula:** - ``` - additional_gas = num_active_nonce_keys * 20,000 - total_base_cost = 21,000 + additional_gas + signature_verification_cost - ``` -- **Examples:** - - First user key: 21,000 + 0 = 21,000 gas - - Second user key: 21,000 + 20,000 = 41,000 gas - - Third user key: 21,000 + 40,000 = 61,000 gas - -**Rationale for Progressive Pricing:** -1. **State Growth Compensation:** Each new nonce key adds permanent state that nodes must maintain -2. **DoS Prevention:** Linear cost increase prevents attackers from cheaply creating unbounded nonce keys -3. **Fair Usage:** Users who need higher parallel execution pay proportionally to their state footprint -4. **Storage Pattern Alignment:** Costs mirror actual storage operations (cold vs warm access patterns) +- **Rationale:** Cold SLOAD (2,100) + warm SSTORE reset (2,900) for incrementing an existing nonce + +#### Case 3: New User Nonce Key (nonce == 0) +- **Additional Cost:** 22,100 gas +- **Total:** 43,100 gas +- **Rationale:** Cold SLOAD (2,100) + SSTORE set (20,000) for writing to a new storage slot + +**Rationale for Fixed Pricing:** +1. **Simplicity:** Fixed costs based on actual EVM storage operations are straightforward to reason about +2. **Storage Pattern Alignment:** Costs directly mirror EVM cold SSTORE costs for new vs existing slots +3. **State Growth:** Creating new nonce keys incurs the higher cost naturally through SSTORE set pricing ### Key Authorization Gas Schedule @@ -1065,8 +1044,8 @@ def calculate_tempo_tx_base_gas(tx): # Constants BASE_TX_GAS = 21_000 - COLD_SSTORE_GAS = 5_000 - NEW_NONCE_KEY_MULTIPLIER = 20_000 + EXISTING_NONCE_KEY_GAS = 5_000 # Cold SLOAD (2,100) + warm SSTORE reset (2,900) + NEW_NONCE_KEY_GAS = 22_100 # Cold SLOAD (2,100) + SSTORE set (20,000) KEYCHAIN_VALIDATION_GAS = 3_000 # 2,100 SLOAD + 900 processing buffer # Step 1: Determine signature verification cost @@ -1088,15 +1067,14 @@ def calculate_tempo_tx_base_gas(tx): nonce_gas = 0 else: # User nonce key - current_sequence = get_nonce(tx.sender_address, tx.nonce_key) + current_nonce = get_nonce(tx.sender_address, tx.nonce_key) - if current_sequence > 0: - # Existing nonce key - nonce_gas = COLD_SSTORE_GAS + if current_nonce > 0: + # Existing nonce key - cold SLOAD + warm SSTORE reset + nonce_gas = EXISTING_NONCE_KEY_GAS else: - # New nonce key - progressive pricing - num_active_keys = num_active_user_keys(tx.sender_address) - nonce_gas = num_active_keys * NEW_NONCE_KEY_MULTIPLIER + # New nonce key - cold SLOAD + SSTORE set + nonce_gas = NEW_NONCE_KEY_GAS # Step 3: Calculate key authorization cost (if present) if tx.key_authorization is not None: @@ -1145,40 +1123,3 @@ Because a single transaction can invalidate multiple others by spending balances **Assessment:** While this transaction type introduces additional pre-execution validation costs, all costs are bounded to reasonable limits. The mempool complexity issues around cross-transaction dependencies already exist in Ethereum due to 7702 and accounts with code, making static validation inherently difficult. So the incremental cost from this transaction type is acceptable given these existing constraints. -### State Growth and Nonce Garbage Collection - -The 2D nonce system introduces some state growth concerns, as each account can create a large number of nonce keys. One discussed solution to this has been garbage collection of nonces after transaction expiry. -Current spec makes an *intentionally excludes garbage collection** for nonces. - -#### Rationale for Excluding Garbage Collection - -1. **Not Valuable in Isolation, but future compatible** -In the current implementation, each new nonce is stored in a precompile storage. So nonce state, growth is the exact same problem as general state growth. -So it is not valuable to enshrine a partial solution just for nonces, until we solve the broader state growth problem. - -2. **Progressive Gas Model Addresses State Growth** -The linearly increasing gas cost model provides economic pressure against state bloat: - - 1st new sequence key: 20,000 gas - - 2nd new sequence key: 40,000 gas - - Nth new sequence key: N × 20,000 gas -This creates a **practical economic limit** on the number of sequence keys per account. We can also introduce a protocol limit of 32 or 64 nonce keys for each account. - -3. **Technical Complexity with Nonce Keys** -It is unclear how garbage collection would work safely with sequence-based nonces -Example: If a nonce key is at sequence X and a wallet signs for X+1, but X gets garbage collected before submission, the transaction with X+1 would fail unexpectedly - -4. **Future Extensibility** -The specification includes an optional `validBefore` field in the transaction structure -If garbage collection becomes necessary, this field can be made mandatory - -#### State Growth Analysis - -**Worst Case Scenario:** -- An attacker willing to pay increasing gas costs could create many nonce keys -- However, the linear cost model makes this economically prohibitive at scale -- Example: Creating 100 nonce keys would require cumulative gas costs of ~5,000,000 gas just for the nonce key creation - - -**Practical Usage:** -- Most users will use 1-5 parallel nonce keys for typical parallel transaction patterns -- Power users requiring higher parallelism will pay proportionally diff --git a/docs/specs/src/Nonce.sol b/docs/specs/src/Nonce.sol index 1f42646b58..075140a478 100644 --- a/docs/specs/src/Nonce.sol +++ b/docs/specs/src/Nonce.sol @@ -12,12 +12,10 @@ import { INonce } from "./interfaces/INonce.sol"; /// ```solidity /// contract Nonce { /// mapping(address => mapping(uint256 => uint64)) public nonces; // slot 0 -/// mapping(address => uint256) public activeKeyCount; // slot 1 /// } /// ``` /// /// - Slot 0: 2D nonce mapping - keccak256(abi.encode(nonce_key, keccak256(abi.encode(account, 0)))) -/// - Slot 1: Active key count - keccak256(abi.encode(account, 1)) contract Nonce is INonce { // ============ Storage Mappings ============ @@ -25,9 +23,6 @@ contract Nonce is INonce { /// @dev Mapping from account -> nonce key -> nonce value mapping(address => mapping(uint256 => uint64)) private nonces; - /// @dev Mapping from account -> count of active nonce keys - mapping(address => uint256) private activeKeyCount; - // ============ View Functions ============ /// @inheritdoc INonce @@ -41,11 +36,6 @@ contract Nonce is INonce { return nonces[account][nonceKey]; } - /// @inheritdoc INonce - function getActiveNonceKeyCount(address account) external view returns (uint256 count) { - return activeKeyCount[account]; - } - // ============ Internal Functions ============ /// @notice Internal function to increment nonce for a specific account and nonce key @@ -60,12 +50,6 @@ contract Nonce is INonce { uint64 currentNonce = nonces[account][nonceKey]; - // If transitioning from 0 to 1, increment active key count - if (currentNonce == 0) { - activeKeyCount[account]++; - emit ActiveKeyCountChanged(account, activeKeyCount[account]); - } - // Check for overflow if (currentNonce == type(uint64).max) { revert NonceOverflow(); diff --git a/docs/specs/src/interfaces/INonce.sol b/docs/specs/src/interfaces/INonce.sol index 6a98b9edd8..baf3aedd74 100644 --- a/docs/specs/src/interfaces/INonce.sol +++ b/docs/specs/src/interfaces/INonce.sol @@ -14,11 +14,6 @@ interface INonce { /// @param newNonce The new nonce value after incrementing event NonceIncremented(address indexed account, uint256 indexed nonceKey, uint64 newNonce); - /// @notice Emitted when the active key count changes for an account - /// @param account The account whose active key count changed - /// @param newCount The new active key count - event ActiveKeyCountChanged(address indexed account, uint256 newCount); - /// @notice Thrown when trying to access protocol nonce (key 0) through the precompile /// @dev Protocol nonce should be accessed through account state, not this precompile error ProtocolNonceNotSupported(); @@ -35,9 +30,4 @@ interface INonce { /// @return nonce The current nonce value function getNonce(address account, uint256 nonceKey) external view returns (uint64 nonce); - /// @notice Get the number of active nonce keys for an account - /// @param account The account address - /// @return count The number of nonce keys that have been used (nonce > 0) - function getActiveNonceKeyCount(address account) external view returns (uint256 count); - } diff --git a/docs/specs/test/Nonce.t.sol b/docs/specs/test/Nonce.t.sol index 5bb8ac552c..1188675914 100644 --- a/docs/specs/test/Nonce.t.sol +++ b/docs/specs/test/Nonce.t.sol @@ -15,11 +15,9 @@ contract NonceTest is BaseTest { // Storage slots from Nonce contract uint256 private constant NONCES_SLOT = 0; - uint256 private constant ACTIVE_KEY_COUNT_SLOT = 1; // Events from INonce (for event testing, though vm.store won't emit them) event NonceIncremented(address indexed account, uint256 indexed nonceKey, uint64 newNonce); - event ActiveKeyCountChanged(address indexed account, uint256 newCount); /// @dev Helper function to increment nonce using direct storage manipulation /// This works for both precompile and deployed Solidity contract @@ -40,18 +38,6 @@ contract NonceTest is BaseTest { // Read current nonce value uint64 currentNonce = uint64(uint256(vm.load(_NONCE, nonceSlot))); - // If transitioning from 0 to 1, increment active key count - if (currentNonce == 0) { - // Calculate storage slot for activeKeyCount[account] - bytes32 activeCountSlot = keccak256(abi.encode(account, ACTIVE_KEY_COUNT_SLOT)); - - // Read current active count - uint256 currentCount = uint256(vm.load(_NONCE, activeCountSlot)); - - // Increment and store - vm.store(_NONCE, activeCountSlot, bytes32(currentCount + 1)); - } - // Check for overflow require(currentNonce < type(uint64).max, "Nonce overflow"); @@ -86,11 +72,6 @@ contract NonceTest is BaseTest { } } - function test_GetActiveNonceKeyCount_ReturnsZeroInitially() public view { - uint256 count = nonce.getActiveNonceKeyCount(testAlice); - assertEq(count, 0, "Initial active key count should be 0"); - } - // ============ Increment Nonce Tests ============ function test_IncrementNonce_FirstIncrement() public { @@ -99,7 +80,6 @@ contract NonceTest is BaseTest { assertEq(newNonce, 1, "First increment should return 1"); assertEq(nonce.getNonce(testAlice, 5), 1, "Nonce should be stored as 1"); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 1, "Active key count should be 1"); } function test_IncrementNonce_MultipleIncrements() public { @@ -109,29 +89,24 @@ contract NonceTest is BaseTest { } assertEq(nonce.getNonce(testAlice, 5), 10, "Final nonce should be 10"); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 1, "Active key count should still be 1"); } function test_IncrementNonce_DifferentKeys() public { // Increment key 1 uint64 nonce1 = _incrementNonceViaStorage(testAlice, 1); assertEq(nonce1, 1, "Key 1 first nonce should be 1"); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 1, "Active count should be 1"); // Increment key 2 uint64 nonce2 = _incrementNonceViaStorage(testAlice, 2); assertEq(nonce2, 1, "Key 2 first nonce should be 1"); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 2, "Active count should be 2"); // Increment key 1 again nonce1 = _incrementNonceViaStorage(testAlice, 1); assertEq(nonce1, 2, "Key 1 second nonce should be 2"); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 2, "Active count should still be 2"); // Increment key 3 uint64 nonce3 = _incrementNonceViaStorage(testAlice, 3); assertEq(nonce3, 1, "Key 3 first nonce should be 1"); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 3, "Active count should be 3"); } function test_IncrementNonce_RevertIf_ProtocolKey() public { @@ -171,8 +146,6 @@ contract NonceTest is BaseTest { // Check they're independent assertEq(nonce.getNonce(testAlice, 5), 10, "Alice's nonce should be 10"); assertEq(nonce.getNonce(testBob, 5), 20, "Bob's nonce should be 20"); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 1, "Alice should have 1 active key"); - assertEq(nonce.getActiveNonceKeyCount(testBob), 1, "Bob should have 1 active key"); } function test_DifferentAccounts_DifferentKeys() public { @@ -188,10 +161,6 @@ contract NonceTest is BaseTest { // Charlie uses key 1 _incrementNonceViaStorage(testCharlie, 1); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 3, "Alice should have 3 active keys"); - assertEq(nonce.getActiveNonceKeyCount(testBob), 2, "Bob should have 2 active keys"); - assertEq(nonce.getActiveNonceKeyCount(testCharlie), 1, "Charlie should have 1 active key"); - // Verify independence assertEq(nonce.getNonce(testAlice, 1), 1, "Alice key 1 should be 1"); assertEq(nonce.getNonce(testCharlie, 1), 1, "Charlie key 1 should be 1"); @@ -199,42 +168,6 @@ contract NonceTest is BaseTest { assertEq(nonce.getNonce(testBob, 1), 0, "Bob key 1 should be 0"); } - // ============ Active Key Count Tests ============ - - function test_ActiveKeyCount_IncrementsOnlyOnFirstUse() public { - assertEq(nonce.getActiveNonceKeyCount(testAlice), 0, "Should start at 0"); - - // First use of key 1 - _incrementNonceViaStorage(testAlice, 1); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 1, "Should be 1 after first key"); - - // Second use of key 1 (shouldn't increment count) - _incrementNonceViaStorage(testAlice, 1); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 1, "Should still be 1"); - - // Third use of key 1 - _incrementNonceViaStorage(testAlice, 1); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 1, "Should still be 1"); - - // First use of key 2 - _incrementNonceViaStorage(testAlice, 2); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 2, "Should be 2 after second key"); - } - - function test_ActiveKeyCount_MultipleKeys() public { - uint256[] memory keys = new uint256[](5); - keys[0] = 1; - keys[1] = 10; - keys[2] = 100; - keys[3] = 1000; - keys[4] = 10_000; - - for (uint256 i = 0; i < keys.length; i++) { - _incrementNonceViaStorage(testAlice, keys[i]); - assertEq(nonce.getActiveNonceKeyCount(testAlice), i + 1, "Active count should match"); - } - } - // ============ Event Tests ============ // Note: Event tests are not included because vm.store() doesn't emit events. // The protocol implementation will emit events, but we test functionality here. @@ -289,43 +222,6 @@ contract NonceTest is BaseTest { assertEq(nonce.getNonce(account2, nonceKey), count2, "Account2 nonce should match count2"); } - function testFuzz_ActiveKeyCount(address account, uint256[] memory nonceKeys) public { - vm.assume(nonceKeys.length > 0 && nonceKeys.length <= 20); - - // Deduplicate and filter nonce keys - uint256[] memory uniqueKeys = new uint256[](nonceKeys.length); - uint256 uniqueCount = 0; - - for (uint256 i = 0; i < nonceKeys.length; i++) { - if (nonceKeys[i] == 0) continue; // Skip protocol nonce - - bool isDuplicate = false; - for (uint256 j = 0; j < uniqueCount; j++) { - if (uniqueKeys[j] == nonceKeys[i]) { - isDuplicate = true; - break; - } - } - - if (!isDuplicate) { - uniqueKeys[uniqueCount] = nonceKeys[i]; - uniqueCount++; - } - } - - // Increment each unique key once - for (uint256 i = 0; i < uniqueCount; i++) { - _incrementNonceViaStorage(account, uniqueKeys[i]); - } - - // Active count should match unique key count - assertEq( - nonce.getActiveNonceKeyCount(account), - uniqueCount, - "Active key count should match unique keys used" - ); - } - // ============ Edge Case Tests ============ function test_EdgeCase_MaxNonceKey() public { @@ -352,8 +248,6 @@ contract NonceTest is BaseTest { _incrementNonceViaStorage(testAlice, i); } - assertEq(nonce.getActiveNonceKeyCount(testAlice), 50, "Should track 50 active keys"); - // Verify each key has nonce 1 for (uint256 i = 1; i <= 50; i++) { assertEq(nonce.getNonce(testAlice, i), 1, "Each key should have nonce 1"); @@ -372,7 +266,6 @@ contract NonceTest is BaseTest { assertEq(nonce.getNonce(testAlice, 1), 10, "Key 1 should have nonce 10"); assertEq(nonce.getNonce(testAlice, 2), 10, "Key 2 should have nonce 10"); - assertEq(nonce.getActiveNonceKeyCount(testAlice), 2, "Should have 2 active keys"); } // ============ Gas Tests ============ @@ -386,21 +279,12 @@ contract NonceTest is BaseTest { assertTrue(gasUsed > 0, "Should consume some gas"); } - function test_Gas_GetActiveNonceKeyCount() public view { - uint256 gasBefore = gasleft(); - nonce.getActiveNonceKeyCount(testAlice); - uint256 gasUsed = gasBefore - gasleft(); - - assertTrue(gasUsed > 0, "Should consume some gas"); - } - function test_Gas_IncrementNonce_FirstTime() public { uint256 gasBefore = gasleft(); _incrementNonceViaStorage(testAlice, 1); uint256 gasUsed = gasBefore - gasleft(); assertTrue(gasUsed > 0, "Should consume gas"); - // First increment should cost more due to active count update } function test_Gas_IncrementNonce_Subsequent() public { @@ -411,7 +295,6 @@ contract NonceTest is BaseTest { uint256 gasUsed = gasBefore - gasleft(); assertTrue(gasUsed > 0, "Should consume gas"); - // Subsequent increments should cost less (no active count update) } }