Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/revm/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub struct TempoEvm<DB: Database, I> {
pub logs: Vec<Log>,
/// 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<DB: Database, I> TempoEvm<DB, I> {
Expand Down Expand Up @@ -68,6 +70,7 @@ impl<DB: Database, I> TempoEvm<DB, I> {
inner,
logs: Vec::new(),
collected_fee: U256::ZERO,
nonce_2d_gas: 0,
}
}
}
Expand Down
130 changes: 125 additions & 5 deletions crates/revm/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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;
Comment thread
klkvr marked this conversation as resolved.
Outdated

/// 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");
Expand Down Expand Up @@ -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)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 22100 usually, why does it have to scale with number of active keys?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
This comes with the added benefit that we don't have to keep an active nonce key count.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
When we update the new state creation gas costs, we can update this accordingly

}

/// Tempo EVM [`Handler`] implementation with Tempo specific modifications:
///
/// Fees are paid in fee tokens instead of account balance.
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @rakita validate_initial_tx_gas should take &mut Evm so that we don't have to do this hack


// 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)
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -924,6 +987,7 @@ where
} else {
journal.checkpoint_commit();
evm.collected_fee = gas_balance_spending;
evm.nonce_2d_gas = nonce_2d_gas;

Ok(())
}
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
12 changes: 6 additions & 6 deletions docs/pages/protocol/transactions/spec-tempo-transaction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ For transactions using nonce keys:
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
base_gas_cost = 21_000 + (num_active_nonce_keys + 1) * 20_000
```

This linearly increasing fee compensates for state growth and mitigates DOS vectors from unbounded sequence key creation.
Expand Down Expand Up @@ -893,13 +893,13 @@ Transactions using parallelizable nonces incur additional costs based on the non
- **Additional Cost:** Progressive based on active keys
- **Formula:**
```
additional_gas = num_active_nonce_keys * 20,000
additional_gas = (num_active_nonce_keys + 1) * 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
- First user key: 21,000 + 20,000 = 41,000 gas
- Second user key: 21,000 + 40,000 = 61,000 gas
- Third user key: 21,000 + 60,000 = 81,000 gas

**Rationale for Progressive Pricing:**
1. **State Growth Compensation:** Each new nonce key adds permanent state that nodes must maintain
Expand Down Expand Up @@ -1096,7 +1096,7 @@ def calculate_tempo_tx_base_gas(tx):
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
nonce_gas = (num_active_keys + 1) * NEW_NONCE_KEY_MULTIPLIER

# Step 3: Calculate key authorization cost (if present)
if tx.key_authorization is not None:
Expand Down
Loading