Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions crates/contracts/src/precompiles/nonce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/node/tests/it/tempo_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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)?;
Expand Down
11 changes: 8 additions & 3 deletions crates/precompiles/src/nonce/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ impl Precompile for NonceManager {
INonce::getNonceCall::SELECTOR => {
view::<INonce::getNonceCall>(calldata, |call| self.get_nonce(call))
}
// Deprecated post-AllegroModerato: active key count is no longer tracked
INonce::getActiveNonceKeyCountCall::SELECTOR => {
view::<INonce::getActiveNonceKeyCountCall>(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::<INonce::getActiveNonceKeyCountCall>(calldata, |call| {
self.get_active_nonce_key_count(call)
})
}
}
_ => unknown_selector(selector, self.storage.gas_used(), self.storage.spec()),
};
Expand Down
21 changes: 15 additions & 6 deletions crates/precompiles/src/nonce/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Address, Mapping<U256, u64>>,
/// Deprecated post-AllegroModerato: tracks number of active nonce keys per account
active_key_count: Mapping<Address, U256>,
}

Expand All @@ -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,
Expand All @@ -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)?;
}

Expand All @@ -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()?;

Expand All @@ -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 {
Expand Down
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
133 changes: 125 additions & 8 deletions crates/revm/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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<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 - 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.
Expand Down Expand Up @@ -447,14 +485,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,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
Expand Down Expand Up @@ -648,9 +708,10 @@ where
err => TempoInvalidTransaction::NonceManagerError(err.to_string()).into(),
})?;

Result::<(), EVMError<DB::Error, TempoInvalidTransaction>>::Ok(())
Ok::<_, EVMError<DB::Error, TempoInvalidTransaction>>(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.
Expand Down Expand Up @@ -919,6 +980,7 @@ where
} else {
journal.checkpoint_commit();
evm.collected_fee = gas_balance_spending;
evm.nonce_2d_gas = nonce_2d_gas;

Ok(())
}
Expand Down Expand Up @@ -1339,14 +1401,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 @@ -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};

Expand Down Expand Up @@ -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::<TempoHardfork>::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<Infallible, TempoInvalidTransaction>>(
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<Infallible, TempoInvalidTransaction>>(
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<Infallible, TempoInvalidTransaction>>(())
})
.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<Infallible, TempoInvalidTransaction>>(
calculate_2d_nonce_gas(&nm, caller, U256::from(1)).unwrap(),
)
})
.unwrap();
assert_eq!(gas, EXISTING_NONCE_KEY_GAS);
}
}
Loading
Loading